diff --git a/.babelrc.js b/.babelrc.js index 52fd762007e..add243a5b5d 100644 --- a/.babelrc.js +++ b/.babelrc.js @@ -1,26 +1 @@ - -let path = require('path'); - -function useLocal(module) { - return require.resolve(module, { - paths: [ - __dirname - ] - }) -} - -module.exports = { - "presets": [ - [ - useLocal('@babel/preset-env'), - { - "useBuiltIns": "entry", - "corejs": "3.13.0" - } - ] - ], - "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 803140dbe0b..c11f87b6f59 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -7,7 +7,7 @@ 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 @@ -16,71 +16,72 @@ aliases: working_directory: ~/Prebid.js - &restore_dep_cache - keys: - - v1-dependencies-{{ checksum "package.json" }} - # fallback to using the latest cache if no exact match is found - - v1-dependencies- + keys: + - v1-dependencies-{{ checksum "package.json" }} + # fallback to using the latest cache if no exact match is found + - v1-dependencies- - &save_dep_cache - paths: - - node_modules - key: v1-dependencies-{{ checksum "package.json" }} + paths: + - node_modules + key: v1-dependencies-{{ checksum "package.json" }} - &install - name: Install gulp cli - command: sudo npm install -g gulp-cli + name: Install gulp cli + command: sudo npm install -g gulp-cli -version: 2.1 -orbs: - aws-s3: circleci/aws-s3@2.0.0 + - &run_unit_test + name: BrowserStack testing + command: gulp test --browserstack --nolintfix + + - &run_endtoend_test + name: BrowserStack End to end testing + command: gulp e2e-test + + # Download and run BrowserStack local + - &setup_browserstack + name : Download BrowserStack Local binary and start it. + command : | + # Download the browserstack binary file + wget "https://www.browserstack.com/browserstack-local/BrowserStackLocal-linux-x64.zip" + # Unzip it + unzip BrowserStackLocal-linux-x64.zip + # Run the file with user's access key + ./BrowserStackLocal ${BROWSERSTACK_ACCESS_KEY} & + + - &unit_test_steps + - checkout + - restore_cache: *restore_dep_cache + - run: npm ci + - save_cache: *save_dep_cache + - run: *install + - run: *setup_browserstack + - run: *run_unit_test + + - &endtoend_test_steps + - checkout + - restore_cache: *restore_dep_cache + - run: npm install + - save_cache: *save_dep_cache + - run: *install + - run: *setup_browserstack + - run: *run_endtoend_test + +version: 2 jobs: build: <<: *environment - steps: - - checkout - - run: echo "export UPLOAD_DEST='s3://embedproduction/files/instbid-$(git describe --tags).js'" >> "$BASH_ENV" - - restore_cache: *restore_dep_cache - - run: npm install - - save_cache: *save_dep_cache - - run: *install - - run: - name: Build Prebid.js - command: gulp build --modules=modules.json - - aws-s3/copy: - from: build/dist/prebid.js - to: '"$UPLOAD_DEST"' - arguments: --cache-control 'max-age=86400' - build_v6: - <<: *environment - steps: - - checkout - - run: echo "export UPLOAD_DEST='s3://embedproduction/files/instbid-$(git describe --tags).js'" >> "$BASH_ENV" - - restore_cache: *restore_dep_cache - - run: npm install - - save_cache: *save_dep_cache - - run: *install - - run: - name: Build Prebid.js version 6 - command: gulp build --modules=modules.json - - aws-s3/copy: - from: build/dist/prebid.js - to: '"$UPLOAD_DEST"' - arguments: --cache-control 'max-age=86400' + steps: *unit_test_steps + e2etest: + <<: *environment + steps: *endtoend_test_steps workflows: version: 2 - build: + commit: jobs: - - build: - context: org-aws - filters: - branches: - only: - - master - - build_v6: - context: org-aws - filters: - branches: - only: - - prebid6 + - build + +experimental: + pipelines: true diff --git a/.eslintrc.js b/.eslintrc.js index d3379d70919..95515e8ba98 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -45,12 +45,19 @@ module.exports = { 'no-throw-literal': 'off', 'no-undef': 2, 'no-useless-escape': 'off', - 'no-console': 'error' + 'no-console': 'error', }, overrides: Object.keys(allowedModules).map((key) => ({ files: key + '/**/*.js', rules: { - 'prebid/validate-imports': ['error', allowedModules[key]] + 'prebid/validate-imports': ['error', allowedModules[key]], + 'no-restricted-globals': [ + 'error', + { + name: 'require', + message: 'use import instead' + } + ] } })).concat([{ // code in other packages (such as plugins/eslint) is not "seen" by babel and its parser will complain. 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/PR_REVIEW.md b/PR_REVIEW.md index 1152e2942bf..2934a30fb47 100644 --- a/PR_REVIEW.md +++ b/PR_REVIEW.md @@ -51,11 +51,16 @@ Follow steps above for general review process. In addition, please verify the fo - If the adapter being submitted is an alias type, check with the bidder contact that is being aliased to make sure it's allowed. - All bidder parameter conventions must be followed: - Video params must be read from AdUnit.mediaTypes.video when available; however bidder config can override the ad unit. - - First party data must be read from [`fpd.context` and `fpd.user`](https://docs.prebid.org/dev-docs/publisher-api-reference.html#setConfig-fpd). + - First party data must be read from [getConfig('ortb2');](https://docs.prebid.org/dev-docs/publisher-api-reference/setConfig.html#setConfig-fpd). - Adapters that accept a floor parameter must also support the [floors module](https://docs.prebid.org/dev-docs/modules/floors.html) -- look for a call to the `getFloor()` function. - Adapters cannot accept an schain parameter. Rather, they must look for the schain parameter at bidRequest.schain. - - The bidRequest page referrer must checked in addition to any bidder-specific parameter. + - The bidderRequest.refererInfo.referer must be checked in addition to any bidder-specific parameter. - If they're getting the COPPA flag, it must come from config.getConfig('coppa'); + - Page position must come from bidrequest.mediaTypes.banner.pos or bidrequest.mediaTypes.video.pos + - Global OpenRTB fields should come from [getConfig('ortb2');](https://docs.prebid.org/dev-docs/publisher-api-reference/setConfig.html#setConfig-fpd): + - bcat, battr, badv + - Impression-specific OpenRTB fields should come from bidrequest.ortb2imp + - instl - Below are some examples of bidder specific updates that should require docs update (in their dev-docs/bidders/BIDDER.md file): - If they support the GDPR consentManagement module and TCF1, add `gdpr_supported: true` - If they support the GDPR consentManagement module and TCF2, add `tcf2_supported: true` 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..ff49436384b 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'); @@ -33,8 +32,8 @@ var prebid = require('./package.json'); var dateString = 'Updated : ' + (new Date()).toISOString().substring(0, 10); var banner = '/* <%= prebid.name %> v<%= prebid.version %>\n' + dateString + '*/\n'; var port = 9999; -const FAKE_SERVER_HOST = argv.host ? argv.host : 'localhost'; -const FAKE_SERVER_PORT = 4444; +const INTEG_SERVER_HOST = argv.host ? argv.host : 'localhost'; +const INTEG_SERVER_PORT = 4444; const { spawn } = require('child_process'); // these modules must be explicitly listed in --modules to be included in the build, won't be part of "all" modules @@ -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.'; } @@ -155,9 +167,9 @@ function gulpBundle(dev) { return bundle(dev).pipe(gulp.dest('build/' + (dev ? 'dev' : 'dist'))); } -function nodeBundle(modules) { +function nodeBundle(modules, dev = false) { return new Promise((resolve, reject) => { - bundle(false, modules) + bundle(dev, modules) .on('error', (err) => { reject(err); }) @@ -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. @@ -230,41 +243,18 @@ function testTaskMaker(options = {}) { if (options.notest) { done(); } else if (options.e2e) { - let wdioCmd = path.join(__dirname, 'node_modules/.bin/wdio'); - let wdioConf = path.join(__dirname, 'wdio.conf.js'); - let wdioOpts; - - if (options.file) { - wdioOpts = [ - wdioConf, - `--spec`, - `${options.file}` - ] - } else { - wdioOpts = [ - wdioConf - ]; - } - - // run fake-server - const fakeServer = spawn('node', ['./test/fake-server/index.js', `--port=${FAKE_SERVER_PORT}`]); - fakeServer.stdout.on('data', (data) => { - console.log(`stdout: ${data}`); - }); - fakeServer.stderr.on('data', (data) => { - console.log(`stderr: ${data}`); - }); - - execa(wdioCmd, wdioOpts, { stdio: 'inherit' }) + const integ = startIntegServer(); + startLocalServer(); + runWebdriver(options) .then(stdout => { // kill fake server - fakeServer.kill('SIGINT'); + integ.kill('SIGINT'); done(); process.exit(0); }) .catch(err => { // kill fake server - fakeServer.kill('SIGINT'); + integ.kill('SIGINT'); done(new Error(`Tests failed with error: ${err}`)); process.exit(1); }); @@ -283,6 +273,26 @@ function testTaskMaker(options = {}) { const test = testTaskMaker(); +function runWebdriver({file}) { + process.env.TEST_SERVER_HOST = argv.host || 'localhost'; + let wdioCmd = path.join(__dirname, 'node_modules/.bin/wdio'); + let wdioConf = path.join(__dirname, 'wdio.conf.js'); + let wdioOpts; + + if (file) { + wdioOpts = [ + wdioConf, + `--spec`, + `${file}` + ] + } else { + wdioOpts = [ + wdioConf + ]; + } + return execa(wdioCmd, wdioOpts, { stdio: 'inherit' }); +} + function newKarmaCallback(done) { return function (exitCode) { if (exitCode) { @@ -322,41 +332,29 @@ function buildPostbid() { .pipe(gulp.dest('build/postbid/')); } -function setupE2e(done) { - if (!argv.host) { - throw new gutil.PluginError({ - plugin: 'E2E test', - message: gutil.colors.red('Host should be defined e.g. ap.localhost, anlocalhost. localhost cannot be used as safari browserstack is not able to connect to localhost') - }); +function startIntegServer(dev = false) { + const args = ['./test/fake-server/index.js', `--port=${INTEG_SERVER_PORT}`, `--host=${INTEG_SERVER_HOST}`]; + if (dev) { + args.push('--dev=true') } - process.env.TEST_SERVER_HOST = argv.host; - if (argv.https) { - process.env.TEST_SERVER_PROTOCOL = argv.https; - } - argv.e2e = true; - done(); -} - -function injectFakeServerEndpoint() { - return gulp.src(['build/dist/*.js']) - .pipe(replace('https://ib.adnxs.com/ut/v3/prebid', `http://${FAKE_SERVER_HOST}:${FAKE_SERVER_PORT}`)) - .pipe(gulp.dest('build/dist')); -} - -function injectFakeServerEndpointDev() { - return gulp.src(['build/dev/*.js']) - .pipe(replace('https://ib.adnxs.com/ut/v3/prebid', `http://${FAKE_SERVER_HOST}:${FAKE_SERVER_PORT}`)) - .pipe(gulp.dest('build/dev')); -} - -function startFakeServer() { - const fakeServer = spawn('node', ['./test/fake-server/index.js', `--port=${FAKE_SERVER_PORT}`]); - fakeServer.stdout.on('data', (data) => { + const srv = spawn('node', args); + srv.stdout.on('data', (data) => { console.log(`stdout: ${data}`); }); - fakeServer.stderr.on('data', (data) => { + srv.stderr.on('data', (data) => { console.log(`stderr: ${data}`); }); + return srv; +} + +function startLocalServer(options = {}) { + connect.server({ + https: argv.https, + port: port, + host: INTEG_SERVER_HOST, + root: './', + livereload: options.livereload + }); } // Watch Task with Live Reload @@ -372,13 +370,7 @@ function watchTaskMaker(options = {}) { 'modules/**/*.js', ].concat(options.alsoWatch)); - connect.server({ - https: argv.https, - port: port, - host: FAKE_SERVER_HOST, - root: './', - livereload: options.livereload - }); + startLocalServer(options); mainWatcher.on('all', options.task()); done(); @@ -397,7 +389,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); @@ -414,11 +406,13 @@ gulp.task('build-postbid', gulp.series(escapePostbidConfig, buildPostbid)); gulp.task('serve', gulp.series(clean, lint, gulp.parallel('build-bundle-dev', watch, test))); gulp.task('serve-fast', gulp.series(clean, gulp.parallel('build-bundle-dev', watchFast))); gulp.task('serve-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('serve-e2e', gulp.series(clean, 'build-bundle-prod', gulp.parallel(() => startIntegServer(), startLocalServer))) +gulp.task('serve-e2e-dev', gulp.series(clean, 'build-bundle-dev', gulp.parallel(() => startIntegServer(true), startLocalServer))) -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)); +gulp.task('e2e-test-only', () => runWebdriver({file: argv.file})) +gulp.task('e2e-test', gulp.series(clean, 'build-bundle-prod', testTaskMaker({e2e: true}))); // other tasks gulp.task(bundleToStdout); gulp.task('bundle', gulpBundle.bind(null, false)); // used for just concatenating pre-built files with no build step 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/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/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/permutiveRtdProvider_example.html b/integrationExamples/gpt/permutiveRtdProvider_example.html index b6a22096c90..dbb4d2af0d6 100644 --- a/integrationExamples/gpt/permutiveRtdProvider_example.html +++ b/integrationExamples/gpt/permutiveRtdProvider_example.html @@ -45,6 +45,12 @@ } }, bids: [ + { + bidder: 'ix', + params: { + siteId: '123456', + } + }, { bidder: 'appnexus', params: { @@ -135,6 +141,7 @@ pbjs.que.push(function() { pbjs.setConfig({ debug: true, + pageUrl: 'http://www.test.com/test.html', realTimeData: { auctionDelay: 80, // maximum time for RTD modules to respond dataProviders: [ @@ -142,8 +149,20 @@ name: 'permutive', waitForIt: true, params: { - acBidders: ['appnexus', 'rubicon', 'ozone', 'trustx'], + acBidders: ['appnexus', 'rubicon', 'ozone', 'trustx', 'ix'], maxSegs: 500, + transformations: [ + { + id: 'iab', + config: { + segtax: 4, + iabIds: { + 1000001: '777777', + 1000002: '888888' + } + } + } + ], overwrites: { rubicon: function (bid, data, acEnabled, utils, defaultFn) { if (defaultFn){ @@ -160,7 +179,7 @@ } }); pbjs.setBidderConfig({ - bidders: ['appnexus', 'rubicon'], + bidders: ['appnexus', 'rubicon', 'ix'], config: { ortb2: { site: { @@ -180,13 +199,9 @@ gender: 'm', keywords: 'a,b', data: [ - { - name: 'www.dataprovider1.com', - ext: { taxonomyname: 'iab_audience_taxonomy' }, - segment: [{ id: '687' }, { id: '123' }] - }, { name: 'permutive.com', + ext: { segtax: 6 }, segment: [{ id: '1' }] } ] diff --git a/integrationExamples/gpt/userId_example.html b/integrationExamples/gpt/userId_example.html index 585bb1c97ae..52578ebcada 100644 --- a/integrationExamples/gpt/userId_example.html +++ b/integrationExamples/gpt/userId_example.html @@ -77,6 +77,7 @@ "301": true, // zeotapIdPlus "91": true, // criteo "737": true, // amxId + "58": true, // 33acrossId } } } @@ -128,6 +129,17 @@ "expires": 30 } }, + { + "name": "33acrossId", + "params": { + "pid": '0' + }, + "storage": { + "type": 'html5', + "name": '33acrossId', + "expires": 90 + } + }, { "name": "intentIqId", "params": { @@ -252,6 +264,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 b81ec52b2c4..73843c49914 100644 --- a/integrationExamples/gpt/weboramaRtdProvider_example.html +++ b/integrationExamples/gpt/weboramaRtdProvider_example.html @@ -1,9 +1,10 @@ - + + weborama rtd submodule example @@ -26,9 +27,8 @@ params: { setPrebidTargeting: true, // optional sendToBidders: true, // optional - onData: function (data, site) { // optional - var kind = (site) ? 'site' : 'user'; - console.log('onData', kind, data); + onData: function (data, meta) { // optional + console.log('onData', data, meta); }, weboCtxConf: { token: "to-be-defined", // mandatory @@ -36,21 +36,30 @@ setPrebidTargeting: true, // override param.setPrebidTargeting or default true sendToBidders: true, // override param.sendToBidders or default true defaultProfile: { // optional - webo_ctx: ['moon'], + webo_ctx: ["Rugby_Renault_c11495", "Sport_c11893"], webo_ds: ['bar'] }, - //, onData: function (data, ...) { ...} + // enabled: false, + //, onData: function (data,...) { ...} }, weboUserDataConf: { - accountId: 12345, // optional + accountId: 12345, // recommended setPrebidTargeting: true, // override param.setPrebidTargeting or default true - sendToBidders: true, // override param.sendToBidders or default true + sendToBidders: ['smartadserver'], // specify the bidder to share data defaultProfile: { // optional - webo_cs: ['Red'], + webo_cs: ['red'], webo_audiences: ['bam'] }, localStorageProfileKey: 'webo_wam2gam_entry', // default + // enabled: false, //, onData: function (data,...) { ...} + }, + sfbxLiteDataConf: { + enabled: true, + defaultProfile: { // optional + lite_occupation: ['gérant', 'bénévole'], + lite_hobbies: ['sport', 'cinéma'], + }, } } }] @@ -62,6 +71,9 @@ var div_1_sizes = [ [300, 300] ]; + var div_2_sizes = [ + [600, 100] + ]; var PREBID_TIMEOUT = 3000; var FAILSAFE_TIMEOUT = 5000; @@ -106,6 +118,46 @@ networkId: 456456, }, }] + }, + { + code: '/1056029/webo-wam-prebid', + mediaTypes: { + banner: { + sizes: div_2_sizes + } + }, + bids: [{ + bidder: 'smartadserver', + params: { + siteId: 1234, + pageId: 1234, + formatId: 1234, + } + }, { + bidder: 'pubmatic', + params: { + publisherId: '32572', + } + }, { + bidder: 'appnexus', + params: { + placementId: 234234, + } + }, { + bidder: 'rubicon', + params: { + accountId: '14062', + siteId: '70608', + zoneId: '335918', + userId: '12346', + } + }, { + bidder: 'criteo', + params: { + zoneId: 234234, + networkId: 456456, + }, + }] } ]; @@ -138,7 +190,6 @@ }); } - // in case PBJS doesn't load setTimeout(function () { initAdserver(); @@ -146,6 +197,7 @@ googletag.cmd.push(function () { googletag.defineSlot('/1056029/webo-ctx-prebid', div_1_sizes, 'div-gpt-ad-1620653642627-0').addService(googletag.pubads()); + googletag.defineSlot('/1056029/webo-wam-prebid', div_2_sizes, 'div-gpt-ad-1645023761875-0').addService(googletag.pubads()); googletag.pubads().disableInitialLoad(); googletag.enableServices(); }); @@ -154,11 +206,12 @@

- test webo ctx using prebid.js + test webo rtd submodule with prebid.js

Basic Prebid.js Example

Div-1
+
+ 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 2fb46377a64..59bae2013d1 100644 --- a/modules/.submodules.json +++ b/modules/.submodules.json @@ -1,12 +1,15 @@ { "userId": [ + "33acrossIdSystem", "admixerIdSystem", "adtelligentIdSystem", "akamaiDAPIdSystem", "amxIdSystem", "britepoolIdSystem", "connectIdSystem", + "cpexIdSystem", "criteoIdSystem", + "dacIdSystem", "deepintentDpesIdSystem", "dmdIdSystem", "fabrickIdSystem", @@ -14,6 +17,7 @@ "hadronIdSystem", "haloIdSystem", "id5IdSystem", + "ftrackIdSystem", "identityLinkIdSystem", "idxIdSystem", "imuIdSystem", @@ -46,6 +50,7 @@ "dfpAdServerVideo" ], "rtdModule": [ + "airgridRtdProvider", "browsiRtdProvider", "dgkeywordRtdProvider", "geoedgeRtdProvider", 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/33acrossIdSystem.js b/modules/33acrossIdSystem.js new file mode 100644 index 00000000000..3763fee5124 --- /dev/null +++ b/modules/33acrossIdSystem.js @@ -0,0 +1,115 @@ +/** + * This module adds 33acrossId to the User ID module + * The {@link module:modules/userId} module is required + * @module modules/33acrossIdSystem + * @requires module:modules/userId + */ + +import { logMessage, logError } from '../src/utils.js'; +import { ajaxBuilder } from '../src/ajax.js'; +import { submodule } from '../src/hook.js'; +import { uspDataHandler } from '../src/adapterManager.js'; + +const MODULE_NAME = '33acrossId'; +const API_URL = 'https://lexicon.33across.com/v1/envelope'; +const AJAX_TIMEOUT = 10000; + +function getEnvelope(response) { + if (!response.succeeded) { + logError(`${MODULE_NAME}: Unsuccessful response`); + + return; + } + + if (!response.data.envelope) { + logMessage(`${MODULE_NAME}: No envelope was received`); + + return; + } + + return response.data.envelope; +} + +function calculateQueryStringParams(pid, gdprConsentData) { + const uspString = uspDataHandler.getConsentData(); + const gdprApplies = Boolean(gdprConsentData?.gdprApplies); + const params = { + pid, + gdpr: Number(gdprApplies), + }; + + if (uspString) { + params.us_privacy = uspString; + } + + if (gdprApplies) { + params.gdpr_consent = gdprConsentData.consentString || ''; + } + + return params; +} + +/** @type {Submodule} */ +export const thirthyThreeAcrossIdSubmodule = { + /** + * used to link submodule with config + * @type {string} + */ + name: MODULE_NAME, + + gvlid: 58, + + /** + * decode the stored id value for passing to bid requests + * @function + * @param {string} id + * @returns {{'33acrossId':{ envelope: string}}} + */ + decode(id) { + return { + [MODULE_NAME]: { + envelope: id + } + }; + }, + + /** + * performs action to obtain id and return a value in the callback's response argument + * @function + * @param {SubmoduleConfig} [config] + * @returns {IdResponse|undefined} + */ + getId({ params = { } }, gdprConsentData) { + if (typeof params.pid !== 'string') { + logError(`${MODULE_NAME}: Submodule requires a partner ID to be defined`); + + return; + } + + const { pid, apiUrl = API_URL } = params; + + return { + callback(cb) { + ajaxBuilder(AJAX_TIMEOUT)(apiUrl, { + success(response) { + let envelope; + + try { + envelope = getEnvelope(JSON.parse(response)) + } catch (err) { + logError(`${MODULE_NAME}: ID reading error:`, err); + } + cb(envelope); + }, + error(err) { + logError(`${MODULE_NAME}: ID error response`, err); + + cb(); + } + }, calculateQueryStringParams(pid, gdprConsentData), { method: 'GET', withCredentials: true }); + } + }; + } +}; + +submodule('userId', thirthyThreeAcrossIdSubmodule); diff --git a/modules/33acrossIdSystem.md b/modules/33acrossIdSystem.md new file mode 100644 index 00000000000..1e4af89344f --- /dev/null +++ b/modules/33acrossIdSystem.md @@ -0,0 +1,53 @@ +# 33ACROSS ID + +For help adding this submodule, please contact [PrebidUIM@33across.com](PrebidUIM@33across.com). + +### Prebid Configuration + +You can configure this submodule in your `userSync.userIds[]` configuration: + +```javascript +pbjs.setConfig({ + userSync: { + userIds: [ + { + name: "33acrossId", + storage: { + name: "33acrossId", + type: "html5", + expires: 90, + refreshInSeconds: 8*3600 + }, + params: { + pid: "0010b00002GYU4eBAH", + }, + }, + ], + }, +}); +``` + +| Parameters under `userSync.userIds[]` | Scope | Type | Description | Example | +| ---| --- | --- | --- | --- | +| name | Required | String | Name for the 33Across ID submodule | `"33acrossId"` | | +| storage | Required | Object | Configures how to cache User IDs locally in the browser | See [storage settings](#storage-settings) | +| params | Required | Object | Parameters for 33Across ID submodule | See [params](#params) | + +### Storage Settings + +The following settings are available for the `storage` property in the `userSync.userIds[]` object: + +| Param name | Scope | Type | Description | Example | +| --- | --- | --- | --- | --- | +| name | Required | String| Name of the cookie or HTML5 local storage where the user ID will be stored | `"33acrossId"` | +| type | Required | String | `"html5"` (preferred) or `"cookie"` | `"html5"` | +| expires | Strongly Recommended | Number | How long (in days) the user ID information will be stored. 33Across recommends `90`. | `90` | +| refreshInSeconds | Strongly Recommended | Number | The interval (in seconds) for refreshing the user ID. 33Across recommends no more than 8 hours between refreshes. | `8*3600` | + +### Params + +The following settings are available in the `params` property in `userSync.userIds[]` object: + +| Param name | Scope | Type | Description | Example | +| --- | --- | --- | --- | --- | +| pid | Required | String | Partner ID provided by 33Across | `"0010b00002GYU4eBAH"` | diff --git a/modules/adagioBidAdapter.js b/modules/adagioBidAdapter.js index 3381f00ff8f..b98567878a8 100644 --- a/modules/adagioBidAdapter.js +++ b/modules/adagioBidAdapter.js @@ -1,18 +1,38 @@ -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'; +import { getGlobal } from '../src/prebidGlobal.js'; + const BIDDER_CODE = 'adagio'; const LOG_PREFIX = 'Adagio:'; const FEATURES_VERSION = '1'; @@ -21,13 +41,12 @@ 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'; const ADAGIO_PUBKEY_E = 65537; const CURRENCY = 'USD'; -const DEFAULT_FLOOR = 0.1; // This provide a whitelist and a basic validation // of OpenRTB 2.5 options used by the Adagio SSP. @@ -250,32 +269,14 @@ function getDevice() { }; function getSite(bidderRequest) { - let domain = ''; - let page = ''; - let referrer = ''; - const { refererInfo } = bidderRequest; - - if (canAccessTopWindow()) { - const wt = getWindowTop(); - domain = wt.location.hostname; - page = wt.location.href; - referrer = wt.document.referrer || ''; - } else if (refererInfo.reachedTop) { - const url = parseUrl(refererInfo.referer); - domain = url.hostname; - page = refererInfo.referer; - } 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. - const url = parseUrl(refererInfo.stack[0]); - domain = url.hostname; - } + const url = parseUrl(refererInfo.referer); return { - domain, - page, - referrer + domain: url.hostname || '', + page: refererInfo.referer || '', + referrer: canAccessTopWindow() ? getWindowTop().document.referrer || '' : getWindowSelf().document.referrer || '', + top: refererInfo.reachedTop }; }; @@ -570,13 +571,13 @@ function _getFloors(bidRequest) { const info = bidRequest.getFloor({ currency: CURRENCY, mediaType, - size: [] + size }); floors.push(cleanObj({ mt: mediaType, s: isArray(size) ? `${size[0]}x${size[1]}` : undefined, - f: (!isNaN(info.floor) && info.currency === CURRENCY) ? info.floor : DEFAULT_FLOOR + f: (!isNaN(info.floor) && info.currency === CURRENCY) ? info.floor : undefined })); } @@ -804,7 +805,7 @@ function getPrintNumber(adUnitCode, bidderRequest) { return 1; } const adagioBid = find(bidderRequest.bids, bid => bid.adUnitCode === adUnitCode); - return adagioBid.bidRequestsCount || 1; + return adagioBid.bidderRequestsCount || 1; } /** @@ -850,7 +851,9 @@ function storeRequestInAdagioNS(bidRequest) { }], auctionId: bidRequest.auctionId, pageviewId: internal.getPageviewId(), - printNumber + printNumber, + localPbjs: '$$PREBID_GLOBAL$$', + localPbjsRef: getGlobal() }); // (legacy) Store internal adUnit information @@ -918,7 +921,45 @@ export const spec = { }); // Handle priceFloors module - bidRequest.floors = _getFloors(bidRequest); + const computedFloors = _getFloors(bidRequest); + if (isArray(computedFloors) && computedFloors.length) { + bidRequest.floors = computedFloors + + if (deepAccess(bidRequest, 'mediaTypes.banner')) { + const bannerObj = bidRequest.mediaTypes.banner + + const computeNewSizeArray = (sizeArr = []) => { + const size = { size: sizeArr, floor: null } + const bannerFloors = bidRequest.floors.filter(floor => floor.mt === BANNER) + const BannerSizeFloor = bannerFloors.find(floor => floor.s === sizeArr.join('x')) + size.floor = (bannerFloors) ? (BannerSizeFloor) ? BannerSizeFloor.f : bannerFloors[0].f : null + return size + } + + // `bannerSizes`, internal property name + bidRequest.mediaTypes.banner.bannerSizes = (isArray(bannerObj.sizes[0])) + ? bannerObj.sizes.map(sizeArr => { + return computeNewSizeArray(sizeArr) + }) + : computeNewSizeArray(bannerObj.sizes) + } + + if (deepAccess(bidRequest, 'mediaTypes.video')) { + const videoObj = bidRequest.mediaTypes.video + const videoFloors = bidRequest.floors.filter(floor => floor.mt === VIDEO); + const playerSize = (videoObj.playerSize && isArray(videoObj.playerSize[0])) ? videoObj.playerSize[0] : videoObj.playerSize + const videoSizeFloor = (playerSize) ? videoFloors.find(floor => floor.s === playerSize.join('x')) : undefined + + bidRequest.mediaTypes.video.floor = (videoFloors) ? videoSizeFloor ? videoSizeFloor.f : videoFloors[0].f : null + } + + if (deepAccess(bidRequest, 'mediaTypes.native')) { + const nativeFloors = bidRequest.floors.filter(floor => floor.mt === NATIVE); + if (nativeFloors.length) { + bidRequest.mediaTypes.native.floor = nativeFloors[0].f + } + } + } if (deepAccess(bidRequest, 'mediaTypes.video')) { _buildVideoBidRequest(bidRequest); @@ -937,6 +978,8 @@ export const spec = { // remove useless props delete adUnitCopy.floorData; delete adUnitCopy.params.siteId; + delete adUnitCopy.userId + delete adUnitCopy.userIdAsEids groupedAdUnits[adUnitCopy.params.organizationId] = groupedAdUnits[adUnitCopy.params.organizationId] || []; groupedAdUnits[adUnitCopy.params.organizationId].push(adUnitCopy); diff --git a/modules/adbookpspBidAdapter.js b/modules/adbookpspBidAdapter.js index 1b93d4fe1c6..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 @@ -577,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/adfBidAdapter.js b/modules/adfBidAdapter.js index f0425a174ff..0b9c72a2cee 100644 --- a/modules/adfBidAdapter.js +++ b/modules/adfBidAdapter.js @@ -206,6 +206,11 @@ export const spec = { request.is_debug = !!test; request.test = 1; } + + if (config.getConfig('coppa')) { + deepSetValue(request, 'regs.coppa', 1); + } + if (deepAccess(bidderRequest, 'gdprConsent.gdprApplies') !== undefined) { deepSetValue(request, 'user.ext.consent', bidderRequest.gdprConsent.consentString); deepSetValue(request, 'regs.ext.gdpr', bidderRequest.gdprConsent.gdprApplies & 1); 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 6a8c98650c0..7f5af047993 100644 --- a/modules/adhashBidAdapter.js +++ b/modules/adhashBidAdapter.js @@ -1,8 +1,81 @@ -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', @@ -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 []; } 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 0dcde7dff91..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'; /* 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 916bc9a45ce..e09a6495730 100644 --- a/modules/adlooxAnalyticsAdapter.md +++ b/modules/adlooxAnalyticsAdapter.md @@ -129,6 +129,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..21bcb6cee26 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 || @@ -110,6 +110,7 @@ export const spec = { if (bid.userId) { getUserId(placement.eids, bid.userId.uid2 && bid.userId.uid2.id, 'uidapi.com'); getUserId(placement.eids, bid.userId.lotamePanoramaId, 'lotame.com'); + getUserId(placement.eids, bid.userId.idx, 'idx.lat'); } if (traff === VIDEO) { placement.playerSize = bid.mediaTypes[VIDEO].playerSize; 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/adomikAnalyticsAdapter.js b/modules/adomikAnalyticsAdapter.js index e81f6e50054..40809c59093 100644 --- a/modules/adomikAnalyticsAdapter.js +++ b/modules/adomikAnalyticsAdapter.js @@ -1,11 +1,10 @@ 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 +// Events used in adomik analytics adapter. const auctionInit = CONSTANTS.EVENTS.AUCTION_INIT; const auctionEnd = CONSTANTS.EVENTS.AUCTION_END; const bidRequested = CONSTANTS.EVENTS.BID_REQUESTED; @@ -31,17 +30,13 @@ let adomikAdapter = Object.assign(adapter({}), break; case bidResponse: - adomikAdapter.bucketEvents.push({ - type: 'response', - event: adomikAdapter.buildBidResponse(args) - }); + adomikAdapter.saveBidResponse(args); break; case bidWon: - adomikAdapter.sendWonEvent({ - id: args.adId, - placementCode: args.adUnitCode - }); + args.id = args.adId; + args.placementCode = args.adUnitCode; + adomikAdapter.sendWonEvent(args); break; case bidRequested: @@ -70,16 +65,28 @@ adomikAdapter.initializeBucketEvents = function() { adomikAdapter.bucketEvents = []; } +adomikAdapter.saveBidResponse = function(args) { + let responseSaved = adomikAdapter.bucketEvents.find((bucketEvent) => + bucketEvent.type == 'response' && bucketEvent.event.id == args.id + ); + if (responseSaved) { return true; } + adomikAdapter.bucketEvents.push({ + type: 'response', + event: adomikAdapter.buildBidResponse(args) + }); +} + adomikAdapter.maxPartLength = function () { return (ua.includes(' MSIE ')) ? 1600 : 60000; }; adomikAdapter.sendTypedEvent = function() { + let [testId, testValue] = adomikAdapter.getKeyValues(); const groupedTypedEvents = adomikAdapter.buildTypedEvents(); const bulkEvents = { - testId: adomikAdapter.currentContext.testId, - testValue: adomikAdapter.currentContext.testValue, + testId: testId, + testValue: testValue, uid: adomikAdapter.currentContext.uid, ahbaid: adomikAdapter.currentContext.id, hostname: window.location.hostname, @@ -115,10 +122,8 @@ adomikAdapter.sendTypedEvent = function() { const stringBulkEvents = JSON.stringify(bulkEvents) logInfo('Events sent to adomik prebid analytic ' + stringBulkEvents); - // Encode object in base64 const encodedBuf = window.btoa(stringBulkEvents); - // Create final url and split it (+endpoint length) const encodedUri = encodeURIComponent(encodedBuf); const maxLength = adomikAdapter.maxPartLength(); const splittedUrl = encodedUri.match(new RegExp(`.{1,${maxLength}}`, 'g')); @@ -131,16 +136,18 @@ adomikAdapter.sendTypedEvent = function() { }; adomikAdapter.sendWonEvent = function (wonEvent) { - let keyValues = { testId: adomikAdapter.currentContext.testId, testValue: adomikAdapter.currentContext.testValue } - wonEvent = {...wonEvent, ...keyValues} - const stringWonEvent = JSON.stringify(wonEvent) + let [testId, testValue] = adomikAdapter.getKeyValues(); + let keyValues = { testId: testId, testValue: testValue }; + let samplingInfo = { sampling: adomikAdapter.currentContext.sampling }; + wonEvent = { ...adomikAdapter.buildBidResponse(wonEvent), ...keyValues, ...samplingInfo }; + + const stringWonEvent = JSON.stringify(wonEvent); logInfo('Won event sent to adomik prebid analytic ' + stringWonEvent); - // Encode object in base64 const encodedBuf = window.btoa(stringWonEvent); const encodedUri = encodeURIComponent(encodedBuf); const img = new Image(1, 1); - img.src = `https://${adomikAdapter.currentContext.url}/?q=${encodedUri}&id=${adomikAdapter.currentContext.id}&won=true` + img.src = `https://${adomikAdapter.currentContext.url}/?q=${encodedUri}&id=${adomikAdapter.currentContext.id}&won=true`; } adomikAdapter.buildBidResponse = function (bid) { @@ -202,33 +209,49 @@ adomikAdapter.buildTypedEvents = function () { return groupedTypedEvents; } -adomikAdapter.adapterEnableAnalytics = adomikAdapter.enableAnalytics; +adomikAdapter.getKeyValues = function () { + let preventTest = sessionStorage.getItem(window.location.hostname + '_NoAdomikTest') + let inScope = sessionStorage.getItem(window.location.hostname + '_AdomikTestInScope') + let keyValues = JSON.parse(sessionStorage.getItem(window.location.hostname + '_AdomikTest')) + let testId; + let testValue; + if (typeof (keyValues) === 'object' && keyValues != undefined && !preventTest && inScope) { + testId = keyValues.testId + testValue = keyValues.testOptionLabel + } + return [testId, testValue] +} -adomikAdapter.enableAnalytics = function (config) { - adomikAdapter.currentContext = {}; - const initOptions = config.options; - - _sampled = typeof config === 'undefined' || - typeof config.sampling === 'undefined' || - Math.random() < parseFloat(config.sampling); - - if (_sampled) { - if (initOptions) { - adomikAdapter.currentContext = { - uid: initOptions.id, - url: initOptions.url, - testId: initOptions.testId, - testValue: initOptions.testValue, - id: '', - timeouted: false, - sampling: config.sampling - } - logInfo('Adomik Analytics enabled with config', initOptions); - adomikAdapter.adapterEnableAnalytics(config); - } - } else { - logInfo('Adomik Analytics ignored for sampling', config.sampling); +adomikAdapter.enable = function(options) { + adomikAdapter.currentContext = { + uid: options.id, + url: options.url, + id: '', + timeouted: false, + sampling: options.sampling } + logInfo('Adomik Analytics enabled with config', options); + adomikAdapter.adapterEnableAnalytics(options); +}; + +adomikAdapter.checkOptions = function(options) { + if (typeof options !== 'undefined') { + if (options.id && options.url) { adomikAdapter.enable(options); } else { logInfo('Adomik Analytics disabled because id and/or url is missing from config', options); } + } else { logInfo('Adomik Analytics disabled because config is missing'); } +}; + +adomikAdapter.checkSampling = function(options) { + _sampled = typeof options === 'undefined' || + typeof options.sampling === 'undefined' || + (options.sampling > 0 && Math.random() < parseFloat(options.sampling)); + if (_sampled) { adomikAdapter.checkOptions(options) } else { logInfo('Adomik Analytics ignored for sampling', options.sampling); } +}; + +adomikAdapter.adapterEnableAnalytics = adomikAdapter.enableAnalytics; + +adomikAdapter.enableAnalytics = function ({ provider, options }) { + logInfo('Adomik Analytics enableAnalytics', provider); + adomikAdapter.checkSampling(options); }; adapterManager.registerAnalyticsAdapter({ diff --git a/modules/adotBidAdapter.js b/modules/adotBidAdapter.js index b1adcd1311d..ac49f7ae32d 100644 --- a/modules/adotBidAdapter.js +++ b/modules/adotBidAdapter.js @@ -1,16 +1,16 @@ import {Renderer} from '../src/Renderer.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; import {BANNER, NATIVE, VIDEO} from '../src/mediaTypes.js'; -import {isStr, isFn, isArray, 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 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 REQUIRED_VIDEO_PARAMS = ['mimes', 'minduration', 'maxduration', 'protocols']; +const REQUIRED_VIDEO_PARAMS = ['mimes', 'protocols']; const DOMAIN_REGEX = new RegExp('//([^/]*)'); const FIRST_PRICE = 1; const IMP_BUILDER = { banner: buildBanner, video: buildVideo, native: buildNative }; 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 eac8d8afdf0..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'; 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..13174ff337c 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,9 +18,10 @@ 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', + janet: () => 'ghb.bidder.jmgads.com', + pgam: () => 'ghb.pgamssp.com', } const getUri = function (bidderCode) { let bidderWithoutSuffix = bidderCode.split('_')[0]; @@ -36,12 +37,9 @@ const syncsCache = {}; export const spec = { code: BIDDER_CODE, gvlid: 410, - aliases: ['onefiftytwomedia', 'selectmedia', 'appaloosa', 'bidsxchange', 'streamkey', + aliases: ['onefiftytwomedia', 'selectmedia', 'appaloosa', 'bidsxchange', 'streamkey', 'janet', { code: 'navelix', gvlid: 380 }, - { - code: 'mediafuse', - skipPbsAliasing: true - } + 'pgam' ], 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 7e4c3a729e5..6fcce753596 100644 --- a/modules/adyoulikeBidAdapter.js +++ b/modules/adyoulikeBidAdapter.js @@ -1,8 +1,8 @@ -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'; @@ -61,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) => { @@ -88,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; @@ -122,7 +124,7 @@ export const spec = { return { method: 'POST', - url: createEndpoint(bidRequests, bidderRequest), + url: createEndpoint(bidRequests, bidderRequest, hasVideo), data, options }; @@ -217,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) }); } @@ -347,14 +350,6 @@ function getTrackers(eventsArray, jsTrackers) { return result; } -function getVideoAd(response) { - var adJson = {}; - if (typeof response.Ad === 'string' && response.Ad.indexOf('\/\*PREBID\*\/') > 0) { - adJson = JSON.parse(response.Ad.match(/\/\*PREBID\*\/(.*)\/\*PREBID\*\//)[1]); - return deepAccess(adJson, 'Content.MainVideo.Vast'); - } -} - function getNativeAssets(response, nativeConfig) { if (typeof response.Native === 'object') { return response.Native; @@ -483,8 +478,10 @@ function createBid(response, bidRequests) { }; // retreive video response if present - const vast64 = response.Vast || getVideoAd(response); + const vast64 = response.Vast; if (vast64) { + bid.width = response.Width; + bid.height = response.Height; bid.vastXml = window.atob(vast64); bid.mediaType = 'video'; } else if (request.Native) { 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..e9011343a74 100644 --- a/modules/airgridRtdProvider.js +++ b/modules/airgridRtdProvider.js @@ -5,18 +5,26 @@ * @module modules/airgridRtdProvider * @requires module:modules/realTimeData */ -import {config} from '../src/config.js'; -import {submodule} from '../src/hook.js'; -import {mergeDeep, isPlainObject, deepSetValue, deepAccess} from '../src/utils.js'; -import {getGlobal} from '../src/prebidGlobal.js'; -import {getStorageManager} from '../src/storageManager.js'; +import { config } from '../src/config.js'; +import { submodule } from '../src/hook.js'; +import { + mergeDeep, + isPlainObject, + deepSetValue, + deepAccess, +} from '../src/utils.js'; +import { getGlobal } from '../src/prebidGlobal.js'; +import { getStorageManager } from '../src/storageManager.js'; const MODULE_NAME = 'realTimeData'; const SUBMODULE_NAME = 'airgrid'; const AG_TCF_ID = 782; -export const AG_AUDIENCE_IDS_KEY = 'edkt_matched_audience_ids' +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 @@ -24,13 +32,13 @@ export const storage = getStorageManager(AG_TCF_ID, SUBMODULE_NAME); * @return {void} */ export function attachScriptTagToDOM(rtdConfig) { - var edktInitializor = window.edktInitializor = window.edktInitializor || {}; + var edktInitializor = (window.edktInitializor = window.edktInitializor || {}); if (!edktInitializor.invoked) { edktInitializor.invoked = true; edktInitializor.accountId = rtdConfig.params.accountId; edktInitializor.publisherId = rtdConfig.params.publisherId; edktInitializor.apiKey = rtdConfig.params.apiKey; - edktInitializor.load = function(e) { + edktInitializor.load = function (e) { var p = e || 'sdk'; var n = document.createElement('script'); n.type = 'module'; @@ -48,7 +56,7 @@ export function attachScriptTagToDOM(rtdConfig) { */ export function getMatchedAudiencesFromStorage() { const audiences = storage.getDataFromLocalStorage(AG_AUDIENCE_IDS_KEY); - if (!audiences) return [] + if (!audiences) return []; try { return JSON.parse(audiences); } catch (e) { @@ -68,8 +76,8 @@ function setAudiencesToAppNexusAdUnits(adUnits, audiences) { if (bid.bidder && bid.bidder === 'appnexus') { deepSetValue(bid, 'params.keywords.perid', audiences || []); } - }) - }) + }); + }); } /** @@ -82,7 +90,7 @@ export function setAudiencesUsingBidderOrtb2(rtdConfig, audiences) { const bidders = deepAccess(rtdConfig, 'params.bidders'); if (!bidders || bidders.length === 0) return; const allBiddersConfig = config.getBidderConfig(); - const agOrtb2 = {} + const agOrtb2 = {}; deepSetValue(agOrtb2, 'ortb2.user.ext.data.airgrid', audiences || []); bidders.forEach((bidder) => { @@ -92,11 +100,19 @@ export function setAudiencesUsingBidderOrtb2(rtdConfig, audiences) { } config.setBidderConfig({ bidders: [bidder], - config: mergeDeep(bidderConfig, agOrtb2) + config: mergeDeep(bidderConfig, agOrtb2), }); }); } +export function setAudiencesUsingAppNexusAuctionKeywords(audiences) { + config.setConfig({ + appnexusAuctionKeywords: { + perid: audiences, + }, + }); +} + /** * Module init * @param {Object} rtdConfig @@ -116,23 +132,29 @@ function init(rtdConfig, userConsent) { * @param {Object} userConsent * @return {void} */ -export function passAudiencesToBidders(bidConfig, onDone, rtdConfig, userConsent) { +export function passAudiencesToBidders( + bidConfig, + onDone, + rtdConfig, + userConsent +) { const adUnits = bidConfig.adUnits || getGlobal().adUnits; const audiences = getMatchedAudiencesFromStorage(); if (audiences.length > 0) { + setAudiencesUsingAppNexusAuctionKeywords(audiences); setAudiencesUsingBidderOrtb2(rtdConfig, audiences); if (adUnits) { setAudiencesToAppNexusAdUnits(adUnits, audiences); } } onDone(); -}; +} /** @type {RtdSubmodule} */ export const airgridSubmodule = { name: SUBMODULE_NAME, init: init, - getBidRequestData: passAudiencesToBidders + getBidRequestData: passAudiencesToBidders, }; submodule(MODULE_NAME, airgridSubmodule); diff --git a/modules/airgridRtdProvider.md b/modules/airgridRtdProvider.md index 7ee502b4c10..6251c63fce9 100644 --- a/modules/airgridRtdProvider.md +++ b/modules/airgridRtdProvider.md @@ -1,15 +1,17 @@ - --- - layout: page_v2 - title: AirGrid RTD SubModule - description: Client-side, cookieless and privacy-first audiences. - page_type: module - module_type: rtd - module_code : example - enable_download : true - sidebarType : 1 - --- - -# AirGrid +--- +layout: page_v2 +title: AirGrid RTD Provider +display_name: AirGrid RTD Provider +description: Client-side, cookieless and privacy-first audiences. +page_type: module +module_type: rtd +module_code : airgridRtdProvider +enable_download : true +vendor_specific: true +sidebarType : 1 +--- + +# AirGrid RTD Provider AirGrid is a privacy-first, cookie-less audience platform. Designed to help publishers increase inventory yield, whilst providing audience signal to buyers in the bid request, without exposing raw user level data to any party. @@ -17,13 +19,17 @@ whilst providing audience signal to buyers in the bid request, without exposing This real-time data module provides quality first-party data, contextual data, site-level data and more that is injected into bid request objects destined for different bidders in order to optimize targeting. +{:.no_toc} +* TOC +{:toc} + ## Usage -Compile the Halo RTD module into your Prebid build: +Compile the AirGrid RTD module (`airgridRtdProvider`) into your Prebid build, along with the parent RTD Module (`rtdModule`): `gulp build --modules=rtdModule,airgridRtdProvider,appnexusBidAdapter` -Add the AirGrid RTD provider to your Prebid config. In this example we will configure publisher 1234 to retrieve segments from Audigent. See the "Parameter Descriptions" below for more detailed information of the configuration parameters. +Next we configure the module, via `pbjs.setConfig`. See the **Parameter Descriptions** below for more detailed information of the configuration parameters. ```js pbjs.setConfig( @@ -50,6 +56,7 @@ pbjs.setConfig( ### Parameter Descriptions +{: .table .table-bordered .table-striped } | Name |Type | Description | Notes | | :------------ | :------------ | :------------ |:------------ | | name | `String` | RTD sub module name | Always 'airgrid' | @@ -61,7 +68,7 @@ pbjs.setConfig( _Note: Although the module supports passing segment data to any bidder using the ORTB2 spec, there is no way for this to be currently monetised. Please reach out to support, to discuss using bidders other than Xandr/AppNexus._ -If you do not have your own `apiKey`, `accountId` & `publisherId` please reach out to [support@airgrid.io](mailto:support@airgrid.io) +If you do not have your own `apiKey`, `accountId` & `publisherId` please reach out to [support@airgrid.io](mailto:support@airgrid.io) or you can sign up via the [AirGrid platform](https://app.airgrid.io). ## Testing @@ -89,7 +96,7 @@ If you require further assistance or are interested in discussing the module fun - [hello@airgrid.io](mailto:hello@airgrid.io) for general questions. - [support@airgrid.io](mailto:support@airgrid.io) for technical questions. -You are also able to find more examples and other integration routes on the [AirGrid docs site](docs.airgrid.io). +You are also able to find more examples and other integration routes on the [AirGrid docs site](https://docs.airgrid.io), or learn more on our [site](https://airgrid.io)! Happy Coding! 😊 The AirGrid Team. 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/alkimiBidAdapter.js b/modules/alkimiBidAdapter.js new file mode 100644 index 00000000000..261fd9dee68 --- /dev/null +++ b/modules/alkimiBidAdapter.js @@ -0,0 +1,119 @@ +import { registerBidder } from '../src/adapters/bidderFactory.js'; +import { deepClone, deepAccess } from '../src/utils.js'; +import { ajax } from '../src/ajax.js'; +import { VIDEO } from '../src/mediaTypes.js'; +import { config } from '../src/config.js'; + +const BIDDER_CODE = 'alkimi'; +export const ENDPOINT = 'https://exchange.alkimi-onboarding.com/bid?prebid=true'; + +export const spec = { + code: BIDDER_CODE, + supportedMediaTypes: ['banner', 'video'], + + isBidRequestValid: function (bid) { + return !!(bid.params && bid.params.bidFloor && bid.params.token); + }, + + buildRequests: function (validBidRequests, bidderRequest) { + let bids = []; + let bidIds = []; + validBidRequests.forEach(bidRequest => { + let sizes = prepareSizes(bidRequest.sizes) + + bids.push({ + token: bidRequest.params.token, + pos: bidRequest.params.pos, + bidFloor: bidRequest.params.bidFloor, + width: sizes[0].width, + height: sizes[0].height, + impMediaType: getFormatType(bidRequest) + }) + bidIds.push(bidRequest.bidId) + }) + + const alkimiConfig = config.getConfig('alkimi'); + + let payload = { + requestId: bidderRequest.auctionId, + signRequest: { bids, randomUUID: alkimiConfig && alkimiConfig.randomUUID }, + bidIds, + referer: bidderRequest.refererInfo.referer, + signature: alkimiConfig && alkimiConfig.signature + } + + const options = { + contentType: 'application/json', + customHeaders: { + 'Rtb-Direct': true + } + } + + return { + method: 'POST', + url: ENDPOINT, + data: payload, + options + }; + }, + + interpretResponse: function (serverResponse, request) { + const serverBody = serverResponse.body; + if (!serverBody || typeof serverBody !== 'object') { + return []; + } + + const { prebidResponse } = serverBody; + if (!prebidResponse || typeof prebidResponse !== 'object') { + return []; + } + + let bids = []; + prebidResponse.forEach(bidResponse => { + let bid = deepClone(bidResponse); + bid.cpm = parseFloat(bidResponse.cpm); + + // banner or video + if (VIDEO === bid.mediaType) { + bid.vastXml = bid.ad; + } + + bid.meta = {}; + bid.meta.advertiserDomains = bid.adomain || []; + + bids.push(bid); + }) + + return bids; + }, + + onBidWon: function (bid) { + let winUrl; + if (bid.winUrl || bid.vastUrl) { + winUrl = bid.winUrl ? bid.winUrl : bid.vastUrl; + winUrl = winUrl.replace(/\$\{AUCTION_PRICE\}/, bid.cpm); + } else if (bid.ad) { + let trackImg = bid.ad.match(/(?!^)/); + bid.ad = bid.ad.replace(trackImg[0], ''); + winUrl = trackImg[0].split('"')[1]; + winUrl = winUrl.replace(/\$%7BAUCTION_PRICE%7D/, bid.cpm); + } else { + return false; + } + + ajax(winUrl, null); + return true; + } +} + +function prepareSizes(sizes) { + return sizes && sizes.map(size => ({ width: size[0], height: size[1] })); +} + +const getFormatType = bidRequest => { + if (deepAccess(bidRequest, 'mediaTypes.banner')) return 'Banner' + if (deepAccess(bidRequest, 'mediaTypes.video')) return 'Video' + if (deepAccess(bidRequest, 'mediaTypes.audio')) return 'Audio' +} + +registerBidder(spec); diff --git a/modules/alkimiBidAdapter.md b/modules/alkimiBidAdapter.md new file mode 100644 index 00000000000..2d1fd42c70f --- /dev/null +++ b/modules/alkimiBidAdapter.md @@ -0,0 +1,35 @@ +# Overview + +``` +Module Name: Alkimi Bidder Adapter +Module Type: Bidder Adapter +Maintainer: kalidas@alkimiexchange.com +``` + +# Description + +Connects to Alkimi Bidder for bids. +Alkimi bid adapter supports Banner and Video ads. + +# Test Parameters +``` +const adUnits = [ + { + code: 'banner1', + mediaTypes: { + banner: { // Media Type can be banner or video or ... + sizes: [[300, 250]], + } + }, + bids: [ + { + bidder: 'alkimi', + params: { + bidFloor: 0.5, + token: 'a6b042a5-2d68-4170-a051-77fbaf00203a', // Publisher Token(Id) provided by Alkimi + } + } + ] + } +] +``` 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/apacdexBidAdapter.js b/modules/apacdexBidAdapter.js index 421eb99b4c1..d7b6b7c4020 100644 --- a/modules/apacdexBidAdapter.js +++ b/modules/apacdexBidAdapter.js @@ -2,22 +2,9 @@ import { deepAccess, isPlainObject, isArray, replaceAuctionPrice, isFn } from '. import { config } from '../src/config.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; const BIDDER_CODE = 'apacdex'; -const CONFIG = { - 'apacdex': { - 'ENDPOINT': 'https://useast.quantumdex.io/auction/apacdex', - 'USERSYNC': 'https://sync.quantumdex.io/usersync/apacdex' - }, - 'quantumdex': { - 'ENDPOINT': 'https://useast.quantumdex.io/auction/quantumdex', - 'USERSYNC': 'https://sync.quantumdex.io/usersync/quantumdex' - }, - 'valueimpression': { - 'ENDPOINT': 'https://useast.quantumdex.io/auction/adapter', - 'USERSYNC': 'https://sync.quantumdex.io/usersync/adapter' - } -}; +const ENDPOINT = 'https://useast.quantumdex.io/auction/pbjs' +const USERSYNC = 'https://sync.quantumdex.io/usersync/pbjs' -var bidderConfig = CONFIG[BIDDER_CODE]; var bySlotTargetKey = {}; var bySlotSizesCount = {} @@ -56,8 +43,6 @@ export const spec = { let test; let bids = []; - bidderConfig = CONFIG[validBidRequests[0].bidder]; - test = config.getConfig('debug'); validBidRequests.forEach(bidReq => { @@ -156,13 +141,14 @@ export const spec = { transactionId: bid.transactionId, sizes: bid.sizes, bidId: bid.bidId, + adUnitCode: bid.adUnitCode, bidFloor: bid.bidFloor } }); return { method: 'POST', - url: bidderConfig.ENDPOINT, + url: ENDPOINT, data: payload, withCredentials: true, bidderRequests: bids @@ -209,32 +195,47 @@ export const spec = { }); return bidResponses; }, - getUserSyncs: function (syncOptions, serverResponses) { + getUserSyncs: function (syncOptions, serverResponses, gdprConsent, uspConsent) { const syncs = []; - try { - if (syncOptions.iframeEnabled) { - syncs.push({ - type: 'iframe', - url: bidderConfig.USERSYNC - }); + if (hasPurpose1Consent(gdprConsent)) { + let params = ''; + if (gdprConsent && typeof gdprConsent.consentString === 'string') { + // add 'gdpr' only if 'gdprApplies' is defined + if (typeof gdprConsent.gdprApplies === 'boolean') { + params = `?gdpr=${Number(gdprConsent.gdprApplies)}&gdpr_consent=${gdprConsent.consentString}`; + } else { + params = `?gdpr_consent=${gdprConsent.consentString}`; + } } - if (serverResponses.length > 0 && serverResponses[0].body && serverResponses[0].body.pixel) { - serverResponses[0].body.pixel.forEach(px => { - if (px.type === 'image' && syncOptions.pixelEnabled) { - syncs.push({ - type: 'image', - url: px.url - }); - } - if (px.type === 'iframe' && syncOptions.iframeEnabled) { - syncs.push({ - type: 'iframe', - url: px.url - }); - } - }); + if (uspConsent) { + params += `${params ? '&' : '?'}us_privacy=${encodeURIComponent(uspConsent)}`; } - } catch (e) { } + + try { + if (syncOptions.iframeEnabled) { + syncs.push({ + type: 'iframe', + url: USERSYNC + params + }); + } + if (serverResponses.length > 0 && serverResponses[0].body && serverResponses[0].body.pixel) { + serverResponses[0].body.pixel.forEach(px => { + if (px.type === 'image' && syncOptions.pixelEnabled) { + syncs.push({ + type: 'image', + url: px.url + params + }); + } + if (px.type === 'iframe' && syncOptions.iframeEnabled) { + syncs.push({ + type: 'iframe', + url: px.url + params + }); + } + }); + } + } catch (e) { } + } return syncs; } }; @@ -377,4 +378,14 @@ function getBidFloor(bid) { return null; } +function hasPurpose1Consent(gdprConsent) { + let result = true; + if (gdprConsent) { + if (gdprConsent.gdprApplies && gdprConsent.apiVersion === 2) { + result = !!(deepAccess(gdprConsent, 'vendorData.purpose.consents.1') === true); + } + } + return result; +} + registerBidder(spec); diff --git a/modules/appnexusBidAdapter.js b/modules/appnexusBidAdapter.js index 1829323df2a..aa5b604781d 100644 --- a/modules/appnexusBidAdapter.js +++ b/modules/appnexusBidAdapter.js @@ -1,14 +1,38 @@ -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 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 { bidderSettings } from '../src/bidderSettings.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, + getWindowFromDocument +} 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'; @@ -61,7 +85,7 @@ const SCRIPT_TAG_START = ' encodeURIComponent(url)).join(',') } + let pubPageUrl = config.getConfig('pageUrl'); + if (isStr(pubPageUrl) && pubPageUrl !== '') { + refererinfo.rd_can = pubPageUrl; + } payload.referrer_detection = refererinfo; } @@ -270,6 +298,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; @@ -359,12 +394,32 @@ export const spec = { } }, - transformBidParams: function (params, isOpenRtb) { + transformBidParams: function (params, isOpenRtb, adUnit, bidRequests) { + let conversionFn = transformBidderParamKeywords; + if (isOpenRtb === true) { + let s2sEndpointUrl = null; + let s2sConfig = config.getConfig('s2sConfig'); + + if (isPlainObject(s2sConfig)) { + s2sEndpointUrl = deepAccess(s2sConfig, 'endpoint.p1Consent'); + } else if (isArray(s2sConfig)) { + s2sConfig.forEach(s2sCfg => { + if (includes(s2sCfg.bidders, adUnit.bids[0].bidder)) { + s2sEndpointUrl = deepAccess(s2sCfg, '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); @@ -660,7 +715,10 @@ function newBid(serverBid, rtbBid, bidderRequest) { if (rtbBid.renderer_url) { const videoBid = find(bidderRequest.bids, bid => bid.bidId === serverBid.uuid); - const rendererOptions = deepAccess(videoBid, 'renderer.options'); + let rendererOptions = deepAccess(videoBid, 'mediaTypes.video.renderer.options'); // mediaType definition has preference (shouldn't options be .config?) + if (!rendererOptions) { + rendererOptions = deepAccess(videoBid, 'renderer.options'); // second the adUnit definition has preference (shouldn't options be .config?) + } bid.renderer = newRenderer(bid.adUnitCode, rtbBid, rendererOptions); } break; @@ -763,6 +821,13 @@ function bidToTag(bid) { } if (bid.params.position) { tag.position = { 'above': 1, 'below': 2 }[bid.params.position] || 0; + } else { + let mediaTypePos = deepAccess(bid, `mediaTypes.banner.pos`) || deepAccess(bid, `mediaTypes.video.pos`); + // only support unknown, atf, and btf values for position at this time + if (mediaTypePos === 0 || mediaTypePos === 1 || mediaTypePos === 3) { + // ortb spec treats btf === 3, but our system interprets btf === 2; so converting the ortb value here for consistency + tag.position = (mediaTypePos === 3) ? 2 : mediaTypePos; + } } if (bid.params.trafficSourceCode) { tag.traffic_source_code = bid.params.trafficSourceCode; @@ -1081,9 +1146,13 @@ function buildNativeRequest(params) { * @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'); + try { + const el = document.getElementById(elementId).querySelectorAll("div[id^='google_ads']"); + if (el[0]) { + el[0].style.setProperty('display', 'none'); + } + } catch (e) { + // element not found! } } @@ -1099,12 +1168,13 @@ function hideSASIframe(elementId) { } } -function outstreamRender(bid) { +function outstreamRender(bid, doc) { hidedfpContainer(bid.adUnitCode); hideSASIframe(bid.adUnitCode); // push to render queue because ANOutstreamVideo may not be loaded yet bid.renderer.push(() => { - window.ANOutstreamVideo.renderAd({ + const win = getWindowFromDocument(doc) || window; + win.ANOutstreamVideo.renderAd({ tagId: bid.adResponse.tag_id, sizes: [bid.getSize().split('x')], targetId: bid.adUnitCode, // target div id to render video @@ -1157,4 +1227,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 index 559afefa94b..855aee65f34 100644 --- a/modules/asealBidAdapter.js +++ b/modules/asealBidAdapter.js @@ -1,30 +1,77 @@ import { registerBidder } from '../src/adapters/bidderFactory.js'; import { BANNER } from '../src/mediaTypes.js'; import { config } from '../src/config.js'; +import { generateUUID, getWindowTop, getWindowSelf } from '../src/utils.js'; +import { getStorageManager } from '../src/storageManager.js'; export const BIDDER_CODE = 'aseal'; -const SUPPORTED_AD_TYPES = [BANNER]; +export 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 WEB_SESSION_ID_KEY = '__tkwsid'; +export const HEADER_AOTTER_VERSION = 'prebid_0.0.2'; + +export const storage = getStorageManager({ bidderCode: BIDDER_CODE }); + +const getTrekWebSessionId = () => { + let wsid = + storage.localStorageIsEnabled() && + storage.getDataFromLocalStorage(WEB_SESSION_ID_KEY); + + if (!wsid) { + wsid = generateUUID(); + setTrekWebSessionId(wsid); + } + + return wsid; +}; + +const setTrekWebSessionId = (wsid) => { + if (storage.localStorageIsEnabled()) { + storage.setDataInLocalStorage(WEB_SESSION_ID_KEY, wsid); + } +}; + +const canAccessTopWindow = () => { + try { + return !!getWindowTop().location.href; + } catch (errro) { + return false; + } +}; export const spec = { code: BIDDER_CODE, aliases: ['aotter', 'trek'], supportedMediaTypes: SUPPORTED_AD_TYPES, - - isBidRequestValid: (bid) => !!bid.params.placeUid && typeof bid.params.placeUid === 'string', - + 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 clientId = config.getConfig('aseal.clientId') || ''; + + const windowTop = getWindowTop(); + const windowSelf = getWindowSelf(); + + const w = canAccessTopWindow() ? windowTop : windowSelf; const data = { bids: validBidRequests, refererInfo: bidderRequest.refererInfo, + device: { + webSessionId: getTrekWebSessionId(), + }, + payload: { + meta: { + dr: w.document.referrer, + drs: windowSelf.document.referrer, + drt: (canAccessTopWindow() && windowTop.document.referrer) || '', + dt: w.document.title, + dl: w.location.href, + }, + }, }; const options = { @@ -36,14 +83,15 @@ export const spec = { }, }; - return [{ - method: 'POST', - url: API_ENDPOINT, - data, - options, - }]; + return [ + { + method: 'POST', + url: API_ENDPOINT, + data, + options, + }, + ]; }, - interpretResponse: (serverResponse, bidRequest) => { if (!Array.isArray(serverResponse.body)) { return []; diff --git a/modules/audiencerunBidAdapter.js b/modules/audiencerunBidAdapter.js index 2c100bce27b..2744e38e820 100644 --- a/modules/audiencerunBidAdapter.js +++ b/modules/audiencerunBidAdapter.js @@ -1,32 +1,30 @@ -import { deepAccess, isFn, logError, getValue, getBidIdParameter, _each, isArray, triggerPixel } from '../src/utils.js'; +import { + deepAccess, + isFn, + logError, + getValue, + getBidIdParameter, + _each, + isArray, + triggerPixel, + formatQS, +} from '../src/utils.js'; import { config } from '../src/config.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; import { BANNER } from '../src/mediaTypes.js'; +import { createEidsArray } from './userId/eids.js'; const BIDDER_CODE = 'audiencerun'; const BASE_URL = 'https://d.audiencerun.com'; const AUCTION_URL = `${BASE_URL}/prebid`; const TIMEOUT_EVENT_URL = `${BASE_URL}/ps/pbtimeout`; +const ERROR_EVENT_URL = `${BASE_URL}/js_log`; const DEFAULT_CURRENCY = 'USD'; let requestedBids = []; /** - * Gets bidder request referer - * - * @param {Object} bidderRequest - * @return {string} - */ -function getPageUrl(bidderRequest) { - return ( - config.getConfig('pageUrl') || - deepAccess(bidderRequest, 'refererInfo.referer') || - null - ); -} - -/** - * Returns bidfloor through floors module if available + * Returns bidfloor through floors module if available. * * @param {Object} bid * @returns {number} @@ -44,19 +42,53 @@ function getBidFloor(bid) { }); return bidFloor.floor; } catch (_) { - return 0 + return 0; } } +/** + * Returns the most top page referer. + * + * @returns {string} + */ +function getPageReferer() { + let t, e; + do { + t = t ? t.parent : window; + try { + e = t.document.referrer; + } catch (_) { + break; + } + } while (t !== window.top); + return e; +} + +/** + * Returns bidder request page url. + * + * @param {Object} bidderRequest + * @return {string} + */ +function getPageUrl(bidderRequest) { + return ( + config.getConfig('pageUrl') || + deepAccess(bidderRequest, 'refererInfo.referer') || + getPageReferer() || + null + ); +} + export const spec = { - version: '1.1.0', + version: '1.2.0', code: BIDDER_CODE, + gvlid: 944, supportedMediaTypes: [BANNER], /** * Determines whether or not the given bid request is valid. * - * @param {object} bid The bid to validate. + * @param {BidRequest} bid The bid params to validate. * @return boolean True if this is a valid bid, and false otherwise. */ isBidRequestValid: function (bid) { @@ -95,12 +127,19 @@ export const spec = { const payload = { libVersion: this.version, - referer: getPageUrl(bidderRequest), + pageUrl: config.getConfig('pageUrl'), + pageReferer: getPageReferer(), + referer: deepAccess(bidderRequest, 'refererInfo.referer'), + refererInfo: deepAccess(bidderRequest, 'refererInfo'), currencyCode: config.getConfig('currency.adServerCurrency'), timeout: config.getConfig('bidderTimeout'), bids, }; + payload.uspConsent = deepAccess(bidderRequest, 'uspConsent'); + payload.schain = deepAccess(bidRequests, '0.schain'); + payload.userId = deepAccess(bidRequests, '0.userId') ? createEidsArray(bidRequests[0].userId) : []; + if (bidderRequest && bidderRequest.gdprConsent) { payload.gdpr = { consent: bidderRequest.gdprConsent.consentString, @@ -117,7 +156,7 @@ export const spec = { return { method: 'POST', - url: AUCTION_URL, + url: deepAccess(bidRequests, '0.params.auctionUrl', AUCTION_URL), data: JSON.stringify(payload), options: { withCredentials: true, @@ -201,7 +240,9 @@ export const spec = { } timeoutData.forEach((bid) => { - const bidOnTimeout = requestedBids.find((requestedBid) => requestedBid.bidId === bid.bidId); + const bidOnTimeout = requestedBids.find( + (requestedBid) => requestedBid.bidId === bid.bidId + ); if (bidOnTimeout) { triggerPixel( @@ -210,6 +251,18 @@ export const spec = { } }); }, + + /** + * Registers bidder specific code, which will execute if the bidder responded with an error. + * @param {{bidderRequest: object}} args An object from which we extract bidderRequest object. + */ + onBidderError: function ({ bidderRequest }) { + const queryString = formatQS({ + message: `Prebid.js: Server call for ${bidderRequest.bidderCode} failed.`, + url: encodeURIComponent(getPageUrl(bidderRequest)), + }); + triggerPixel(`${ERROR_EVENT_URL}/?${queryString}`); + }, }; registerBidder(spec); diff --git a/modules/automatadBidAdapter.js b/modules/automatadBidAdapter.js index 726bbef9bd6..e3d0d87783a 100644 --- a/modules/automatadBidAdapter.js +++ b/modules/automatadBidAdapter.js @@ -18,7 +18,7 @@ export const spec = { isBidRequestValid: function (bid) { // will receive request bid. check if have necessary params for bidding - return (bid && bid.hasOwnProperty('params') && bid.params.hasOwnProperty('siteId') && bid.params.hasOwnProperty('placementId') && bid.hasOwnProperty('mediaTypes') && bid.mediaTypes.hasOwnProperty('banner')) + return (bid && bid.hasOwnProperty('params') && bid.params.hasOwnProperty('siteId') && bid.params.siteId != null && bid.hasOwnProperty('mediaTypes') && bid.mediaTypes.hasOwnProperty('banner') && typeof bid.mediaTypes.banner == 'object') }, buildRequests: function (validBidRequests, bidderRequest) { @@ -29,16 +29,29 @@ export const spec = { const siteId = validBidRequests[0].params.siteId const impressions = validBidRequests.map(bidRequest => { - return { - id: bidRequest.bidId, - adUnitCode: bidRequest.adUnitCode, - placement: bidRequest.params.placementId, - banner: { - format: bidRequest.sizes.map(sizeArr => ({ - w: sizeArr[0], - h: sizeArr[1], - })) - }, + if (bidRequest.params.hasOwnProperty('placementId')) { + return { + id: bidRequest.bidId, + adUnitCode: bidRequest.adUnitCode, + placement: bidRequest.params.placementId, + banner: { + format: bidRequest.sizes.map(sizeArr => ({ + w: sizeArr[0], + h: sizeArr[1], + })) + }, + } + } else { + return { + id: bidRequest.bidId, + adUnitCode: bidRequest.adUnitCode, + banner: { + format: bidRequest.sizes.map(sizeArr => ({ + w: sizeArr[0], + h: sizeArr[1], + })) + }, + } } }) diff --git a/modules/automatadBidAdapter.md b/modules/automatadBidAdapter.md index 56a4b53c067..94bc707c75b 100644 --- a/modules/automatadBidAdapter.md +++ b/modules/automatadBidAdapter.md @@ -25,8 +25,8 @@ var adUnits = [ bids: [{ bidder: 'automatad', params: { - siteId: 'someValue', - placementId: 'someValue' + siteId: 'someValue', // required + placementId: 'someValue' // optional } }] } 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/beopBidAdapter.js b/modules/beopBidAdapter.js index 2e74170fcaf..ba960838395 100644 --- a/modules/beopBidAdapter.js +++ b/modules/beopBidAdapter.js @@ -1,4 +1,5 @@ import { deepAccess, isArray, logWarn, triggerPixel, buildUrl, logInfo, getValue, getBidIdParameter } from '../src/utils.js'; +import { getRefererInfo } from '../src/refererDetection.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; import { config } from '../src/config.js'; const BIDDER_CODE = 'beop'; @@ -36,11 +37,11 @@ export const spec = { */ buildRequests: function(validBidRequests, bidderRequest) { const slots = validBidRequests.map(beOpRequestSlotsMaker); - let pageUrl = deepAccess(window, 'location.href') || deepAccess(bidderRequest, 'refererInfo.canonicalUrl') || config.getConfig('pageUrl'); - let fpd = config.getLegacyFpd(config.getConfig('ortb2')); - let gdpr = bidderRequest.gdprConsent; - let firstSlot = slots[0]; - let payloadObject = { + const pageUrl = getPageUrl(bidderRequest.refererInfo, window); + const fpd = config.getLegacyFpd(config.getConfig('ortb2')); + const gdpr = bidderRequest.gdprConsent; + const firstSlot = slots[0]; + const payloadObject = { at: new Date().toString(), nid: firstSlot.nid, nptnid: firstSlot.nptnid, @@ -100,6 +101,7 @@ export const spec = { function buildTrackingParams(data, info, value) { const accountId = data.params.accountId; + const pageUrl = getPageUrl(null, window); return { pid: accountId === undefined ? data.ad.match(/account: \“([a-f\d]{24})\“/)[1] : accountId, nid: data.params.networkId, @@ -110,7 +112,7 @@ function buildTrackingParams(data, info, value) { se_ca: 'bid', se_ac: info, se_va: value, - url: window.location.href + url: pageUrl }; } @@ -141,4 +143,47 @@ function beOpRequestSlotsMaker(bid) { } } +const protocolRelativeRegExp = /^\/\// +function isProtocolRelativeUrl(url) { + return url && url.match(protocolRelativeRegExp) != null; +} + +const withProtocolRegExp = /[a-z]{1,}:\/\// +function isNoProtocolUrl(url) { + return url && url.match(withProtocolRegExp) == null; +} + +function ensureProtocolInUrl(url, defaultProtocol) { + if (isProtocolRelativeUrl(url)) { + return `${defaultProtocol}${url}`; + } else if (isNoProtocolUrl(url)) { + return `${defaultProtocol}//${url}`; + } + return url; +} + +/** + * sometimes trying to access a field (protected?) triggers an exception + * Ex deepAccess(window, 'top.location.href') might throw if it crosses origins + * so here is a lenient version + */ +function safeDeepAccess(obj, path) { + try { + return deepAccess(obj, path) + } catch (_e) { + return null; + } +} + +function getPageUrl(refererInfo, window) { + refererInfo = refererInfo || getRefererInfo(); + let pageUrl = refererInfo.canonicalUrl || safeDeepAccess(window, 'top.location.href') || deepAccess(window, 'location.href'); + // Ensure the protocol is present (looks like sometimes the extracted pageUrl misses it) + if (pageUrl != null) { + const defaultProtocol = safeDeepAccess(window, 'top.location.protocol') || deepAccess(window, 'location.protocol'); + pageUrl = ensureProtocolInUrl(pageUrl, defaultProtocol); + } + return pageUrl; +} + registerBidder(spec); diff --git a/modules/betweenBidAdapter.js b/modules/betweenBidAdapter.js index b2f63488e12..e4907c15974 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'], @@ -53,7 +53,7 @@ export const spec = { params.maxd = video.maxd; params.mind = video.mind; params.pos = 'atf'; - ENDPOINT += '&jst=pvc'; + params.jst = 'pvc'; params.codeType = includes(CODE_TYPES, video.codeType) ? video.codeType : 'inpage'; } @@ -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/biddoBidAdapter.js b/modules/biddoBidAdapter.js new file mode 100644 index 00000000000..5512ca60f8e --- /dev/null +++ b/modules/biddoBidAdapter.js @@ -0,0 +1,92 @@ +import {registerBidder} from '../src/adapters/bidderFactory.js'; +import {BANNER} from '../src/mediaTypes.js'; + +const BIDDER_CODE = 'biddo'; +const ENDPOINT_URL = 'https://ad.adopx.net/delivery/impress'; + +export const spec = { + code: BIDDER_CODE, + supportedMediaTypes: [BANNER], + /** + * Determines whether or not the given bid request is valid. + * + * @param {BidRequest} bidRequest The bid request params to validate. + * @return boolean True if this is a valid bid request, and false otherwise. + */ + isBidRequestValid: function(bidRequest) { + return !!bidRequest.params.zoneId; + }, + /** + * Make a server request from the list of BidRequests. + * + * @param {Array} validBidRequests an array of bid requests + * @return ServerRequest Info describing the request to the server. + */ + buildRequests: function(validBidRequests) { + let serverRequests = []; + + validBidRequests.forEach(bidRequest => { + const sizes = bidRequest.mediaTypes.banner.sizes; + + sizes.forEach(([width, height]) => { + bidRequest.params.requestedSizes = [width, height]; + + const payload = { + ctype: 'div', + pzoneid: bidRequest.params.zoneId, + width, + height, + }; + + const payloadString = Object.keys(payload).map(k => k + '=' + encodeURIComponent(payload[k])).join('&'); + + serverRequests.push({ + method: 'GET', + url: ENDPOINT_URL, + data: payloadString, + bidderRequest: bidRequest, + }); + }); + }); + + return serverRequests; + }, + /** + * Unpack the response from the server into a list of bids. + * + * @param {ServerResponse} serverResponse A successful response from the server. + * @param {BidRequest} bidderRequest A matched bid request for this response. + * @return Array An array of bids which were nested inside the server. + */ + interpretResponse: function(serverResponse, {bidderRequest}) { + const response = serverResponse.body; + const bidResponses = []; + + if (response && response.template && response.template.html) { + const {bidId} = bidderRequest; + const [width, height] = bidderRequest.params.requestedSizes; + + const bidResponse = { + requestId: bidId, + cpm: response.hb.cpm, + creativeId: response.banner.hash, + currency: 'USD', + netRevenue: response.hb.netRevenue, + ttl: 600, + ad: response.template.html, + mediaType: 'banner', + meta: { + advertiserDomains: response.hb.adomains || [], + }, + width, + height, + }; + + bidResponses.push(bidResponse); + } + + return bidResponses; + }, +} + +registerBidder(spec); diff --git a/modules/biddoBidAdapter.md b/modules/biddoBidAdapter.md new file mode 100644 index 00000000000..baea44b22f2 --- /dev/null +++ b/modules/biddoBidAdapter.md @@ -0,0 +1,30 @@ +# Overview + +``` +Module Name: Biddo Bidder Adapter +Module Type: Bidder Adapter +Maintainer: contact@biddo.net +``` + +# Description + +Module that connects to Invamia demand sources. + +# Test Parameters + +``` + const adUnits = [{ + code: 'test-div', + mediaTypes: { + banner: { + sizes: [[300, 250]], + }, + }, + bids: [{ + bidder: 'biddo', + params: { + zoneId: 7254, + }, + }], + }]; +``` diff --git a/modules/bidwatchAnalyticsAdapter.js b/modules/bidwatchAnalyticsAdapter.js new file mode 100644 index 00000000000..26a8c370af3 --- /dev/null +++ b/modules/bidwatchAnalyticsAdapter.js @@ -0,0 +1,90 @@ +import adapter from '../src/AnalyticsAdapter.js'; +import adapterManager from '../src/adapterManager.js'; +import CONSTANTS from '../src/constants.json'; +import { ajax } from '../src/ajax.js'; + +const analyticsType = 'endpoint'; +const url = 'URL_TO_SERVER_ENDPOINT'; + +const { + EVENTS: { + AUCTION_END, + BID_WON, + } +} = CONSTANTS; + +let allEvents = {} +let initOptions = {} +let endpoint = 'https://default' +let objectToSearchForBidderCode = ['bidderRequests', 'bidsReceived', 'noBids'] + +function getAdapterNameForAlias(aliasName) { + return adapterManager.aliasRegistry[aliasName] || aliasName; +} + +function setOriginalBidder(arg) { + Object.keys(arg).forEach(key => { + arg[key]['originalBidder'] = getAdapterNameForAlias(arg[key]['bidderCode']); + if (typeof arg[key]['creativeId'] == 'number') { arg[key]['creativeId'] = arg[key]['creativeId'].toString(); } + }); + return arg +} + +function checkBidderCode(args) { + if (typeof args == 'object') { + for (let i = 0; i < objectToSearchForBidderCode.length; i++) { + if (typeof args[objectToSearchForBidderCode[i]] == 'object') { args[objectToSearchForBidderCode[i]] = setOriginalBidder(args[objectToSearchForBidderCode[i]]) } + } + } + if (typeof args['bidderCode'] == 'string') { args['originalBidder'] = getAdapterNameForAlias(args['bidderCode']); } else if (typeof args['bidder'] == 'string') { args['originalBidder'] = getAdapterNameForAlias(args['bidder']); } + if (typeof args['creativeId'] == 'number') { args['creativeId'] = args['creativeId'].toString(); } + return args +} + +function addEvent(eventType, args) { + if (allEvents[eventType] == undefined) { allEvents[eventType] = [] } + if (eventType && args) { args = checkBidderCode(args); } + allEvents[eventType].push(args); +} + +function handleBidWon(args) { + if (typeof allEvents.bidRequested == 'object' && allEvents.bidRequested.length > 0 && allEvents.bidRequested[0].gdprConsent) { args.gdpr = allEvents.bidRequested[0].gdprConsent; } + ajax(endpoint + '.bidwatch.io/analytics/bid_won', null, JSON.stringify(args), {method: 'POST', withCredentials: true}); +} + +function handleAuctionEnd() { + ajax(endpoint + '.bidwatch.io/analytics/auctions', null, JSON.stringify(allEvents), {method: 'POST', withCredentials: true}); +} + +let bidwatchAnalytics = Object.assign(adapter({url, analyticsType}), { + track({ + eventType, + args + }) { + addEvent(eventType, args); + switch (eventType) { + case AUCTION_END: + handleAuctionEnd(); + break; + case BID_WON: + handleBidWon(args); + break; + } + }}); + +// save the base class function +bidwatchAnalytics.originEnableAnalytics = bidwatchAnalytics.enableAnalytics; + +// override enableAnalytics so we can get access to the config passed in from the page +bidwatchAnalytics.enableAnalytics = function (config) { + bidwatchAnalytics.originEnableAnalytics(config); // call the base class function + initOptions = config.options; + if (initOptions.domain) { endpoint = 'https://' + initOptions.domain; } +}; + +adapterManager.registerAnalyticsAdapter({ + adapter: bidwatchAnalytics, + code: 'bidwatch' +}); + +export default bidwatchAnalytics; diff --git a/modules/bidwatchAnalyticsAdapter.md b/modules/bidwatchAnalyticsAdapter.md new file mode 100644 index 00000000000..bfa453640b8 --- /dev/null +++ b/modules/bidwatchAnalyticsAdapter.md @@ -0,0 +1,21 @@ +# Overview +Module Name: bidwatch Analytics Adapter + +Module Type: Analytics Adapter + +Maintainer: tech@bidwatch.io + +# Description + +Analytics adapter for bidwatch.io. + +# Test Parameters + +``` +{ + provider: 'bidwatch', + options : { + domain: 'test.endpoint' + } +} +``` diff --git a/modules/big-richmediaBidAdapter.js b/modules/big-richmediaBidAdapter.js index cd8b2462eb8..2ee31e8cfd6 100644 --- a/modules/big-richmediaBidAdapter.js +++ b/modules/big-richmediaBidAdapter.js @@ -8,7 +8,7 @@ const BIDDER_CODE = 'big-richmedia'; const metadataByRequestId = {}; export const spec = { - version: '1.4.0', + version: '1.5.0', code: BIDDER_CODE, gvlid: baseAdapter.GVLID, // use base adapter gvlid supportedMediaTypes: [ BANNER, VIDEO ], @@ -78,6 +78,14 @@ export const spec = { customSelector, isReplayable }; + + // This is a workaround needed for the rendering step (so that the adserver iframe does not get resized to 1800x1000 + // when there is skin demand + if (format === 'skin') { + renderParams.width = 1 + renderParams.height = 1 + } + const encoded = window.btoa(JSON.stringify(renderParams)); bid.ad = ` `; diff --git a/modules/bizzclickBidAdapter.js b/modules/bizzclickBidAdapter.js index 6223626834d..a798671cbaf 100644 --- a/modules/bizzclickBidAdapter.js +++ b/modules/bizzclickBidAdapter.js @@ -95,6 +95,7 @@ export const spec = { }, regs: { coppa: config.getConfig('coppa') === true ? 1 : 0, + ext: {} }, user: { ext: {} @@ -106,25 +107,15 @@ export const spec = { imp: [impObject], }; - if (bidderRequest && bidderRequest.uspConsent) { - data.regs.ext.us_privacy = bidderRequest.uspConsent; - } - - if (bidderRequest && bidderRequest.gdprConsent) { - let { gdprApplies, consentString } = bidderRequest.gdprConsent; - data.regs.ext.gdpr = gdprApplies ? 1 : 0; - data.user.ext.consent = consentString; - } - - if (bidRequest.schain) { - deepSetValue(data, 'source.ext.schain', bidRequest.schain); - } - let connection = navigator.connection || navigator.webkitConnection; if (connection && connection.effectiveType) { data.device.connectiontype = connection.effectiveType; } if (bidRequest) { + if (bidRequest.schain) { + deepSetValue(data, 'source.ext.schain', bidRequest.schain); + } + if (bidRequest.gdprConsent && bidRequest.gdprConsent.gdprApplies) { deepSetValue(data, 'regs.ext.gdpr', bidRequest.gdprConsent.gdprApplies ? 1 : 0); deepSetValue(data, 'user.ext.consent', bidRequest.gdprConsent.consentString); 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/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/cointrafficBidAdapter.js b/modules/cointrafficBidAdapter.js index e3d3c65a4f0..f61d58664ca 100644 --- a/modules/cointrafficBidAdapter.js +++ b/modules/cointrafficBidAdapter.js @@ -4,7 +4,7 @@ import { BANNER } from '../src/mediaTypes.js' import { config } from '../src/config.js' const BIDDER_CODE = 'cointraffic'; -const ENDPOINT_URL = 'https://appspb.cointraffic.io/pb/tmp'; +const ENDPOINT_URL = 'https://apps-pbd.ctengine.io/pb/tmp'; const DEFAULT_CURRENCY = 'EUR'; const ALLOWED_CURRENCIES = [ 'EUR', 'USD', 'JPY', 'BGN', 'CZK', 'DKK', 'GBP', 'HUF', 'PLN', 'RON', 'SEK', 'CHF', 'ISK', 'NOK', 'HRK', 'RUB', 'TRY', diff --git a/modules/colossussspBidAdapter.js b/modules/colossussspBidAdapter.js index 72df7c7b465..fec0d1b6510 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) { @@ -60,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, @@ -90,7 +112,6 @@ export const spec = { placementId: bid.params.placement_id, groupId: bid.params.group_id, bidId: bid.bidId, - sizes: bid.mediaTypes[traff].sizes, traffic: traff, eids: [], floor: {} @@ -124,7 +145,12 @@ export const spec = { rtiPartner: 'TDID' }); } + if (traff === BANNER) { + placement.sizes = bid.mediaTypes[BANNER].sizes + } + if (traff === VIDEO) { + placement.sizes = bid.mediaTypes[VIDEO].playerSize; placement.playerSize = bid.mediaTypes[VIDEO].playerSize; placement.minduration = bid.mediaTypes[VIDEO].minduration; placement.maxduration = bid.mediaTypes[VIDEO].maxduration; @@ -175,10 +201,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/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..5fbcc0f8ac1 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, onError}) { + processCmpData(staticConsentData, {onSuccess, onError}) } /** * This function handles interacting with an IAB compliant CMP to obtain the consent information of the user. * Given the async nature of the CMP's API, we pass in acting success/error callback functions to exit this function * based on the appropriate result. - * @param {function(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] || 1; + height = sizes?.[0]?.[1] || 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..d6bf913b366 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, onError}) { + processUspData(staticConsentData, {onSuccess, onError}); } /** * 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.js b/modules/consumableBidAdapter.js index 1a2845ba85b..de08fc8677a 100644 --- a/modules/consumableBidAdapter.js +++ b/modules/consumableBidAdapter.js @@ -122,10 +122,30 @@ export const spec = { bid.currency = 'USD'; bid.creativeId = decision.adId; bid.ttl = 30; - bid.meta = { advertiserDomains: decision.adomain ? decision.adomain : [] } bid.netRevenue = true; bid.referrer = bidRequest.bidderRequest.refererInfo.referer; + bid.meta = { + advertiserDomains: decision.adomain || [] + }; + + if (decision.cats) { + if (decision.cats.length > 0) { + bid.meta.primaryCatId = decision.cats[0]; + if (decision.cats.length > 1) { + bid.meta.secondaryCatIds = decision.cats.slice(1); + } + } + } + + if (decision.networkId) { + bid.meta.networkId = decision.networkId; + } + + if (decision.mediaType) { + bid.meta.mediaType = decision.mediaType; + } + bidResponses.push(bid); } } @@ -136,13 +156,15 @@ export const spec = { getUserSyncs: function(syncOptions, serverResponses) { if (syncOptions.iframeEnabled) { - return [{ - type: 'iframe', - url: 'https://sync.serverbid.com/ss/' + siteId + '.html' - }]; + if (!serverResponses || serverResponses.length === 0 || !serverResponses[0].body.bdr || serverResponses[0].body.bdr !== 'cx') { + return [{ + type: 'iframe', + url: 'https://sync.serverbid.com/ss/' + siteId + '.html' + }]; + } } - if (syncOptions.pixelEnabled && serverResponses.length > 0) { + if (syncOptions.pixelEnabled && serverResponses && serverResponses.length > 0) { return serverResponses[0].body.pixels; } else { logWarn(bidder + ': Please enable iframe based user syncing.'); diff --git a/modules/conversantBidAdapter.js b/modules/conversantBidAdapter.js index d7c3315a814..7ee8b1b7681 100644 --- a/modules/conversantBidAdapter.js +++ b/modules/conversantBidAdapter.js @@ -5,9 +5,9 @@ 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 = { @@ -136,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) { diff --git a/modules/cpexIdSystem.js b/modules/cpexIdSystem.js new file mode 100644 index 00000000000..4600601cb11 --- /dev/null +++ b/modules/cpexIdSystem.js @@ -0,0 +1,49 @@ +/** + * This module adds 'caid' to the User ID module + * The {@link module:modules/userId} module is required + * @module modules/cpexIdSystem + * @requires module:modules/userId + */ + +import { submodule } from '../src/hook.js' +import { getStorageManager } from '../src/storageManager.js' + +window.top.cpexIdVersion = '0.0.3' + +// Returns StorageManager +export const storage = getStorageManager({ gvlid: 570, moduleName: 'cpexId' }) + +// Returns the id string from either cookie or localstorage +const getId = () => { return storage.getCookie('caid') || storage.getDataFromLocalStorage('caid') } + +/** @type {Submodule} */ +export const cpexIdSubmodule = { + /** + * used to link submodule with config + * @type {string} + */ + name: 'cpexId', + /** + * Vendor ID of Czech Publisher Exchange + * @type {Number} + */ + gvlid: 570, + /** + * decode the stored id value for passing to bid requests + * @function decode + * @param {(Object|string)} value + * @returns {(Object|undefined)} + */ + decode (value) { return { cpexId: getId() } }, + /** + * 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) { return { cpexId: getId() } } +} + +submodule('userId', cpexIdSubmodule) diff --git a/modules/cpexIdSystem.md b/modules/cpexIdSystem.md new file mode 100644 index 00000000000..8aceb7fe4ec --- /dev/null +++ b/modules/cpexIdSystem.md @@ -0,0 +1,27 @@ +## CPEx User ID Submodule + +CPExID is provided by [Czech Publisher Exchange](https://www.cpex.cz/), or CPEx. It is a user ID for ad targeting by using first party cookie, or localStorage mechanism. Please contact CPEx before using this ID. + +## Building Prebid with CPExID Support + +First, make sure to add the cpexId to your Prebid.js package with: + +``` +gulp build --modules=cpexIdSystem +``` + +The following configuration parameters are available: + +```javascript +pbjs.setConfig({ + userSync: { + userIds: [{ + name: 'cpexId' + }] + } +}); +``` + +| Param under userSync.userIds[] | Scope | Type | Description | Example | +| --- | --- | --- | --- | --- | +| name | Required | String | The name of this module. | `"cpexId"` | diff --git a/modules/craftBidAdapter.js b/modules/craftBidAdapter.js index 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 0285ab6be5b..2e7cc93cb43 100644 --- a/modules/criteoBidAdapter.js +++ b/modules/criteoBidAdapter.js @@ -1,9 +1,9 @@ -import { isArray, getUniqueIdentifierStr, parseUrl, deepAccess, logWarn, logError, logInfo } 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 { 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 '../src/polyfill.js'; import { verify } from 'criteo-direct-rsa-validate/build/verify.js'; // ref#2 import { getStorageManager } from '../src/storageManager.js'; @@ -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: '; /* @@ -35,7 +35,7 @@ const FAST_BID_PUBKEY_N = 'ztQYwCE5BU7T9CDM5he6rKoabstXRmkzx54zFPZkWbK530dwtLBDe export const spec = { code: BIDDER_CODE, gvlid: GVLID, - supportedMediaTypes: [ BANNER, VIDEO, NATIVE ], + supportedMediaTypes: [BANNER, VIDEO, NATIVE], /** f * @param {object} bid @@ -65,11 +65,11 @@ export const spec = { buildRequests: (bidRequests, bidderRequest) => { let url; let data; - let fpd = config.getLegacyFpd(config.getConfig('ortb2')) || {}; + let fpd = config.getConfig('ortb2') || {}; Object.assign(bidderRequest, { - publisherExt: fpd.context, - userExt: fpd.user, + publisherExt: fpd.site?.ext, + userExt: fpd.user?.ext, ceh: config.getConfig('criteo.ceh') }); @@ -139,6 +139,13 @@ export const spec = { height: slot.height, dealId: slot.dealCode, }; + if (body.ext?.paf?.transmission && slot.ext?.paf?.content_id) { + const pafResponseMeta = { + content_id: slot.ext.paf.content_id, + transmission: response.ext.paf.transmission + }; + bid.meta = Object.assign({}, bid.meta, { paf: pafResponseMeta }); + } if (slot.adomain) { bid.meta = Object.assign({}, bid.meta, { advertiserDomains: slot.adomain }); } @@ -265,11 +272,11 @@ function checkNativeSendId(bidRequest) { return !(bidRequest.nativeParams && ( (bidRequest.nativeParams.image && ((bidRequest.nativeParams.image.sendId !== true || bidRequest.nativeParams.image.sendTargetingKeys === true))) || - (bidRequest.nativeParams.icon && ((bidRequest.nativeParams.icon.sendId !== true || bidRequest.nativeParams.icon.sendTargetingKeys === true))) || - (bidRequest.nativeParams.clickUrl && ((bidRequest.nativeParams.clickUrl.sendId !== true || bidRequest.nativeParams.clickUrl.sendTargetingKeys === true))) || - (bidRequest.nativeParams.displayUrl && ((bidRequest.nativeParams.displayUrl.sendId !== true || bidRequest.nativeParams.displayUrl.sendTargetingKeys === true))) || - (bidRequest.nativeParams.privacyLink && ((bidRequest.nativeParams.privacyLink.sendId !== true || bidRequest.nativeParams.privacyLink.sendTargetingKeys === true))) || - (bidRequest.nativeParams.privacyIcon && ((bidRequest.nativeParams.privacyIcon.sendId !== true || bidRequest.nativeParams.privacyIcon.sendTargetingKeys === true))) + (bidRequest.nativeParams.icon && ((bidRequest.nativeParams.icon.sendId !== true || bidRequest.nativeParams.icon.sendTargetingKeys === true))) || + (bidRequest.nativeParams.clickUrl && ((bidRequest.nativeParams.clickUrl.sendId !== true || bidRequest.nativeParams.clickUrl.sendTargetingKeys === true))) || + (bidRequest.nativeParams.displayUrl && ((bidRequest.nativeParams.displayUrl.sendId !== true || bidRequest.nativeParams.displayUrl.sendTargetingKeys === true))) || + (bidRequest.nativeParams.privacyLink && ((bidRequest.nativeParams.privacyLink.sendId !== true || bidRequest.nativeParams.privacyLink.sendTargetingKeys === true))) || + (bidRequest.nativeParams.privacyIcon && ((bidRequest.nativeParams.privacyIcon.sendId !== true || bidRequest.nativeParams.privacyIcon.sendTargetingKeys === true))) )); } @@ -285,7 +292,7 @@ function buildCdbRequest(context, bidRequests, bidderRequest) { const request = { publisher: { url: context.url, - ext: bidderRequest.publisherExt + ext: bidderRequest.publisherExt, }, slots: bidRequests.map(bidRequest => { networkId = bidRequest.params.networkId || networkId; @@ -312,9 +319,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 = { @@ -375,11 +382,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)); } @@ -406,7 +412,7 @@ function hasValidVideoMediaType(bidRequest) { var requiredMediaTypesParams = ['mimes', 'playerSize', 'maxduration', 'protocols', 'api', 'skip', 'placement', 'playbackmethod']; - requiredMediaTypesParams.forEach(function(param) { + requiredMediaTypesParams.forEach(function (param) { if (deepAccess(bidRequest, 'mediaTypes.video.' + param) === undefined && deepAccess(bidRequest, 'params.video.' + param) === undefined) { isValid = false; logError('Criteo Bid Adapter: mediaTypes.video.' + param + ' is required'); 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 a59a9880af1..289ec8fbf69 100644 --- a/modules/currency.js +++ b/modules/currency.js @@ -5,6 +5,7 @@ import CONSTANTS from '../src/constants.json'; import { ajax } from '../src/ajax.js'; import { config } from '../src/config.js'; import { getHook } from '../src/hook.js'; +import {promiseControls} from '../src/utils/promise.js'; const DEFAULT_CURRENCY_RATE_URL = 'https://cdn.jsdelivr.net/gh/prebid/currency-file@1/latest.json?date=$$TODAY$$'; const CURRENCY_RATE_PRECISION = 4; @@ -21,22 +22,12 @@ var bidderCurrencyDefault = {}; var defaultRates; export const ready = (() => { - let isDone, resolver, promise; + let ctl; function reset() { - isDone = false; - resolver = null; - promise = new Promise((resolve) => { - resolver = resolve; - if (isDone) resolve(); - }) - } - function done() { - isDone = true; - if (resolver != null) { resolver() } + ctl = promiseControls(); } reset(); - - return {done, reset, promise: () => promise} + return {done: () => ctl.resolve(), reset, promise: () => ctl.promise} })(); /** @@ -168,6 +159,8 @@ function initCurrency(url) { } } ); + } else { + ready.done(); } } 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/pbsInterceptor.js b/modules/debugging/pbsInterceptor.js index 5af2384cad9..c8de1ed9753 100644 --- a/modules/debugging/pbsInterceptor.js +++ b/modules/debugging/pbsInterceptor.js @@ -1,6 +1,6 @@ import {deepClone, delayExecution} from '../../src/utils.js'; import {createBid} from '../../src/bidfactory.js'; -import * as CONSTANTS from '../../src/constants.json'; +import {default as CONSTANTS} from '../../src/constants.json'; export function pbsBidInterceptor (next, interceptBids, s2sBidRequest, bidRequests, ajax, { onResponse, 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..37f038d2a67 100644 --- a/modules/dfpAdServerVideo.js +++ b/modules/dfpAdServerVideo.js @@ -9,8 +9,9 @@ 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'; +import {getPPID} from '../src/adserver.js'; /** * @typedef {Object} DfpVideoParams @@ -88,7 +89,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 +119,18 @@ export function buildDfpVideoUrl(options) { const uspConsent = uspDataHandler.getConsentData(); if (uspConsent) { queryParams.us_privacy = uspConsent; } - return buildUrl({ + if (!queryParams.ppid) { + const ppid = getPPID(); + if (ppid != null) { + queryParams.ppid = ppid; + } + } + + 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 +241,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 +270,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 +293,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/distroscaleBidAdapter.js b/modules/distroscaleBidAdapter.js new file mode 100644 index 00000000000..822bea3603a --- /dev/null +++ b/modules/distroscaleBidAdapter.js @@ -0,0 +1,262 @@ +import { logWarn, isPlainObject, isStr, isArray, isFn, inIframe, mergeDeep, deepSetValue, logError, deepClone } from '../src/utils.js'; +import { registerBidder } from '../src/adapters/bidderFactory.js'; +import { config } from '../src/config.js'; +import { BANNER } from '../src/mediaTypes.js'; +const BIDDER_CODE = 'distroscale'; +const SHORT_CODE = 'ds'; +const LOG_WARN_PREFIX = 'DistroScale: '; +const ENDPOINT = 'https://hb.jsrdn.com/hb?from=pbjs'; +const DEFAULT_CURRENCY = 'USD'; +const AUCTION_TYPE = 1; +const GVLID = 754; +const UNDEF = undefined; + +const SUPPORTED_MEDIATYPES = [ BANNER ]; + +function _getHost(url) { + let a = document.createElement('a'); + a.href = url; + return a.hostname; +} + +function _getBidFloor(bid, mType, sz) { + if (isFn(bid.getFloor)) { + let floor = bid.getFloor({ + currency: DEFAULT_CURRENCY, + mediaType: mType || '*', + size: sz || '*' + }); + if (isPlainObject(floor) && !isNaN(floor.floor) && floor.currency === DEFAULT_CURRENCY) { + return floor.floor; + } + } + return null; +} + +function _createImpressionObject(bid) { + var impObj = UNDEF; + var i; + var sizes = {}; + var sizesCount = 0; + + function addSize(arr) { + var w, h; + if (arr && arr.length > 1) { + w = parseInt(arr[0]); + h = parseInt(arr[1]); + } + sizes[w + 'x' + h] = { + w: w, + h: h, + area: w * h, + idx: + ({ + '970x250': 1, + '300x250': 2 + })[w + 'x' + h] || Math.max(w * h, 200) + }; + sizesCount++; + } + + // Gather all sizes + if (isArray(bid.sizes)) { + for (i = 0; i < bid.sizes.length; i++) { + addSize(bid.sizes[i]); + } + } + if (bid.params && bid.params.width && bid.params.height) { + addSize([bid.params.width, bid.params.height]); + } + if (bid.mediaTypes && BANNER in bid.mediaTypes && bid.mediaTypes[BANNER] && bid.mediaTypes[BANNER].sizes) { + for (i = 0; i < bid.mediaTypes[BANNER].sizes.length; i++) { + addSize(bid.mediaTypes[BANNER].sizes[i]); + } + } + if (sizesCount == 0) { + logWarn(LOG_WARN_PREFIX + 'Error: missing sizes: ' + bid.params.adUnit + '. Ignoring the banner impression in the adunit.'); + } else { + // Use the first preferred size + var keys = Object.keys(sizes); + keys.sort(function(a, b) { + return sizes[a].idx - sizes[b].idx + }); + var bannerObj = { + pos: 0, + w: sizes[keys[0]].w, + h: sizes[keys[0]].h, + topframe: inIframe() ? 0 : 1, + format: [{ + 'w': sizes[keys[0]].w, + 'h': sizes[keys[0]].h + }] + }; + + impObj = { + id: bid.bidId, + tagid: bid.params.zoneid || '', + secure: 1, + ext: { + pubid: bid.params.pubid || '', + zoneid: bid.params.zoneid || '' + } + }; + + var floor = _getBidFloor(bid, BANNER, [sizes[keys[0]].w, sizes[keys[0]].h]); + if (floor > 0) { + impObj.bidfloor = floor; + impObj.bidfloorcur = DEFAULT_CURRENCY; + } + + impObj[BANNER] = bannerObj; + } + + return impObj; +} + +export const spec = { + code: BIDDER_CODE, + gvlid: GVLID, + supportedMediaTypes: SUPPORTED_MEDIATYPES, + aliases: [SHORT_CODE], + + isBidRequestValid: bid => { + if (bid && bid.params && bid.params.pubid && isStr(bid.params.pubid)) { + return true; + } else { + logWarn(LOG_WARN_PREFIX + 'Error: pubid is mandatory and cannot be numeric'); + } + return false; + }, + + buildRequests: (validBidRequests, bidderRequest) => { + var pageUrl = (bidderRequest && bidderRequest.refererInfo && bidderRequest.refererInfo.referer) || window.location.href; + + var payload = { + id: '' + (new Date()).getTime(), + at: AUCTION_TYPE, + cur: [DEFAULT_CURRENCY], + site: { + page: pageUrl + }, + device: { + ua: navigator.userAgent, + js: 1, + h: screen.height, + w: screen.width, + language: (navigator.language && navigator.language.replace(/-.*/, '')) || 'en', + dnt: (navigator.doNotTrack == '1' || navigator.msDoNotTrack == '1' || navigator.doNotTrack == 'yes') ? 1 : 0 + }, + imp: [], + user: {}, + ext: {} + }; + + validBidRequests.forEach(b => { + var bid = deepClone(b); + var impObj = _createImpressionObject(bid); + if (impObj) { + payload.imp.push(impObj); + } + }); + + if (payload.imp.length == 0) { + return; + } + + payload.site.domain = _getHost(payload.site.page); + + // add the content object from config in request + if (typeof config.getConfig('content') === 'object') { + payload.site.content = config.getConfig('content'); + } + + // merge the device from config.getConfig('device') + if (typeof config.getConfig('device') === 'object') { + payload.device = Object.assign(payload.device, config.getConfig('device')); + } + + // adding schain object + if (validBidRequests[0].schain) { + deepSetValue(payload, 'source.schain', validBidRequests[0].schain); + } + + // Attaching GDPR Consent Params + if (bidderRequest && bidderRequest.gdprConsent) { + deepSetValue(payload, 'user.consent', bidderRequest.gdprConsent.consentString); + deepSetValue(payload, 'regs.gdpr', (bidderRequest.gdprConsent.gdprApplies ? 1 : 0)); + } + + // CCPA + if (bidderRequest && bidderRequest.uspConsent) { + deepSetValue(payload, 'regs.us_privacy', bidderRequest.uspConsent); + } + + // coppa compliance + if (config.getConfig('coppa') === true) { + deepSetValue(payload, 'regs.coppa', 1); + } + + // First Party Data + const commonFpd = config.getConfig('ortb2') || {}; + if (commonFpd.site) { + mergeDeep(payload, {site: commonFpd.site}); + } + if (commonFpd.user) { + mergeDeep(payload, {user: commonFpd.user}); + } + + // User IDs + if (validBidRequests[0].userIdAsEids && validBidRequests[0].userIdAsEids.length > 0) { + // Standard ORTB structure + deepSetValue(payload, 'user.eids', validBidRequests[0].userIdAsEids); + } else if (validBidRequests[0].userId && Object.keys(validBidRequests[0].userId).length > 0) { + // Fallback to non-ortb structure + deepSetValue(payload, 'user.ext.userId', validBidRequests[0].userId); + } + + return { + method: 'POST', + url: ENDPOINT, + data: payload, + bidderRequest: bidderRequest + }; + }, + + interpretResponse: (response, request) => { + const bidResponses = []; + try { + if (response.body && response.body.seatbid && isArray(response.body.seatbid)) { + // Supporting multiple bid responses for same adSize + response.body.seatbid.forEach(seatbidder => { + seatbidder.bid && + isArray(seatbidder.bid) && + seatbidder.bid.forEach(bid => { + let newBid = { + requestId: bid.impid, + cpm: (parseFloat(bid.price) || 0), + currency: DEFAULT_CURRENCY, + width: parseInt(bid.w), + height: parseInt(bid.h), + creativeId: bid.crid || bid.id, + netRevenue: true, + ttl: 300, + ad: bid.adm, + meta: { + advertiserDomains: [] + } + }; + if (isArray(bid.adomain) && bid.adomain.length > 0) { + newBid.meta.advertiserDomains = bid.adomain; + } + bidResponses.push(newBid); + }); + }); + } + } catch (error) { + logError(error); + } + return bidResponses; + } +}; + +registerBidder(spec); diff --git a/modules/distroscaleBidAdapter.md b/modules/distroscaleBidAdapter.md new file mode 100644 index 00000000000..1d7948b2a02 --- /dev/null +++ b/modules/distroscaleBidAdapter.md @@ -0,0 +1,30 @@ +# Overview + +``` +Module Name: DistroScale Bid Adapter +Module Type: Bidder Adapter +Maintainer: prebid@distroscale.com +``` + +# Description + +Connects to DistroScale exchange for bids. DistroScale bid adapter supports Banner currently. + +# Test Parameters +``` +var adUnits = [{ + code: 'banner-1', + mediaTypes: { + banner: { + sizes: [[970, 250]], + } + }, + bids: [{ + bidder: 'distroscale', + params: { + pubid: '12345' // required, must be a string + ,zoneid: '67890' // optional, must be a string + } + }] +}]; +``` diff --git a/modules/e_volutionBidAdapter.js b/modules/e_volutionBidAdapter.js index 884c4f0c067..63332db8725 100644 --- a/modules/e_volutionBidAdapter.js +++ b/modules/e_volutionBidAdapter.js @@ -41,6 +41,19 @@ function getBidFloor(bid) { } } +function getUserId(eids, id, source, uidExt) { + if (id) { + var uid = { id }; + if (uidExt) { + uid.ext = uidExt; + } + eids.push({ + source, + uids: [ uid ] + }); + } +} + export const spec = { code: BIDDER_CODE, supportedMediaTypes: [BANNER, VIDEO, NATIVE], @@ -86,7 +99,12 @@ export const spec = { const placement = { placementId: bid.params.placementId, bidId: bid.bidId, - bidfloor: getBidFloor(bid) + bidfloor: getBidFloor(bid), + eids: [] + } + + if (bid.userId) { + getUserId(placement.eids, bid.userId.id5id, 'id5-sync.com'); } if (bid.mediaTypes && bid.mediaTypes[BANNER] && bid.mediaTypes[BANNER].sizes) { diff --git a/modules/emx_digitalBidAdapter.js b/modules/emx_digitalBidAdapter.js index 0ed23f11631..66fd2eb2ac1 100644 --- a/modules/emx_digitalBidAdapter.js +++ b/modules/emx_digitalBidAdapter.js @@ -1,9 +1,19 @@ -import { isArray, logWarn, logError, parseUrl, deepAccess, isStr, _each, getBidIdParameter, isFn, isPlainObject } from '../src/utils.js'; -import { registerBidder } from '../src/adapters/bidderFactory.js'; -import { BANNER, VIDEO } from '../src/mediaTypes.js'; -import { Renderer } from '../src/Renderer.js'; -import includes from 'core-js-pure/features/array/includes.js'; -import find from 'core-js-pure/features/array/find.js'; +import { + _each, + deepAccess, + getBidIdParameter, + isArray, + isFn, + isPlainObject, + isStr, + logError, + logWarn, + parseUrl +} from '../src/utils.js'; +import {registerBidder} from '../src/adapters/bidderFactory.js'; +import {BANNER, VIDEO} from '../src/mediaTypes.js'; +import {Renderer} from '../src/Renderer.js'; +import {find, includes} from '../src/polyfill.js'; const BIDDER_CODE = 'emx_digital'; const ENDPOINT = 'hb.emxdgt.com'; diff --git a/modules/engageyaBidAdapter.js b/modules/engageyaBidAdapter.js index 3e0f1d443b1..95ab8ecbd03 100644 --- a/modules/engageyaBidAdapter.js +++ b/modules/engageyaBidAdapter.js @@ -1,12 +1,10 @@ import { BANNER, NATIVE } from '../src/mediaTypes.js'; import { createTrackPixelHtml } from '../src/utils.js'; - -const { - registerBidder -} = require('../src/adapters/bidderFactory.js'); +import {registerBidder} from '../src/adapters/bidderFactory.js'; const BIDDER_CODE = 'engageya'; const ENDPOINT_URL = 'https://recs.engageya.com/rec-api/getrecs.json'; const ENDPOINT_METHOD = 'GET'; +const MAX_DEVIATION = 0.05; const SUPPORTED_SIZES = [ [100, 75], [236, 202], [100, 100], [130, 130], [200, 200], [250, 250], [300, 272], [300, 250], [300, 230], [300, 214], [300, 187], [300, 166], [300, 150], [300, 133], [300, 120], [400, 200], [300, 200], [250, 377], [620, 410], [207, 311], [310, 166], [310, 333], [190, 106], [228, 132], [300, 174], [80, 60], [600, 500], [600, 600], [1080, 610], [1080, 610], [624, 350], [650, 1168], [1080, 1920], [300, 374], [336, 280] ]; @@ -98,7 +96,18 @@ function isValidSize([width, height]) { if (!width || !height) { return false; } - return SUPPORTED_SIZES.some(([supportedWidth, supportedHeight]) => supportedWidth === width && supportedHeight === height); + return SUPPORTED_SIZES.some(([supportedWidth, supportedHeight]) => { + if (supportedWidth === width && supportedHeight === height) { + return true; + } + const supportedRatio = supportedWidth / supportedHeight; + const ratioDeviation = supportedRatio / width * height; + if (Math.abs(ratioDeviation - 1) > MAX_DEVIATION) { + return false; + } + return supportedWidth > width || + (width - supportedWidth) / width <= MAX_DEVIATION; + }); } export const spec = { diff --git a/modules/engageyaBidAdapter.md b/modules/engageyaBidAdapter.md index 541ba548eeb..67205350f71 100644 --- a/modules/engageyaBidAdapter.md +++ b/modules/engageyaBidAdapter.md @@ -3,7 +3,7 @@ ``` Module Name: Engageya's Bidder Adapter Module Type: Bidder Adapter -Maintainer: reem@engageya.com +Maintainer: prebid@engageya.com ``` # Description @@ -65,4 +65,4 @@ Module that connects to Engageya's demand sources ] } ]; -``` \ No newline at end of file +``` diff --git a/modules/eplanningAnalyticsAdapter.js b/modules/eplanningAnalyticsAdapter.js index fb77014400c..365f91382f7 100644 --- a/modules/eplanningAnalyticsAdapter.js +++ b/modules/eplanningAnalyticsAdapter.js @@ -2,8 +2,7 @@ import { logError } from '../src/utils.js'; import {ajax} from '../src/ajax.js'; import adapter from '../src/AnalyticsAdapter.js'; import adapterManager from '../src/adapterManager.js'; - -const CONSTANTS = require('../src/constants.json'); +import CONSTANTS from '../src/constants.json'; const analyticsType = 'endpoint'; const EPL_HOST = 'https://ads.us.e-planning.net/hba/1/'; diff --git a/modules/eplanningBidAdapter.js b/modules/eplanningBidAdapter.js index 330754091a1..780531964ad 100644 --- a/modules/eplanningBidAdapter.js +++ b/modules/eplanningBidAdapter.js @@ -3,9 +3,8 @@ import { getGlobal } from '../src/prebidGlobal.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; import { getStorageManager } from '../src/storageManager.js'; -export const storage = getStorageManager(); - const BIDDER_CODE = 'eplanning'; +export const storage = getStorageManager({bidderCode: BIDDER_CODE}); const rnd = Math.random(); const DEFAULT_SV = 'pbjs.e-planning.net'; const DEFAULT_ISV = 'i.e-planning.net'; diff --git a/modules/fintezaAnalyticsAdapter.js b/modules/fintezaAnalyticsAdapter.js index 12abd2d0efd..7ecc7e963b5 100644 --- a/modules/fintezaAnalyticsAdapter.js +++ b/modules/fintezaAnalyticsAdapter.js @@ -3,9 +3,9 @@ import { ajax } from '../src/ajax.js'; import adapter from '../src/AnalyticsAdapter.js'; import adapterManager from '../src/adapterManager.js'; import { getStorageManager } from '../src/storageManager.js'; +import CONSTANTS from '../src/constants.json'; const storage = getStorageManager(); -const CONSTANTS = require('../src/constants.json'); const ANALYTICS_TYPE = 'endpoint'; const FINTEZA_HOST = 'https://content.mql5.com/tr'; diff --git a/modules/flocIdSystem.js b/modules/flocIdSystem.js index 0cff7e86d73..3fddbaa7129 100644 --- a/modules/flocIdSystem.js +++ b/modules/flocIdSystem.js @@ -28,12 +28,7 @@ function enableOriginTrial(token) { * @param errorCallback */ function getFlocData(successCallback, errorCallback) { - document.interestCohort() - .then((data) => { - successCallback(data); - }).catch((error) => { - errorCallback(error); - }); + errorCallback('The Floc has flown'); } /** @@ -82,7 +77,7 @@ export const flocIdSubmodule = { return; } // Validate feature is enabled - const isFlocEnabled = !!window.chrome && (!!window.chrome.webstore || !!window.chrome.runtime) && !!document.featurePolicy && !!document.featurePolicy.features() && document.featurePolicy.features().includes('interest-cohort'); + const isFlocEnabled = false; if (isFlocEnabled) { const configParams = (config && config.params) || {}; diff --git a/modules/fluctBidAdapter.js b/modules/fluctBidAdapter.js index b7cc848edc4..44b9f3bf217 100644 --- a/modules/fluctBidAdapter.js +++ b/modules/fluctBidAdapter.js @@ -1,6 +1,5 @@ import { _each, isEmpty } from '../src/utils.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; -import URLSearchParams from 'core-js-pure/web/url-search-params' const BIDDER_CODE = 'fluct'; const END_POINT = 'https://hb.adingo.jp/prebid'; diff --git a/modules/ftrackIdSystem.js b/modules/ftrackIdSystem.js new file mode 100644 index 00000000000..21206109ee0 --- /dev/null +++ b/modules/ftrackIdSystem.js @@ -0,0 +1,198 @@ +/** + * This module adds ftrack to the User ID module + * The {@link module:modules/userId} module is required + * @module modules/ftrack + * @requires module:modules/userId + */ + +import * as utils from '../src/utils.js'; +import { submodule } from '../src/hook.js'; +import { getStorageManager } from '../src/storageManager.js'; +import { uspDataHandler } from '../src/adapterManager.js'; + +const MODULE_NAME = 'ftrackId'; +const LOG_PREFIX = 'FTRACK - '; +const LOCAL_STORAGE_EXP_DAYS = 30; +const VENDOR_ID = null; +const LOCAL_STORAGE = 'html5'; +const FTRACK_STORAGE_NAME = 'ftrackId'; +const FTRACK_PRIVACY_STORAGE_NAME = `${FTRACK_STORAGE_NAME}_privacy`; +const FTRACK_URL = 'https://d9.flashtalking.com/d9core'; +const storage = getStorageManager({gvlid: VENDOR_ID, moduleName: MODULE_NAME}); + +let consentInfo = { + gdpr: { + applies: 0, + consentString: null, + pd: null + }, + usPrivacy: { + value: null + } +}; + +/** @type {Submodule} */ +export const ftrackIdSubmodule = { + /** + * used to link submodule with config + * @type {string} + */ + name: `ftrack`, + + /** + * Decodes the 'value' + * @function decode (required method) + * @param {(Object|string)} value + * @param {SubmoduleConfig|undefined} config + * @returns {(Object|undefined)} an object with the key being ideally camel case + * similar to the module name and ending in id or Id + */ + decode (value, config) { + return { + ftrackId: value + }; + }, + + /** + * performs action(s) to obtain ids from D9 and return the Device IDs + * should be the only method that gets a new ID (from ajax calls or a cookie/local storage) + * @function getId (required method) + * @param {SubmoduleConfig} config + * @param {ConsentData} consentData + * @param {(Object|undefined)} cacheIdObj + * @returns {IdResponse|undefined} + */ + getId (config, consentData, cacheIdObj) { + if (this.isConfigOk(config) === false || this.isThereConsent(consentData) === false) return undefined; + + return { + callback: function () { + window.D9v = { + UserID: '99999999999999', + CampID: '3175', + CCampID: '148556' + }; + window.D9r = { + callback: function(response) { + if (response) { + storage.setDataInLocalStorage(`${FTRACK_STORAGE_NAME}_exp`, (new Date(Date.now() + (1000 * 60 * 60 * 24 * LOCAL_STORAGE_EXP_DAYS))).toUTCString()); + storage.setDataInLocalStorage(`${FTRACK_STORAGE_NAME}`, JSON.stringify(response)); + + storage.setDataInLocalStorage(`${FTRACK_PRIVACY_STORAGE_NAME}_exp`, (new Date(Date.now() + (1000 * 60 * 60 * 24 * LOCAL_STORAGE_EXP_DAYS))).toUTCString()); + storage.setDataInLocalStorage(`${FTRACK_PRIVACY_STORAGE_NAME}`, JSON.stringify(consentInfo)); + }; + + return response; + } + }; + + // If config.params.ids does not exist, set defaults + if (!config.params.hasOwnProperty('ids')) { + window.D9r.DeviceID = true; + window.D9r.SingleDeviceID = true; + } else { + if (config.params.ids.hasOwnProperty('device id') && config.params.ids['device id'] === true) { + window.D9r.DeviceID = true; + } + if (config.params.ids.hasOwnProperty('single device id') && config.params.ids['single device id'] === true) { + window.D9r.SingleDeviceID = true; + } + if (config.params.ids.hasOwnProperty('household id') && config.params.ids['household id'] === true) { + window.D9r.HHID = true; + } + } + + if (config.params && config.params.url && config.params.url === FTRACK_URL) { + var ftrackScript = document.createElement('script'); + ftrackScript.setAttribute('src', config.params.url); + window.document.body.appendChild(ftrackScript); + } + } + }; + }, + + /** + * Called when IDs are already in localStorage + * should just be adding additional data to the cacheIdObj object + * @function extendId (optional method) + * @param {SubmoduleConfig} config + * @param {ConsentData} consentData + * @param {(Object|undefined)} cacheIdObj + * @returns {IdResponse|undefined} + */ + extendId (config, consentData, cacheIdObj) { + this.isConfigOk(config); + return cacheIdObj; + }, + + /* + * Validates the config, if it is not correct, then info cannot be saved in localstorage + * @function isConfigOk + * @param {SubmoduleConfig} config from HTML + * @returns {true|false} + */ + isConfigOk: function(config) { + if (!config.storage || !config.storage.type || !config.storage.name) { + utils.logError(LOG_PREFIX + 'config.storage required to be set.'); + return false; + } + + // in a future release, we may return false if storage type or name are not set as required + if (config.storage.type !== LOCAL_STORAGE) { + utils.logWarn(LOG_PREFIX + 'config.storage.type recommended to be "' + LOCAL_STORAGE + '".'); + } + // in a future release, we may return false if storage type or name are not set as required + if (config.storage.name !== FTRACK_STORAGE_NAME) { + utils.logWarn(LOG_PREFIX + 'config.storage.name recommended to be "' + FTRACK_STORAGE_NAME + '".'); + } + + if (!config.hasOwnProperty('params') || !config.params.hasOwnProperty('url') || config.params.url !== FTRACK_URL) { + utils.logWarn(LOG_PREFIX + 'config.params.url is required for ftrack to run. Url should be "' + FTRACK_URL + '".'); + return false; + } + + return true; + }, + + isThereConsent: function(consentData) { + let consentValue = true; + + /* + * Scenario 1: GDPR + * if GDPR Applies is true|1, we do not have consent + * if GDPR Applies does not exist or is false|0, we do not NOT have consent + */ + if (consentData && consentData.gdprApplies && (consentData.gdprApplies === true || consentData.gdprApplies === 1)) { + consentInfo.gdpr.applies = 1; + consentValue = false; + } + // If consentString exists, then we store it even though we are not using it + if (consentData && consentData.consentString !== 'undefined' && !utils.isEmpty(consentData.consentString) && !utils.isEmptyStr(consentData.consentString)) { + consentInfo.gdpr.consentString = consentData.consentString; + } + + /* + * Scenario 2: CCPA/us_privacy + * if usp exists (assuming this check determines the location of the device to be within the California) + * parse the us_privacy string to see if we have consent + * for version 1 of us_privacy strings, if 'Opt-Out Sale' is 'Y' we do not track + */ + const usp = uspDataHandler.getConsentData(); + let usPrivacyVersion; + // let usPrivacyOptOut; + let usPrivacyOptOutSale; + // let usPrivacyLSPA; + if (typeof usp !== 'undefined' && !utils.isEmpty(usp) && !utils.isEmptyStr(usp)) { + consentInfo.usPrivacy.value = usp; + usPrivacyVersion = usp[0]; + // usPrivacyOptOut = usp[1]; + usPrivacyOptOutSale = usp[2]; + // usPrivacyLSPA = usp[3]; + } + if (usPrivacyVersion == 1 && usPrivacyOptOutSale === 'Y') consentValue = false; + + return consentValue; + } +}; + +submodule('userId', ftrackIdSubmodule); diff --git a/modules/ftrackIdSystem.md b/modules/ftrackIdSystem.md new file mode 100644 index 00000000000..0c92f5afab1 --- /dev/null +++ b/modules/ftrackIdSystem.md @@ -0,0 +1,83 @@ +# Flashtalking's FTrack Identity Framework User ID Module + +*The FTrack Identity Framework User ID Module allows publishers to take advantage of Flashtalking's FTrack ID during the bidding process.* + +### [FTrack](https://www.flashtalking.com/identity-framework#FTrack) + +Flashtalking’s cookieless tracking technology uses probabilistic device recognition to derive a privacy-friendly persistent ID for each device. + +**ANTI-FINGERPRINTING** +FTrack operates in strict compliance with [Google’s definition of anti-fingerprinting](https://blog.google/products/ads-commerce/2021-01-privacy-sandbox/). FTrack does not access PII or sensitive information and provides consumers with notification and choice on every impression. We do not participate in the types of activities that most concern privacy advocates (profiling consumers, building audience segments, and/or monetizing consumer data). + +**GDPR COMPLIANT** +Flashtalking is integrated with the IAB EU’s Transparency & Consent Framework (TCF) and operates on a Consent legal basis where required. As a Data Processor under GDPR, Flashtalking does not combine data across customers nor sell data to third parties. + +--- + +### Support or Maintenance: + +Questions? Comments? Bugs? Praise? Please contact FlashTalking's Prebid Support at [prebid-support@flashtalking.com](mailto:prebid-support@flashtalking.com) + +--- + +### FTrack User ID Configuration + +The following configuration parameters are available: + +```javascript +pbjs.setConfig({ + userSync: { + userIds: [{ + name: 'FTrack', + params: { + url: 'https://d9.flashtalking.com/d9core', // required, if not populated ftrack will not run + ids: { + 'device id': true, + 'single device id': true, + 'household id': true + } + }, + storage: { + type: 'html5', // "html5" is the required storage type + name: 'FTrackId', // "FTrackId" is the required storage name + expires: 90, // storage lasts for 90 days + refreshInSeconds: 8*3600 // refresh ID every 8 hours to ensure it's fresh + } + }], + auctionDelay: 50 // 50ms maximum auction delay, applies to all userId modules + } +}); +``` + +| Param under userSync.userIds[] | Scope | Type | Description | Example | +| :-- | :-- | :-- | :-- | :-- | +| name | Required | String | The name of this module: `"FTrack"` | `"FTrack"` | +| params | Required | Object | The IDs available, if not populated then the defaults "Device ID" and "Single Device ID" will be returned | | +| params.url | Required | String | The URL for the ftrack library reference. If not populate, ftrack will not run. | 'https://d9.flashtalking.com/d9core' | +| params.ids | Optional | Object | The ftrack IDs available, if not populated then the defaults "Device ID" and "Single Device ID" will be returned | | +| params.ids['device id'] | Optional | Boolean | Should ftrack return "device id". Set to `true` to return it. If set to `undefined` or `false`, ftrack will not return "device id". Default is `false` | `true` | +| params.ids['single device id'] | Optional | Boolean | Should ftrack return "single device id". Set to `true` to return it. If set to `undefined` or `false`, ftrack will not return "single device id". Default is `false` | `true` | +| params.ids['household id'] | Optional; _Requires pairing with either "device id" or "single device id"_ | Boolean | __1.__ Should ftrack return "household id". Set to `true` to attempt to return it. If set to `undefined` or `false`, ftrack will not return "household id". Default is `false`. __2.__ _This will only return "household id" if value of this field is `true` **AND** "household id" is defined on the device._ __3.__ _"household id" requires either "device id" or "single device id" to be also set to `true`, otherwise ftrack will not return "household id"._ | `true` | +| storage | Required | Object | Storage settings for how the User ID module will cache the FTrack ID locally | | +| storage.type | Required | String | This is where the results of the user ID will be stored. FTrack **requires** `"html5"`. | `"html5"` | +| storage.name | Required | String | The name of the local storage where the user ID will be stored. FTrack **requires** `"FTrackId"`. | `"FTrackId"` | +| storage.expires | Optional | Integer | How long (in days) the user ID information will be stored. FTrack recommends `90`. | `90` | +| storage.refreshInSeconds | Optional | Integer | How many seconds until the FTrack ID will be refreshed. FTrack strongly recommends 8 hours between refreshes | `8*3600` | + +--- + +### Privacy Policies. + +Complete information available on the Flashtalking [privacy policy page](https://www.flashtalking.com/privacypolicy). + +#### OPTING OUT OF INTEREST-BASED ADVERTISING & COLLECTION OF PERSONAL INFORMATION + +Please visit our [Opt Out Page](https://www.flashtalking.com/optout). + +#### REQUEST REMOVAL OF YOUR PERSONAL DATA (WHERE APPLICABLE) + +You may request by emailing [mailto:privacy@flashtalking.com](privacy@flashtalking.com). + +#### GDPR + +In its current state, Flashtalking’s FTrack Identity Framework User ID Module does not create an ID if a user's consentData is "truthy" (true, 1). In other words, if GDPR applies in any way to a user, FTrack does not create an ID. \ No newline at end of file diff --git a/modules/gamoshiBidAdapter.js b/modules/gamoshiBidAdapter.js index 34b164f26ca..22a70db0fab 100644 --- a/modules/gamoshiBidAdapter.js +++ b/modules/gamoshiBidAdapter.js @@ -1,9 +1,21 @@ -import { isFn, isPlainObject, isStr, isNumber, getDNT, deepSetValue, inIframe, isArray, deepAccess, logError, logWarn } from '../src/utils.js'; +import { + deepAccess, + deepSetValue, + getDNT, + inIframe, + isArray, + isFn, + isNumber, + isPlainObject, + isStr, + logError, + logWarn +} from '../src/utils.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; import {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'; const ENDPOINTS = { 'gamoshi': 'https://rtb.gamoshi.io' @@ -99,17 +111,11 @@ export const spec = { source: {ext: {}}, regs: {ext: {}} }; - const gdprConsent = bidderRequest.gdprConsent; - if (gdprConsent && gdprConsent.consentString && gdprConsent.gdprApplies) { - rtbBidRequest.ext.gdpr_consent = { - consent_string: gdprConsent.consentString, - consent_required: gdprConsent.gdprApplies - }; - - deepSetValue(rtbBidRequest, 'regs.ext.gdpr', gdprConsent.gdprApplies === true ? 1 : 0); - deepSetValue(rtbBidRequest, 'user.ext.consent', gdprConsent.consentString); - } + const gdprConsent = getGdprConsent(bidderRequest); + rtbBidRequest.ext.gdpr_consent = gdprConsent; + deepSetValue(rtbBidRequest, 'regs.ext.gdpr', gdprConsent.consent_required === true ? 1 : 0); + deepSetValue(rtbBidRequest, 'user.ext.consent', gdprConsent.consent_string); if (validBidRequests[0].schain) { deepSetValue(rtbBidRequest, 'source.ext.schain', validBidRequests[0].schain); @@ -185,6 +191,7 @@ export const spec = { if (bidRequest && bidRequest.userId) { addExternalUserId(eids, deepAccess(bidRequest, `userId.id5id.uid`), 'id5-sync.com', 'ID5ID'); addExternalUserId(eids, deepAccess(bidRequest, `userId.tdid`), 'adserver.org', 'TDID'); + addExternalUserId(eids, deepAccess(bidRequest, `userId.idl_env`), 'liveramp.com', 'idl'); } if (eids.length > 0) { rtbBidRequest.user.ext.eids = eids; @@ -361,4 +368,20 @@ function replaceMacros(url, macros) { .replace('[US_PRIVACY]', macros.uspConsent); } +function getGdprConsent(bidderRequest) { + const gdprConsent = bidderRequest.gdprConsent; + + if (gdprConsent && gdprConsent.consentString && gdprConsent.gdprApplies) { + return { + consent_string: gdprConsent.consentString, + consent_required: gdprConsent.gdprApplies + }; + } + + return { + consent_required: false, + consent_string: '', + }; +} + registerBidder(spec); diff --git a/modules/gdprEnforcement.js b/modules/gdprEnforcement.js index 978bd8de9e3..161f530f202 100644 --- a/modules/gdprEnforcement.js +++ b/modules/gdprEnforcement.js @@ -2,15 +2,14 @@ * This module gives publishers extra set of features to enforce individual purposes of TCF v2 */ -import { deepAccess, logWarn, isArray, hasDeviceAccess } from '../src/utils.js'; -import { config } from '../src/config.js'; -import adapterManager, { gdprDataHandler } from '../src/adapterManager.js'; -import find from 'core-js-pure/features/array/find.js'; -import includes from 'core-js-pure/features/array/includes.js'; -import { registerSyncInner } from '../src/adapters/bidderFactory.js'; -import { getHook } from '../src/hook.js'; -import { validateStorageEnforcement } from '../src/storageManager.js'; -import events from '../src/events.js'; +import {deepAccess, hasDeviceAccess, isArray, logWarn} from '../src/utils.js'; +import {config} from '../src/config.js'; +import adapterManager, {gdprDataHandler} from '../src/adapterManager.js'; +import {find, includes} from '../src/polyfill.js'; +import {registerSyncInner} from '../src/adapters/bidderFactory.js'; +import {getHook} from '../src/hook.js'; +import {validateStorageEnforcement} from '../src/storageManager.js'; +import * as events from '../src/events.js'; import CONSTANTS from '../src/constants.json'; const TCF2 = { diff --git a/modules/gjirafaBidAdapter.js b/modules/gjirafaBidAdapter.js index c6777ebe44e..48b2cd43c3b 100644 --- a/modules/gjirafaBidAdapter.js +++ b/modules/gjirafaBidAdapter.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, diff --git a/modules/glimpseBidAdapter.js b/modules/glimpseBidAdapter.js index 64b987254e9..35aaf56c604 100644 --- a/modules/glimpseBidAdapter.js +++ b/modules/glimpseBidAdapter.js @@ -1,19 +1,27 @@ -import { BANNER } from '../src/mediaTypes.js' -import { config } from '../src/config.js' -import { getStorageManager } from '../src/storageManager.js' -import { isArray } from '../src/utils.js' -import { registerBidder } from '../src/adapters/bidderFactory.js' - -const storageManager = getStorageManager() - -const GVLID = 1012 -const BIDDER_CODE = 'glimpse' -const ENDPOINT = 'https://api.glimpsevault.io/ads/serving/public/v1/prebid' +import { registerBidder } from '../src/adapters/bidderFactory.js'; +import { config } from '../src/config.js'; +import { BANNER } from '../src/mediaTypes.js'; +import { getStorageManager } from '../src/storageManager.js'; +import { + isArray, + isEmpty, + isEmptyStr, + isStr, + isPlainObject, +} from '../src/utils.js'; + +const GVLID = 1012; +const BIDDER_CODE = 'glimpse'; +const storageManager = getStorageManager({ + gvlid: GVLID, + bidderCode: BIDDER_CODE, +}); +const ENDPOINT = 'https://market.glimpsevault.io/public/v1/prebid'; const LOCAL_STORAGE_KEY = { vault: { jwt: 'gp_vault_jwt', }, -} +}; export const spec = { gvlid: GVLID, @@ -21,126 +29,121 @@ export const spec = { supportedMediaTypes: [BANNER], /** - * Determines whether or not the given bid request is valid + * Determines if the bid request is valid * @param bid {BidRequest} The bid to validate * @return {boolean} */ isBidRequestValid: (bid) => { - return ( - hasValue(bid) && - hasValue(bid.params) && - hasStringValue(bid.params.placementId) - ) + const pid = bid?.params?.pid; + return isStr(pid) && !isEmptyStr(pid); }, /** - * Builds http request for Glimpse bids + * Builds the http request * @param validBidRequests {BidRequest[]} * @param bidderRequest {BidderRequest} * @returns {ServerRequest} */ buildRequests: (validBidRequests, bidderRequest) => { - const auth = getVaultJwt() - const referer = getReferer(bidderRequest) - const gdprConsent = getGdprConsentChoice(bidderRequest) - const bidRequests = validBidRequests.map(processBidRequest) - const firstPartyData = getFirstPartyData() + const url = buildQuery(bidderRequest); + const auth = getVaultJwt(); + const referer = getReferer(bidderRequest); + const imp = validBidRequests.map(processBidRequest); + const fpd = getFirstPartyData(); const data = { auth, data: { referer, - gdprConsent, - bidRequests, - site: firstPartyData.site, - user: firstPartyData.user, - bidderCode: spec.code, - } - } + imp, + fpd, + }, + }; return { method: 'POST', - url: ENDPOINT, + url, data: JSON.stringify(data), options: {}, - } + }; }, /** - * Parse response from Glimpse server - * @param bidResponse {ServerResponse} + * Parse http response + * @param response {ServerResponse} * @returns {Bid[]} */ - interpretResponse: (bidResponse) => { - const isValidResponse = isValidBidResponse(bidResponse) - - if (isValidResponse) { - const {auth, data} = bidResponse.body - setVaultJwt(auth) - return data.bids + interpretResponse: (response) => { + if (isValidResponse(response)) { + const { auth, data } = response.body; + setVaultJwt(auth); + const bids = data.bids.map(processBidResponse); + return bids; } - - return [] + return []; }, -} +}; function setVaultJwt(auth) { - storageManager.setDataInLocalStorage(LOCAL_STORAGE_KEY.vault.jwt, auth) + storageManager.setDataInLocalStorage(LOCAL_STORAGE_KEY.vault.jwt, auth); } function getVaultJwt() { - return storageManager.getDataFromLocalStorage(LOCAL_STORAGE_KEY.vault.jwt) || '' + return ( + storageManager.getDataFromLocalStorage(LOCAL_STORAGE_KEY.vault.jwt) || '' + ); } function getReferer(bidderRequest) { - const hasReferer = - hasValue(bidderRequest) && - hasValue(bidderRequest.refererInfo) && - hasStringValue(bidderRequest.refererInfo.referer) - - if (hasReferer) { - return bidderRequest.refererInfo.referer - } - - return '' + return bidderRequest?.refererInfo?.referer || ''; } -function getGdprConsentChoice(bidderRequest) { - const hasGdprConsent = - hasValue(bidderRequest) && - hasValue(bidderRequest.gdprConsent) +function buildQuery(bidderRequest) { + let url = appendQueryParam(ENDPOINT, 'ver', '$prebid.version$'); - if (hasGdprConsent) { - const gdprConsent = bidderRequest.gdprConsent - const hasGdprApplies = hasBooleanValue(gdprConsent.gdprApplies) + const timeout = config.getConfig('bidderTimeout'); + url = appendQueryParam(url, 'tmax', timeout); - return { - consentString: gdprConsent.consentString || '', - vendorData: gdprConsent.vendorData || {}, - gdprApplies: hasGdprApplies ? gdprConsent.gdprApplies : true, - } + if (gdprApplies(bidderRequest)) { + const consentString = bidderRequest.gdprConsent.consentString; + url = appendQueryParam(url, 'gdpr', consentString); } - return { - consentString: '', - vendorData: {}, - gdprApplies: false, + if (ccpaApplies(bidderRequest)) { + url = appendQueryParam(url, 'ccpa', bidderRequest.uspConsent); + } + + return url; +} + +function appendQueryParam(url, key, value) { + if (!value) { + return url; } + const prefix = url.includes('?') ? '&' : '?'; + return `${url}${prefix}${key}=${encodeURIComponent(value)}`; +} + +function gdprApplies(bidderRequest) { + return Boolean(bidderRequest?.gdprConsent?.gdprApplies); } -function processBidRequest(bidRequest) { - const demand = bidRequest.params.demand || 'glimpse' - const sizes = normalizeSizes(bidRequest.sizes) - const keywords = bidRequest.params.keywords || {} +function ccpaApplies(bidderRequest) { + return ( + isStr(bidderRequest.uspConsent) && + !isEmptyStr(bidderRequest.uspConsent) && + bidderRequest.uspConsent?.substr(1, 3) !== '---' + ); +} + +function processBidRequest(bid) { + const sizes = normalizeSizes(bid.sizes); return { - demand, + bid: bid.bidId, + pid: bid.params.pid, sizes, - keywords, - bidId: bidRequest.bidId, - placementId: bidRequest.params.placementId, - unitCode: bidRequest.adUnitCode, - } + }; } function normalizeSizes(sizes) { @@ -148,84 +151,51 @@ function normalizeSizes(sizes) { isArray(sizes) && sizes.length === 2 && !isArray(sizes[0]) && - !isArray(sizes[1]) + !isArray(sizes[1]); if (isSingleSize) { - return [sizes] + return [sizes]; } - return sizes + return sizes; } function getFirstPartyData() { - const siteKeywords = parseGlobalKeywords('site') - const userKeywords = parseGlobalKeywords('user') - - const siteAttributes = getConfig('ortb2.site.ext.data', {}) - const userAttributes = getConfig('ortb2.user.ext.data', {}) - - return { - site: { - keywords: siteKeywords, - attributes: siteAttributes, - }, - user: { - keywords: userKeywords, - attributes: userAttributes, - }, - } + let fpd = config.getConfig('ortb2') || {}; + optimizeObject(fpd); + return fpd; } -function parseGlobalKeywords(scope) { - const keywords = getConfig(`ortb2.${scope}.keywords`, '') - - return keywords - .split(', ') - .filter((keyword) => keyword !== '') -} - -function getConfig(path, defaultValue) { - return config.getConfig(path) || defaultValue -} - -function isValidBidResponse(bidResponse) { - return ( - hasValue(bidResponse) && - hasValue(bidResponse.body) && - hasValue(bidResponse.body.data) && - hasArrayValue(bidResponse.body.data.bids) && - hasStringValue(bidResponse.body.auth) - ) +function optimizeObject(obj) { + if (!isPlainObject(obj)) { + return; + } + for (const [key, value] of Object.entries(obj)) { + optimizeObject(value); + // only delete empty object, array, or string + if ( + (isPlainObject(value) || isArray(value) || isStr(value)) && + isEmpty(value) + ) { + delete obj[key]; + } + } } -function hasValue(value) { - return ( - value !== undefined && - value !== null - ) +function isValidResponse(bidResponse) { + const auth = bidResponse?.body?.auth; + const bids = bidResponse?.body?.data?.bids; + return isStr(auth) && isArray(bids) && !isEmpty(bids); } -function hasBooleanValue(value) { - return ( - hasValue(value) && - typeof value === 'boolean' - ) -} +function processBidResponse(bid) { + const meta = bid.meta || {}; + meta.advertiserDomains = bid.meta?.advertiserDomains || []; -function hasStringValue(value) { - return ( - hasValue(value) && - typeof value === 'string' && - value.length > 0 - ) -} - -function hasArrayValue(value) { - return ( - hasValue(value) && - isArray(value) && - value.length > 0 - ) + return { + ...bid, + meta, + }; } -registerBidder(spec) +registerBidder(spec); diff --git a/modules/glimpseBidAdapter.md b/modules/glimpseBidAdapter.md index 767efcecf54..e82c5d8f32e 100644 --- a/modules/glimpseBidAdapter.md +++ b/modules/glimpseBidAdapter.md @@ -24,15 +24,14 @@ const adUnits = [ sizes: [[300, 250]], }, }, - bids: [{ - bidder: 'glimpse', - params: { - placementId: 'e53a7f564f8f44cc913b', - keywords: { - country: 'uk', + bids: [ + { + bidder: 'glimpse', + params: { + pid: 'e53a7f564f8f44cc913b', }, }, - }], + ], }, -] +]; ``` diff --git a/modules/glomexBidAdapter.js b/modules/glomexBidAdapter.js index 617a1a3d721..5cabd2515a9 100644 --- a/modules/glomexBidAdapter.js +++ b/modules/glomexBidAdapter.js @@ -1,6 +1,6 @@ -import { registerBidder } from '../src/adapters/bidderFactory.js' -import find from 'core-js-pure/features/array/find.js' -import { BANNER } from '../src/mediaTypes.js' +import {registerBidder} from '../src/adapters/bidderFactory.js'; +import {find} from '../src/polyfill.js'; +import {BANNER} from '../src/mediaTypes.js'; const ENDPOINT = 'https://prebid.mes.glomex.cloud/request-bid' const BIDDER_CODE = 'glomex' diff --git a/modules/gmosspBidAdapter.js b/modules/gmosspBidAdapter.js index fac19896177..087f74906fb 100644 --- a/modules/gmosspBidAdapter.js +++ b/modules/gmosspBidAdapter.js @@ -1,12 +1,9 @@ -import { getDNT, getBidIdParameter, tryAppendQueryString, isEmpty, createTrackPixelHtml, logError, deepSetValue } from '../src/utils.js'; +import { deepAccess, getDNT, getBidIdParameter, tryAppendQueryString, isEmpty, createTrackPixelHtml, logError, deepSetValue, getWindowTop, getWindowLocation } from '../src/utils.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; import { config } from '../src/config.js'; import { BANNER } from '../src/mediaTypes.js'; -import { getStorageManager } from '../src/storageManager.js'; - const BIDDER_CODE = 'gmossp'; const ENDPOINT = 'https://sp.gmossp-sp.jp/hb/prebid/query.ad'; -const storage = getStorageManager(); export const spec = { code: BIDDER_CODE, @@ -34,7 +31,6 @@ export const spec = { const urlInfo = getUrlInfo(bidderRequest.refererInfo); const cur = getCurrencyType(); const dnt = getDNT() ? '1' : '0'; - const imuid = storage.getCookie('_im_uid.1000283') || ''; for (let i = 0; i < validBidRequests.length; i++) { let queryString = ''; @@ -42,6 +38,9 @@ export const spec = { const request = validBidRequests[i]; const tid = request.transactionId; const bid = request.bidId; + const imuid = deepAccess(request, 'userId.imuid'); + const sharedId = deepAccess(request, 'userId.pubcid'); + const idlEnv = deepAccess(request, 'userId.idl_env'); const ver = '$prebid.version$'; const sid = getBidIdParameter('sid', request.params); @@ -50,7 +49,10 @@ export const spec = { queryString = tryAppendQueryString(queryString, 'ver', ver); queryString = tryAppendQueryString(queryString, 'sid', sid); queryString = tryAppendQueryString(queryString, 'im_uid', imuid); + queryString = tryAppendQueryString(queryString, 'shared_id', sharedId); + queryString = tryAppendQueryString(queryString, 'idl_env', idlEnv); queryString = tryAppendQueryString(queryString, 'url', urlInfo.url); + queryString = tryAppendQueryString(queryString, 'meta_url', urlInfo.canonicalLink); queryString = tryAppendQueryString(queryString, 'ref', urlInfo.ref); queryString = tryAppendQueryString(queryString, 'cur', cur); queryString = tryAppendQueryString(queryString, 'dnt', dnt); @@ -112,7 +114,7 @@ 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: function (syncOptions, serverResponses) { const syncs = []; if (!serverResponses.length) { return syncs; @@ -141,27 +143,47 @@ function getCurrencyType() { } function getUrlInfo(refererInfo) { + let canonicalLink = refererInfo.canonicalUrl; + + if (!canonicalLink) { + let metaElements = getMetaElements(); + for (let i = 0; i < metaElements.length && !canonicalLink; i++) { + if (metaElements[i].getAttribute('property') == 'og:url') { + canonicalLink = metaElements[i].content; + } + } + } + return { url: getUrl(refererInfo), + canonicalLink: canonicalLink, ref: getReferrer(), }; } +function getMetaElements() { + try { + return getWindowTop.document.getElementsByTagName('meta'); + } catch (e) { + return document.getElementsByTagName('meta'); + } +} + function getUrl(refererInfo) { if (refererInfo && refererInfo.referer) { return refererInfo.referer; } try { - return window.top.location.href; + return getWindowTop.location.href; } catch (e) { - return window.location.href; + return getWindowLocation.href; } } function getReferrer() { try { - return window.top.document.referrer; + return getWindowTop.document.referrer; } catch (e) { return document.referrer; } diff --git a/modules/gnetBidAdapter.js b/modules/gnetBidAdapter.js index 8b0e953b2b6..274e8db2b50 100644 --- a/modules/gnetBidAdapter.js +++ b/modules/gnetBidAdapter.js @@ -1,9 +1,13 @@ import { registerBidder } from '../src/adapters/bidderFactory.js'; import { _each, isEmpty, parseSizesInput } from '../src/utils.js'; import { BANNER } from '../src/mediaTypes.js'; +import { getStorageManager } from '../src/storageManager.js'; +import {ajax} from '../src/ajax.js'; + +const storage = getStorageManager(); const BIDDER_CODE = 'gnet'; -const ENDPOINT = 'https://service.gnetrtb.com/api/adrequest'; +const ENDPOINT = 'https://service.gnetrtb.com/api'; export const spec = { code: BIDDER_CODE, @@ -36,6 +40,7 @@ export const spec = { data.adUnitCode = request.adUnitCode; data.bidId = request.bidId; data.transactionId = request.transactionId; + data.gftuid = _getCookie(); data.sizes = parseSizesInput(request.sizes); @@ -45,8 +50,7 @@ export const spec = { bidRequests.push({ method: 'POST', - url: ENDPOINT, - mode: 'no-cors', + url: ENDPOINT + '/adrequest', options: { withCredentials: false, }, @@ -99,6 +103,18 @@ export const spec = { return []; }, + + onBidWon: function (bid) { + ajax(ENDPOINT + '/bid-won', null, JSON.stringify(bid), { + method: 'POST', + }); + + return true; + }, }; +function _getCookie() { + return storage.cookiesAreEnabled() ? storage.getCookie('gftuid') : null; +} + registerBidder(spec); diff --git a/modules/goldbachBidAdapter.js b/modules/goldbachBidAdapter.js index 8057925a62c..46ae3054188 100644 --- a/modules/goldbachBidAdapter.js +++ b/modules/goldbachBidAdapter.js @@ -1,35 +1,34 @@ -import { Renderer } from '../src/Renderer.js'; +import {Renderer} from '../src/Renderer.js'; import { - isEmpty, + chunk, convertCamelToUnderscore, - isFn, - createTrackPixelHtml, convertTypes, + createTrackPixelHtml, + deepAccess, deepClone, fill, - getParameterByName, + getBidRequest, getMaxValueFromArray, getMinValueFromArray, - chunk, + getParameterByName, isArray, isArrayOfNums, + isEmpty, + isFn, isNumber, - isStr, isPlainObject, + isStr, logError, logInfo, logMessage, - deepAccess, - getBidRequest, transformBidderParamKeywords } from '../src/utils.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 {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'; const BIDDER_CODE = 'goldbach'; const URL = 'https://ib.adnxs.com/ut/v3/prebid'; @@ -89,9 +88,11 @@ const mappingFileUrl = 'https://acdn.adnxs-simple.com/prebid/appnexus-mapping/ma const SCRIPT_TAG_START = ' { - return {name: 'jwpseg', value: seg}; - }) + segment: segmentProcessing(jwpseg, 'jwpseg'), }] }; } @@ -183,7 +181,9 @@ export const spec = { if (!user) { user = { data: [] }; } - user = mergeDeep(user, { data: ortb2UserData }); + user = mergeDeep(user, { + data: [...ortb2UserData] + }); } if (gdprConsent && gdprConsent.consentString) { @@ -272,6 +272,25 @@ export const spec = { request.regs.coppa = 1; } + const site = config.getConfig('ortb2.site'); + if (site) { + const pageCategory = [...(site.cat || []), ...(site.pagecat || [])].filter((category) => { + return category && typeof category === 'string' + }); + if (pageCategory.length) { + request.site.cat = pageCategory; + } + const genre = deepAccess(site, 'content.genre'); + if (genre && typeof genre === 'string') { + request.site.content = {...request.site.content, genre}; + } + const data = deepAccess(site, 'content.data'); + if (data && data.length) { + const siteContent = request.site.content || {}; + request.site.content = mergeDeep(siteContent, { data }); + } + } + return { method: 'POST', url: ENDPOINT_URL, @@ -475,6 +494,22 @@ function getUserIdFromFPDStorage() { return storage.getDataFromLocalStorage(USER_ID_KEY) || makeNewUserIdInFPDStorage(); } +function segmentProcessing(segment, forceSegName) { + return segment + .map((seg) => { + const value = seg && (seg.value || seg.id || seg); + if (typeof value === 'string' || typeof value === 'number') { + return { + value: value.toString(), + ...(forceSegName && { name: forceSegName }), + ...(seg.name && { name: seg.name }), + }; + } + return null; + }) + .filter((seg) => !!seg); +} + function reformatKeywords(pageKeywords) { const formatedPageKeywords = {}; Object.keys(pageKeywords).forEach((name) => { diff --git a/modules/gridBidAdapter.md b/modules/gridBidAdapter.md index 8eb8dfc19fb..d5ec747aae2 100644 --- a/modules/gridBidAdapter.md +++ b/modules/gridBidAdapter.md @@ -13,11 +13,11 @@ Grid bid adapter supports Banner and Video (instream and outstream). You can allow writing in localStorage `pbjs.setBidderConfig` for the bidder `grid` ``` pbjs.setBidderConfig({ - bidders: ["grid"], - config: { - localStorageWriteAllowed: true - } - }) + bidders: ["grid"], + config: { + localStorageWriteAllowed: true + } +}) ``` # Test Parameters @@ -25,7 +25,11 @@ pbjs.setBidderConfig({ var adUnits = [ { code: 'test-div', - sizes: [[300, 250]], + mediaTypes: { + banner: { + sizes: [[300, 250], [300,600]], + } + }, bids: [ { bidder: "grid", @@ -37,15 +41,19 @@ pbjs.setBidderConfig({ ] },{ code: 'test-div', - sizes: [[728, 90]], bids: [ { bidder: "grid", params: { uid: 2, keywords: { - brandsafety: ['disaster'], - topic: ['stress', 'fear'] + site: { + publisher: [{ + name: 'someKeywordsName', + brandsafety: ['disaster'], + topic: ['stress', 'fear'] + }] + } } } } @@ -54,7 +62,12 @@ pbjs.setBidderConfig({ { code: 'test-div', sizes: [[728, 90]], - mediaTypes: { video: {} }, + mediaTypes: { + video: { + playerSize: [1280, 720], + context: 'instream' + } + }, bids: [ { bidder: "grid", diff --git a/modules/gumgumBidAdapter.js b/modules/gumgumBidAdapter.js index 8e06eb43c14..f7662f54fae 100644 --- a/modules/gumgumBidAdapter.js +++ b/modules/gumgumBidAdapter.js @@ -1,14 +1,13 @@ -import { BANNER, VIDEO } from '../src/mediaTypes.js'; -import { _each, deepAccess, logError, logWarn, parseSizesInput } from '../src/utils.js'; +import {BANNER, VIDEO} from '../src/mediaTypes.js'; +import {_each, deepAccess, logError, logWarn, parseSizesInput} from '../src/utils.js'; -import { config } from '../src/config.js' -import { getStorageManager } from '../src/storageManager.js'; -import includes from 'core-js-pure/features/array/includes'; -import { registerBidder } from '../src/adapters/bidderFactory.js' - -const storage = getStorageManager(); +import {config} from '../src/config.js'; +import {getStorageManager} from '../src/storageManager.js'; +import {includes} from '../src/polyfill.js'; +import {registerBidder} from '../src/adapters/bidderFactory.js'; const BIDDER_CODE = 'gumgum' +const storage = getStorageManager({bidderCode: BIDDER_CODE}); const ALIAS_BIDDER_CODE = ['gg'] const BID_ENDPOINT = `https://g2.gumgum.com/hbid/imp` const JCSI = { t: 0, rq: 8, pbv: '$prebid.version$' } diff --git a/modules/hadronAnalyticsAdapter.js b/modules/hadronAnalyticsAdapter.js new file mode 100644 index 00000000000..0bf805fb94e --- /dev/null +++ b/modules/hadronAnalyticsAdapter.js @@ -0,0 +1,199 @@ +import { ajax } from '../src/ajax.js'; +import adapter from '../src/AnalyticsAdapter.js'; +import adapterManager from '../src/adapterManager.js'; +import * as utils from '../src/utils.js'; +import CONSTANTS from '../src/constants.json'; +import { getStorageManager } from '../src/storageManager.js'; + +/** + * hadronAnalyticsAdapter.js - Audigent Hadron Analytics Adapter + */ + +const HADRON_ANALYTICS_URL = 'https://analytics.hadron.ad.gt/api/v1/analytics' +const HADRONID_ANALYTICS_VER = 'pbadgt0'; +const DEFAULT_PARTNER_ID = 0; +const AU_GVLID = 561; + +export const storage = getStorageManager(); + +var viewId = utils.generateUUID(); + +var partnerId = DEFAULT_PARTNER_ID; +var eventsToTrack = []; + +var w = window; +var d = document; +var e = d.documentElement; +var g = d.getElementsByTagName('body')[0]; +var x = w.innerWidth || e.clientWidth || g.clientWidth; +var y = w.innerHeight || e.clientHeight || g.clientHeight; + +var pageView = { + eventType: 'pageView', + userAgent: window.navigator.userAgent, + timestamp: Date.now(), + timezoneOffset: new Date().getTimezoneOffset(), + language: window.navigator.language, + vendor: window.navigator.vendor, + pageUrl: window.top.location.href, + screenWidth: x, + screenHeight: y +}; + +var eventQueue = [ + pageView +]; + +var startAuction = 0; +var bidRequestTimeout = 0; +let analyticsType = 'endpoint'; + +let hadronAnalyticsAdapter = Object.assign(adapter({url: HADRON_ANALYTICS_URL, analyticsType}), { + track({eventType, args}) { + args = args ? JSON.parse(JSON.stringify(args)) : {}; + var data = {}; + if (!eventsToTrack.includes(eventType)) return; + switch (eventType) { + case CONSTANTS.EVENTS.AUCTION_INIT: { + data = args; + startAuction = data.timestamp; + bidRequestTimeout = data.timeout; + break; + } + + case CONSTANTS.EVENTS.AUCTION_END: { + data = args; + data.start = startAuction; + data.end = Date.now(); + break; + } + + case CONSTANTS.EVENTS.BID_ADJUSTMENT: { + data.bidders = args; + break; + } + + case CONSTANTS.EVENTS.BID_TIMEOUT: { + data.bidders = args; + data.duration = bidRequestTimeout; + break; + } + + case CONSTANTS.EVENTS.BID_REQUESTED: { + data = args; + break; + } + + case CONSTANTS.EVENTS.BID_RESPONSE: { + data = args; + delete data.ad; + break; + } + + case CONSTANTS.EVENTS.BID_WON: { + data = args; + delete data.ad; + delete data.adUrl; + break; + } + + case CONSTANTS.EVENTS.BIDDER_DONE: { + data = args; + break; + } + + case CONSTANTS.EVENTS.SET_TARGETING: { + data.targetings = args; + break; + } + + case CONSTANTS.EVENTS.REQUEST_BIDS: { + data = args; + break; + } + + case CONSTANTS.EVENTS.ADD_AD_UNITS: { + data = args; + break; + } + + case CONSTANTS.EVENTS.AD_RENDER_FAILED: { + data = args; + break; + } + + default: + return; + } + + data.eventType = eventType; + data.timestamp = data.timestamp || Date.now(); + + sendEvent(data); + } +}); + +hadronAnalyticsAdapter.originEnableAnalytics = hadronAnalyticsAdapter.enableAnalytics; + +hadronAnalyticsAdapter.enableAnalytics = function(conf = {}) { + if (typeof conf.options === 'object') { + if (conf.options.partnerId) { + partnerId = conf.options.partnerId; + } else { + partnerId = DEFAULT_PARTNER_ID; + } + if (conf.options.eventsToTrack) { + eventsToTrack = conf.options.eventsToTrack; + } + } else { + utils.logError('HADRON_ANALYTICS_NO_CONFIG_ERROR'); + return; + } + + hadronAnalyticsAdapter.originEnableAnalytics(conf); +} + +function flush() { + // Don't send anything if no partner id was declared + if (partnerId === DEFAULT_PARTNER_ID) return; + if (eventQueue.length > 1) { + var data = { + pageViewId: viewId, + ver: HADRONID_ANALYTICS_VER, + partnerId: partnerId, + events: eventQueue + }; + + ajax(HADRON_ANALYTICS_URL, + () => utils.logInfo('HADRON_ANALYTICS_BATCH_SEND'), + JSON.stringify(data), + { + contentType: 'application/json', + method: 'POST' + } + ); + + eventQueue = [ + pageView + ]; + } +} + +function sendEvent(event) { + eventQueue.push(event); + utils.logInfo(`HADRON_ANALYTICS_EVENT ${event.eventType} `, event); + + if (event.eventType === CONSTANTS.EVENTS.AUCTION_END) { + flush(); + } +} + +adapterManager.registerAnalyticsAdapter({ + adapter: hadronAnalyticsAdapter, + code: 'hadronAnalytics', + gvlid: AU_GVLID +}); + +hadronAnalyticsAdapter.flush = flush; + +export default hadronAnalyticsAdapter; diff --git a/modules/hadronAnalyticsAdapter.md b/modules/hadronAnalyticsAdapter.md new file mode 100644 index 00000000000..f549cf502b2 --- /dev/null +++ b/modules/hadronAnalyticsAdapter.md @@ -0,0 +1,48 @@ +# Overview +Module Name: Hadron Analytics Adapter + +Module Type: Analytics Adapter + +Maintainer: [audigent.com](https://audigent.com) + +# Hadron ID + +The Hadron ID is a container that publishers and ad tech platforms can use to +recognise users' segments where 3rd party cookies are not available. +The Hadron ID is designed to respect users' privacy choices and publishers’ +preferences throughout the advertising value chain. +For more information about the Hadron ID and detailed integration docs, please visit +[our brochure](https://audigent.com/hadron-id). + +# Hadron Analytics Registration + +The Hadron Analytics Adapter is free to use for our customers. +Please visit [audigent/hadron-id](https://audigent.com/hadron-id) to request a demo or get more info. + +The partners' privacy policy is at [https://audigent.com/privacypolicy/#partners](https://audigent.com/privacypolicy/#partners). + +## Hadron Analytics Configuration + +First, make sure to add the Hadron Analytics submodule to your Prebid.js package with: + +``` +gulp build --modules=...,hadronAnalyticsAdapter +``` + +The following configuration parameters are available: + +```javascript +pbjs.enableAnalytics({ + provider: 'hadron', + options: { + partnerId: 1234, // change to the Partner ID you got from Audigent + eventsToTrack: ['auctionEnd','bidWon'] + } +}); +``` + +| Parameter | Scope | Type | Description | Example | +| --- | --- | --- |---------------------------------------------------------| --- | +| provider | Required | String | The name of this module: `hadronAnalytics` | `hadronAnalytics` | +| options.partnerId | Required | Number | This is the Audigent Partner ID obtained from Audigent. | `1234` | +| options.eventsToTrack | Optional | Array of strings | Overrides the set of tracked events | `['auctionEnd','bidWon']` | diff --git a/modules/hadronIdSystem.js b/modules/hadronIdSystem.js index b94fc192ba6..db2620d2422 100644 --- a/modules/hadronIdSystem.js +++ b/modules/hadronIdSystem.js @@ -13,7 +13,7 @@ import { isFn, isStr, isPlainObject, logError } from '../src/utils.js'; const MODULE_NAME = 'hadronId'; const AU_GVLID = 561; -export const storage = getStorageManager(AU_GVLID, 'hadron'); +export const storage = getStorageManager({gvlid: AU_GVLID, moduleName: 'hadron'}); /** * Param or default. diff --git a/modules/hadronRtdProvider.js b/modules/hadronRtdProvider.js index b98b41cf896..0b1081f174a 100644 --- a/modules/hadronRtdProvider.js +++ b/modules/hadronRtdProvider.js @@ -18,7 +18,7 @@ const AU_GVLID = 561; export const HALOID_LOCAL_NAME = 'auHadronId'; export const RTD_LOCAL_NAME = 'auHadronRtd'; -export const storage = getStorageManager(AU_GVLID, SUBMODULE_NAME); +export const storage = getStorageManager({gvlid: AU_GVLID, moduleName: SUBMODULE_NAME}); /** * Deep set an object unless value present. diff --git a/modules/haloIdSystem.js b/modules/haloIdSystem.js index e961f75d31b..2ce18e1e740 100644 --- a/modules/haloIdSystem.js +++ b/modules/haloIdSystem.js @@ -13,7 +13,7 @@ import { isFn, isStr, isPlainObject, logError } from '../src/utils.js'; const MODULE_NAME = 'haloId'; const AU_GVLID = 561; -export const storage = getStorageManager(AU_GVLID, 'halo'); +export const storage = getStorageManager({gvlid: AU_GVLID, moduleName: 'halo'}); /** * Param or default. diff --git a/modules/haloRtdProvider.js b/modules/haloRtdProvider.js index d889310a7c2..1810bfb6f63 100644 --- a/modules/haloRtdProvider.js +++ b/modules/haloRtdProvider.js @@ -18,7 +18,7 @@ const AU_GVLID = 561; export const HALOID_LOCAL_NAME = 'auHaloId'; export const RTD_LOCAL_NAME = 'auHaloRtd'; -export const storage = getStorageManager(AU_GVLID, SUBMODULE_NAME); +export const storage = getStorageManager({gvlid: AU_GVLID, moduleName: SUBMODULE_NAME}); /** * Deep set an object unless value present. diff --git a/modules/hybridBidAdapter.js b/modules/hybridBidAdapter.js index 4383e62c16e..98fecf04d8d 100644 --- a/modules/hybridBidAdapter.js +++ b/modules/hybridBidAdapter.js @@ -1,9 +1,9 @@ -import { _map, logWarn, deepAccess, isArray } from '../src/utils.js'; -import { registerBidder } from '../src/adapters/bidderFactory.js' -import { auctionManager } from '../src/auctionManager.js' -import { BANNER, VIDEO } from '../src/mediaTypes.js' +import {_map, deepAccess, isArray, logWarn} from '../src/utils.js'; +import {registerBidder} from '../src/adapters/bidderFactory.js'; +import {auctionManager} from '../src/auctionManager.js'; +import {BANNER, VIDEO} from '../src/mediaTypes.js'; import {Renderer} from '../src/Renderer.js'; -import find from 'core-js-pure/features/array/find.js'; +import {find} from '../src/polyfill.js'; const BIDDER_CODE = 'hybrid'; const DSP_ENDPOINT = 'https://hbe198.hybrid.ai/prebidhb'; diff --git a/modules/iasRtdProvider.js b/modules/iasRtdProvider.js index 6f7b2d5215d..58899d7a8c0 100644 --- a/modules/iasRtdProvider.js +++ b/modules/iasRtdProvider.js @@ -13,6 +13,28 @@ const FRAUD_FIELD_NAME = 'fr'; const SLOTS_OBJECT_FIELD_NAME = 'slots'; const CUSTOM_FIELD_NAME = 'custom'; const IAS_KW = 'ias-kw'; +const IAS_KEY_MAPPINGS = { + adt: 'adt', + alc: 'alc', + dlm: 'dlm', + hat: 'hat', + off: 'off', + vio: 'vio', + drg: 'drg', + 'ias-kw': 'ias-kw', + fr: 'fr', + vw: 'vw', + grm: 'grm', + pub: 'pub', + vw05: 'vw05', + vw10: 'vw10', + vw15: 'vw15', + vw30: 'vw30', + vw_vv: 'vw_vv', + grm_vv: 'grm_vv', + pub_vv: 'pub_vv', + id: 'id' +}; /** * Module init @@ -26,6 +48,14 @@ export function init(config, userConsent) { utils.logError('missing pubId param for IAS provider'); return false; } + if (params.hasOwnProperty('keyMappings')) { + const keyMappings = params.keyMappings; + for (let prop in keyMappings) { + if (IAS_KEY_MAPPINGS.hasOwnProperty(prop)) { + IAS_KEY_MAPPINGS[prop] = keyMappings[prop] + } + } + } return true; } @@ -62,6 +92,16 @@ function stringifyScreenSize() { return [(window.screen && window.screen.width) || -1, (window.screen && window.screen.height) || -1].join('.'); } +function renameKeyValues(source) { + let result = {}; + for (let prop in IAS_KEY_MAPPINGS) { + if (source.hasOwnProperty(prop)) { + result[IAS_KEY_MAPPINGS[prop]] = source[prop]; + } + } + return result; +} + function formatTargetingData(adUnit) { let result = {}; if (iasTargeting[BRAND_SAFETY_OBJECT_FIELD_NAME]) { @@ -76,7 +116,7 @@ function formatTargetingData(adUnit) { if (iasTargeting[SLOTS_OBJECT_FIELD_NAME] && adUnit in iasTargeting[SLOTS_OBJECT_FIELD_NAME]) { utils.mergeDeep(result, iasTargeting[SLOTS_OBJECT_FIELD_NAME][adUnit]); } - return result; + return renameKeyValues(result); } function constructQueryString(anId, adUnits) { @@ -147,6 +187,7 @@ function getBidRequestData(reqBidsConfigObj, callback, config, userConsent) { undefined, { method: 'GET' } ); + callback() } /** @type {RtdSubmodule} */ diff --git a/modules/id5AnalyticsAdapter.js b/modules/id5AnalyticsAdapter.js index d2803aa3102..69e303b520a 100644 --- a/modules/id5AnalyticsAdapter.js +++ b/modules/id5AnalyticsAdapter.js @@ -3,7 +3,7 @@ import CONSTANTS from '../src/constants.json'; import adapterManager from '../src/adapterManager.js'; import { ajax } from '../src/ajax.js'; import { logInfo, logError } from '../src/utils.js'; -import events from '../src/events.js'; +import * as events from '../src/events.js'; const { EVENTS: { diff --git a/modules/id5IdSystem.js b/modules/id5IdSystem.js index 43d26224164..b57be00d3ac 100644 --- a/modules/id5IdSystem.js +++ b/modules/id5IdSystem.js @@ -5,7 +5,16 @@ * @requires module:modules/userId */ -import { deepAccess, logInfo, deepSetValue, logError, isEmpty, isEmptyStr, logWarn } from '../src/utils.js'; +import { + deepAccess, + logInfo, + deepSetValue, + logError, + isEmpty, + isEmptyStr, + logWarn, + safeJSONParse +} from '../src/utils.js'; import { ajax } from '../src/ajax.js'; import { submodule } from '../src/hook.js'; import { getRefererInfo } from '../src/refererDetection.js'; @@ -24,7 +33,7 @@ const LOG_PREFIX = 'User ID - ID5 submodule: '; // cookie in the array is the most preferred to use const LEGACY_COOKIE_NAMES = [ 'pbjs-id5id', 'id5id.1st', 'id5id' ]; -const storage = getStorageManager(GVLID, MODULE_NAME); +export const storage = getStorageManager({gvlid: GVLID, moduleName: MODULE_NAME}); /** @type {Submodule} */ export const id5IdSubmodule = { @@ -253,7 +262,7 @@ function getLegacyCookieSignature() { let legacyStoredValue; LEGACY_COOKIE_NAMES.forEach(function(cookie) { if (storage.getCookie(cookie)) { - legacyStoredValue = JSON.parse(storage.getCookie(cookie)) || legacyStoredValue; + legacyStoredValue = safeJSONParse(storage.getCookie(cookie)) || legacyStoredValue; } }); return (legacyStoredValue && legacyStoredValue.signature) || ''; diff --git a/modules/idWardRtdProvider.js b/modules/idWardRtdProvider.js new file mode 100644 index 00000000000..a130d3cc8d2 --- /dev/null +++ b/modules/idWardRtdProvider.js @@ -0,0 +1,104 @@ +/** + * This module adds the ID Ward RTD provider to the real time data module + * The {@link module:modules/realTimeData} module is required + * The module will poulate real-time data from ID Ward + * @module modules/idWardRtdProvider + * @requires module:modules/realTimeData + */ +import {config} from '../src/config.js'; +import {getStorageManager} from '../src/storageManager.js'; +import {submodule} from '../src/hook.js'; +import {isPlainObject, mergeDeep, logMessage, logError} from '../src/utils.js'; + +const MODULE_NAME = 'realTimeData'; +const SUBMODULE_NAME = 'idWard'; + +export const storage = getStorageManager({moduleName: SUBMODULE_NAME}); +/** + * Add real-time data & merge segments. + * @param {Object} rtd + */ +function addRealTimeData(rtd) { + if (isPlainObject(rtd.ortb2)) { + const ortb2 = config.getConfig('ortb2') || {}; + logMessage('idWardRtdProvider: merging original: ', ortb2); + logMessage('idWardRtdProvider: merging in: ', rtd.ortb2); + config.setConfig({ortb2: mergeDeep(ortb2, rtd.ortb2)}); + } +} + +/** + * Try parsing stringified array of segment IDs. + * @param {String} data + */ +function tryParse(data) { + try { + return JSON.parse(data); + } catch (err) { + logError(`idWardRtdProvider: failed to parse json:`, data); + return null; + } +} + +/** + * Real-time data retrieval from ID Ward + * @param {Object} reqBidsConfigObj + * @param {function} onDone + * @param {Object} rtdConfig + * @param {Object} userConsent + */ +export function getRealTimeData(reqBidsConfigObj, onDone, rtdConfig, userConsent) { + if (rtdConfig && isPlainObject(rtdConfig.params)) { + const jsonData = storage.getDataFromLocalStorage(rtdConfig.params.cohortStorageKey) + + if (!jsonData) { + return; + } + + const segments = tryParse(jsonData); + + if (segments) { + const udSegment = { + name: 'id-ward.com', + ext: { + segtax: rtdConfig.params.segtax + }, + segment: segments.map(x => ({id: x})) + } + + logMessage('idWardRtdProvider: user.data.segment: ', udSegment); + const data = { + rtd: { + ortb2: { + user: { + data: [ + udSegment + ] + } + } + } + }; + addRealTimeData(data.rtd); + onDone(); + } + } +} + +/** + * Module init + * @param {Object} provider + * @param {Object} userConsent + * @return {boolean} + */ +function init(provider, userConsent) { + return true; +} + +/** @type {RtdSubmodule} */ +export const idWardRtdSubmodule = { + name: SUBMODULE_NAME, + getBidRequestData: getRealTimeData, + init: init +}; + +submodule(MODULE_NAME, idWardRtdSubmodule); diff --git a/modules/idWardRtdProvider.md b/modules/idWardRtdProvider.md new file mode 100644 index 00000000000..5a44bfa49f3 --- /dev/null +++ b/modules/idWardRtdProvider.md @@ -0,0 +1,44 @@ +### Overview + +ID Ward is a data anonymization technology for privacy-preserving advertising. Publishers and advertisers are able to target and retarget custom audience segments covering 100% of consented audiences. +ID Ward’s Real-time Data Provider automatically obtains segment IDs from the ID Ward on-domain script (via localStorage) and passes them to the bid-stream. + +### Integration + + 1) Build the idWardRtd module into the Prebid.js package with: + + ``` + gulp build --modules=idWardRtdProvider,... + ``` + + 2) Use `setConfig` to instruct Prebid.js to initilaize the idWardRtdProvider module, as specified below. + +### Configuration + +``` + pbjs.setConfig({ + realTimeData: { + dataProviders: [ + { + name: "idWard", + waitForIt: true, + params: { + cohortStorageKey: "cohort_ids", + segtax: , + } + } + ] + } + }); + ``` + +Please note that idWardRtdProvider should be integrated into the publisher website along with the [ID Ward Pixel](https://publishers-web.id-ward.com/pixel-integration). +Please reach out to Id Ward representative(support@id-ward.com) if you have any questions or need further help to integrate Prebid, idWardRtdProvider, and Id Ward Pixel + +### Testing +To view an example of available segments returned by Id Ward: +``` +‘gulp serve --modules=rtdModule,idWardRtdProvider,pubmaticBidAdapter +``` +and then point your browser at: +"http://localhost:9999/integrationExamples/gpt/idward_segments_example.html" diff --git a/modules/improvedigitalBidAdapter.js b/modules/improvedigitalBidAdapter.js index a68e90754fb..a0453466b87 100644 --- a/modules/improvedigitalBidAdapter.js +++ b/modules/improvedigitalBidAdapter.js @@ -1,21 +1,66 @@ -import { deepSetValue, logError, _each, getBidRequest, isNumber, isArray, deepAccess, isFn, isPlainObject, logWarn, getBidIdParameter, getUniqueIdentifierStr, isEmpty, isInteger, isStr } 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 { + cleanObj, deepAccess, deepClone, deepSetValue, getBidIdParameter, getBidRequest, getDNT, + getUniqueIdentifierStr, isFn, isPlainObject, logWarn, mergeDeep, parseUrl +} 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 {Renderer} from '../src/Renderer.js'; -import { createEidsArray } from './userId/eids.js'; -import includes from 'core-js-pure/features/array/includes.js'; +import {createEidsArray} from './userId/eids.js'; const BIDDER_CODE = 'improvedigital'; -const RENDERER_URL = 'https://acdn.adnxs.com/video/outstream/ANOutstreamVideo.js'; -const VIDEO_TARGETING = ['skip', 'skipmin', 'skipafter']; +const CREATIVE_TTL = 300; + +const AD_SERVER_URL = 'https://ad.360yield.com/pb'; +const EXTEND_URL = 'https://pbs.360yield.com/openrtb2/auction'; +const IFRAME_SYNC_URL = 'https://hb.360yield.com/prebid-universal-creative/load-cookie.html'; + +const VIDEO_PARAMS = { + DEFAULT_MIMES: ['video/mp4'], + SUPPORTED_PROPERTIES: ['mimes', 'minduration', 'maxduration', 'protocols', 'w', 'h', 'startdelay', 'placement', 'linearity', 'skip', 'skipmin', + 'skipafter', 'sequence', 'battr', 'maxextended', 'minbitrate', 'maxbitrate', 'boxingallowed', 'playbackmethod', 'playbackend', 'delivery', 'pos', 'companionad', + 'api', 'companiontype', 'ext'], + PLACEMENT_TYPE: { + INSTREAM: 1, + OUTSTREAM: 3, + } +}; + +const NATIVE_DATA = { + VERSION: '1.2', + ASSET_TYPES: { + TITLE: 'title', + IMG: 'img', + DATA: 'data', + }, + ASSETS: { + title: {id: 0, name: 'title', assetType: 'title', default: {len: 140}}, + sponsoredBy: {id: 1, name: 'sponsoredBy', assetType: 'data', type: 1}, + icon: {id: 2, name: 'icon', assetType: 'img', type: 2}, + body: {id: 3, name: 'body', assetType: 'data', type: 2}, + image: {id: 4, name: 'image', assetType: 'img', type: 3}, + rating: {id: 5, name: 'rating', assetType: 'data', type: 3}, + likes: {id: 6, name: 'likes', assetType: 'data', type: 4}, + downloads: {id: 7, name: 'downloads', assetType: 'data', type: 5}, + price: {id: 8, name: 'price', assetType: 'data', type: 6}, + salePrice: {id: 9, name: 'salePrice', assetType: 'data', type: 7}, + phone: {id: 10, name: 'phone', assetType: 'data', type: 8}, + address: {id: 11, name: 'address', assetType: 'data', type: 9}, + body2: {id: 12, name: 'body2', assetType: 'data', type: 10}, + displayUrl: {id: 13, name: 'displayUrl', assetType: 'data', type: 11}, + cta: {id: 14, name: 'cta', assetType: 'data', type: 12}, + }, + getAssetById(id) { + return Object.values(this.ASSETS).find(asset => id === asset.id); + } +}; export const spec = { - version: '7.6.0', code: BIDDER_CODE, gvlid: 253, aliases: ['id'], supportedMediaTypes: [BANNER, NATIVE, VIDEO], + syncStore: { extendMode: false, placementId: null }, /** * Determines whether or not the given bid request is valid. @@ -23,7 +68,7 @@ export const spec = { * @param {object} bid The bid to validate. * @return boolean True if this is a valid bid, and false otherwise. */ - isBidRequestValid: function (bid) { + isBidRequestValid(bid) { return !!(bid && bid.params && (bid.params.placementId || (bid.params.placementKey && bid.params.publisherId))); }, @@ -31,174 +76,136 @@ export const spec = { * Make a server request from the list of BidRequests. * * @param {BidRequest[]} bidRequests A non-empty list of bid requests which should be sent to the Server. + * @param bidderRequest * @return ServerRequest Info describing the request to the server. */ - buildRequests: function (bidRequests, bidderRequest) { - let normalizedBids = bidRequests.map((bidRequest) => { - return getNormalizedBidRequest(bidRequest); - }); - - let idClient = new ImproveDigitalAdServerJSClient('hb'); - let requestParameters = { - singleRequestMode: (config.getConfig('improvedigital.singleRequest') === true), - returnObjType: idClient.CONSTANTS.RETURN_OBJ_TYPE.URL_PARAMS_SPLIT, - libVersion: this.version - }; - - const gdprConsent = deepAccess(bidderRequest, 'gdprConsent') - if (gdprConsent) { - // GDPR Consent String - if (gdprConsent.consentString) { - requestParameters.gdpr = gdprConsent.consentString; + buildRequests(bidRequests, bidderRequest) { + const request = { + cur: [config.getConfig('currency.adServerCurrency') || 'USD'], + ext: { + improvedigital: { + sdk: { + name: 'pbjs', + version: '$prebid.version$', + } + } } + }; - // Additional Consent String - const additionalConsent = deepAccess(gdprConsent, 'addtlConsent'); - if (additionalConsent && additionalConsent.indexOf('~') !== -1) { - // Google Ad Tech Provider IDs - const atpIds = additionalConsent.substring(additionalConsent.indexOf('~') + 1); - deepSetValue( - requestParameters, - 'user.ext.consented_providers_settings.consented_providers', - atpIds.split('.').map(id => parseInt(id, 10)) - ); - } + // Device + request.device = (typeof config.getConfig('device') === 'object') ? config.getConfig('device') : {}; + request.device.w = request.device.w || window.innerWidth; + request.device.h = request.device.h || window.innerHeight; + if (getDNT()) { + request.device.dnt = 1; } - if (bidderRequest && bidderRequest.uspConsent) { - requestParameters.usPrivacy = bidderRequest.uspConsent; + // Coppa + const coppa = config.getConfig('coppa'); + if (typeof coppa === 'boolean') { + deepSetValue(request, 'regs.coppa', Number(coppa)); } - if (bidderRequest && bidderRequest.refererInfo && bidderRequest.refererInfo.referer) { - requestParameters.referrer = bidderRequest.refererInfo.referer; - } + if (bidderRequest) { + // GDPR + const gdprConsent = deepAccess(bidderRequest, 'gdprConsent') + if (gdprConsent) { + if (typeof gdprConsent.gdprApplies === 'boolean') { + deepSetValue(request, 'regs.ext.gdpr', Number(gdprConsent.gdprApplies)); + } + deepSetValue(request, 'user.ext.consent', gdprConsent.consentString); + + // Additional Consent String + const additionalConsent = deepAccess(gdprConsent, 'addtlConsent'); + if (additionalConsent && additionalConsent.indexOf('~') !== -1) { + // Google Ad Tech Provider IDs + const atpIds = additionalConsent.substring(additionalConsent.indexOf('~') + 1); + if (atpIds) { + deepSetValue( + request, + 'user.ext.consented_providers_settings.consented_providers', + atpIds.split('.').map(id => parseInt(id, 10)) + ); + } + } + } - // Adding first party data - const site = config.getConfig('ortb2.site'); - if (site) { - const pageCategory = site.pagecat || site.cat; - if (pageCategory && isArray(pageCategory)) { - requestParameters.pagecat = pageCategory.filter((category) => { - return category && isStr(category) - }); + // Timeout + if (bidderRequest.timeout) { + request.tmax = parseInt(bidderRequest.timeout); } - const genre = deepAccess(site, 'content.genre'); - if (genre && isStr(genre)) { - requestParameters.genre = genre; + // US Privacy + if (typeof bidderRequest.uspConsent !== typeof undefined) { + deepSetValue(request, 'regs.ext.us_privacy', bidderRequest.uspConsent); } } - // End of adding first party data - requestParameters.schain = bidRequests[0].schain; - requestParameters.coppa = config.getConfig('coppa') === true; + ID_REQUEST.buildSiteOrApp(request, bidderRequest); - if (bidRequests[0].userId) { - const eids = createEidsArray(bidRequests[0].userId); - if (eids.length) { - deepSetValue(requestParameters, 'user.ext.eids', eids); - } - } + const bidRequest0 = bidRequests[0]; + + deepSetValue(request, 'source.ext.schain', bidRequest0.schain); + deepSetValue(request, 'source.tid', bidRequest0.transactionId); - let requestObj = idClient.createRequest( - normalizedBids, // requestObject - requestParameters - ); + // Save a placement id to send it to the ad server when fetching the user syncs + this.syncStore.placementId = this.syncStore.placementId || bidRequest0.params.placementId; - if (requestObj.errors && requestObj.errors.length > 0) { - logError('ID WARNING 0x01'); + if (bidRequest0.userId) { + const eids = createEidsArray(bidRequest0.userId); + deepSetValue(request, 'user.ext.eids', eids.length ? eids : undefined); } - requestObj.requests.forEach(request => request.bidderRequest = bidderRequest); - return requestObj.requests; + + return ID_REQUEST.buildServerRequests(request, bidRequests, bidderRequest); }, /** * Unpack the response from the server into a list of bids. * * @param {*} serverResponse A successful response from the server. + * @param bidderRequest * @return {Bid[]} An array of bids which were nested inside the server. */ - interpretResponse: function (serverResponse, {bidderRequest}) { + interpretResponse(serverResponse, { bidderRequest }) { + if (!Array.isArray(deepAccess(serverResponse, 'body.seatbid'))) { + return []; + } + const bids = []; - _each(serverResponse.body.bid, function (bidObject) { - if (!bidObject.price || bidObject.price === null || - bidObject.hasOwnProperty('errorCode') || - (!bidObject.adm && !bidObject.native)) { - return; - } - const bidRequest = getBidRequest(bidObject.id, [bidderRequest]); - const bid = {}; - - if (bidObject.native) { - // Native - bid.native = getNormalizedNativeAd(bidObject.native); - // Expose raw oRTB response to the client to allow parsing assets not directly supported by Prebid - bid.ortbNative = bidObject.native; - if (bidObject.nurl) { - bid.native.impressionTrackers.unshift(bidObject.nurl); - } - bid.mediaType = NATIVE; - } else if (bidObject.ad_type && bidObject.ad_type === 'video') { - bid.vastXml = bidObject.adm; - bid.mediaType = VIDEO; - if (isOutstreamVideo(bidRequest)) { - bid.adResponse = { - content: bid.vastXml, - height: bidObject.h, - width: bidObject.w - }; - bid.renderer = createRenderer(bidRequest); - } - } else { - // Banner - let nurl = ''; - if (bidObject.nurl && bidObject.nurl.length > 0) { - nurl = ``; - } - bid.ad = `${nurl}`; - 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; }, @@ -209,549 +216,452 @@ export const spec = { * @param {ServerResponse[]} serverResponses List of server's responses. * @return {UserSync[]} The user syncs which should be dropped. */ - getUserSyncs: function(syncOptions, serverResponses) { - if (syncOptions.pixelEnabled) { - const syncs = []; + getUserSyncs(syncOptions, serverResponses, gdprConsent, uspConsent) { + if (config.getConfig('coppa') === true || !ID_UTIL.hasPurpose1Consent(gdprConsent)) { + return []; + } + + const syncs = []; + if ((this.syncStore.extendMode || !syncOptions.pixelEnabled) && syncOptions.iframeEnabled) { + const { gdprApplies, consentString } = gdprConsent || {}; + syncs.push({ + type: 'iframe', + url: IFRAME_SYNC_URL + + `?placement_id=${this.syncStore.placementId}` + + (this.syncStore.extendMode ? '&pbs=1' : '') + + (typeof gdprApplies === 'boolean' ? `&gdpr=${Number(gdprApplies)}` : '') + + (consentString ? `&gdpr_consent=${consentString}` : '') + + (uspConsent ? `&us_privacy=${encodeURIComponent(uspConsent)}` : '') + }); + } else if (syncOptions.pixelEnabled) { 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(url => { + if (!syncs.some(sync => sync.url === url)) { + syncs.push({ type: 'image', url }); } }); }); - return syncs.map(sync => ({ type: 'image', url: sync })); } - return []; + + return syncs; } }; -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 ]; +registerBidder(spec); + +const ID_REQUEST = { + buildServerRequests(basicRequest, bidRequests, bidderRequest) { + const globalExtendMode = config.getConfig('improvedigital.extend') === true; + const requests = []; + const singleRequestMode = config.getConfig('improvedigital.singleRequest') === true; + + const extendImps = []; + const adServerImps = []; + + function formatRequest(imps, transactionId, extendMode) { + const request = deepClone(basicRequest); + request.imp = imps; + request.id = getUniqueIdentifierStr(); + if (transactionId) { + deepSetValue(request, 'source.tid', transactionId); + } + return { + method: 'POST', + url: extendMode ? EXTEND_URL : AD_SERVER_URL, + data: JSON.stringify(request), + bidderRequest + } + }; + + bidRequests.map((bidRequest) => { + const extendModeEnabled = this.isExtendModeEnabled(globalExtendMode, bidRequest.params); + const imp = this.buildImp(bidRequest, extendModeEnabled); + if (singleRequestMode) { + extendModeEnabled ? extendImps.push(imp) : adServerImps.push(imp); + } else { + requests.push(formatRequest([imp], bidRequest.transactionId, extendModeEnabled)); + } }); - return result; -} -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 (!singleRequestMode) { + return requests; } - if (placementKey) { - normalizedBidRequest.placementKey = placementKey; + // In the single request mode, split imps between those going to the ad server and those going to extend server + if (extendImps.length) { + requests.push(formatRequest(extendImps, null, true)); + } + if (adServerImps.length) { + requests.push(formatRequest(adServerImps, null, false)); } - } - - if (keyValues) { - normalizedBidRequest.keyValues = keyValues; - } - - 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; - } - 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; -} + return requests; + }, -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; - } + isExtendModeEnabled(globalExtendMode, bidParams) { + const extendMode = typeof bidParams.extend === 'boolean' ? bidParams.extend : globalExtendMode; + if (extendMode && !spec.syncStore.extendMode) { + spec.syncStore.extendMode = true; } - }); - // 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); + return extendMode; + }, -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 - } - }; - - this.getErrorReturn = function(errorCode) { - return { - idMappings: {}, - requests: {}, - 'errorCode': errorCode + buildImp(bidRequest, extendMode) { + const imp = { + id: getBidIdParameter('bidId', bidRequest) || getUniqueIdentifierStr(), + secure: Number(window.location.protocol === 'https:'), }; - }; - this.createRequest = function(requestObject, requestParameters, extraRequestParameters) { - if (!requestParameters.libVersion) { - return this.getErrorReturn(this.CONSTANTS.ERROR_CODES.LIB_VERSION_MISSING); + // 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); } - requestParameters.returnObjType = requestParameters.returnObjType || this.CONSTANTS.RETURN_OBJ_TYPE.DEFAULT; - requestParameters.adServerBaseUrl = 'https://' + (requestParameters.adServerBaseUrl || this.CONSTANTS.AD_SERVER_BASE_URL); - - let impressionObjects = []; - let impressionObject; - if (isArray(requestObject)) { - for (let counter = 0; counter < requestObject.length; counter++) { - impressionObject = this.createImpressionObject(requestObject[counter]); - impressionObjects.push(impressionObject); + const bidderParamsPath = extendMode ? 'ext.prebid.bidder.improvedigital' : 'ext.bidder'; + const placementId = getBidIdParameter('placementId', bidRequest.params); + if (placementId) { + deepSetValue(imp, `${bidderParamsPath}.placementId`, placementId); + if (extendMode) { + deepSetValue(imp, 'ext.prebid.storedrequest.id', '' + placementId); } } else { - impressionObject = this.createImpressionObject(requestObject); - impressionObjects.push(impressionObject); + deepSetValue(imp, `${bidderParamsPath}.publisherId`, getBidIdParameter('publisherId', bidRequest.params)); + deepSetValue(imp, `${bidderParamsPath}.placementKey`, getBidIdParameter('placementKey', bidRequest.params)); } - let returnIdMappings = true; - if (requestParameters.returnObjType === this.CONSTANTS.RETURN_OBJ_TYPE.URL_PARAMS_SPLIT) { - returnIdMappings = false; - } + deepSetValue(imp, `${bidderParamsPath}.keyValues`, getBidIdParameter('keyValues', bidRequest.params) || undefined); - let returnObject = {}; - returnObject.requests = []; - if (returnIdMappings) { - returnObject.idMappings = []; + // Adding GPID + const gpid = deepAccess(bidRequest, 'ortb2Imp.ext.gpid') || + deepAccess(bidRequest, 'ortb2Imp.ext.data.pbadslot') || + deepAccess(bidRequest, 'ortb2Imp.ext.data.adserver.adslot'); + + deepSetValue(imp, 'ext.gpid', gpid); + + // Adding Interstitial Signal + if (deepAccess(bidRequest, 'ortb2Imp.instl')) { + imp.instl = 1; } - let errors = null; - let baseUrl = `${requestParameters.adServerBaseUrl}/${this.CONSTANTS.END_POINT}?${this.CONSTANTS.AD_SERVER_URL_PARAM}`; + 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 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--; - } - } + if (deepAccess(bidRequest, 'mediaTypes.banner')) { + imp.banner = this.buildBannerRequest(bidRequest); + } - if (writeLongRequest || - !requestParameters.singleRequestMode || - counter === impressionObjects.length - 1) { - returnObject.requests.push(this.formatRequest(requestParameters, bidRequestObject)); - bidRequestObject = { - bid_request: this.createBasicBidRequestObject(requestParameters, extraRequestParameters) - }; - } + if (deepAccess(bidRequest, 'mediaTypes.native')) { + const nativeImp = this.buildNativeRequest(bidRequest); + if (nativeImp) { + imp.native = nativeImp; } } - if (errors) { - returnObject.errors = errors; - } + return imp; + }, - return returnObject; - }; + buildVideoRequest(bidRequest) { + const videoParams = deepClone(bidRequest.mediaTypes.video); + const videoImproveParams = deepClone(deepAccess(bidRequest, 'params.video', {})); + const video = {...videoParams, ...videoImproveParams}; - 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))}` - }; - default: - const baseUrl = `${requestParameters.adServerBaseUrl}/` + - `${this.CONSTANTS.END_POINT}?${this.CONSTANTS.AD_SERVER_URL_PARAM}`; - return { - url: baseUrl + encodeURIComponent(JSON.stringify(bidRequestObject)) - } + 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; - 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; - } - if (extraRequestParameters) { - for (let prop in extraRequestParameters) { - impressionBidRequestObject[prop] = extraRequestParameters[prop]; - } + // Mimes is required + if (!video.mimes) { + video.mimes = VIDEO_PARAMS.DEFAULT_MIMES; } - if (requestParameters.coppa) { - impressionBidRequestObject.coppa = 1; + // 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; + } } - return impressionBidRequestObject; - }; + Object.keys(video).forEach(prop => { + if (VIDEO_PARAMS.SUPPORTED_PROPERTIES.indexOf(prop) === -1) delete video[prop]; + }); + return video; + }, - this.createImpressionObject = function(placementObject) { - let outputObject = {}; - let impressionObject = {}; - outputObject.impressionObject = impressionObject; + 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; + }, - if (placementObject.id) { - impressionObject.id = placementObject.id; - } 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(); + buildNativeRequest(bidRequest) { + const nativeParams = bidRequest.nativeParams; + if (!nativeParams) { + return null; + } + 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: Number(assetParams.required), + }; + 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); + } } - if (placementObject.placementId) { - impressionObject.pid = placementObject.placementId; + if (!request.assets.length) { + logWarn('No native assets recognized. Ignoring native ad request'); + return null; } - if (placementObject.publisherId) { - impressionObject.pubid = placementObject.publisherId; + return { ver: NATIVE_DATA.VERSION, request: JSON.stringify(request) }; + }, + + isOutstreamVideo(bidRequest) { + return deepAccess(bidRequest, 'mediaTypes.video.context') === 'outstream'; + }, + + getBidFloor(bidRequest) { + if (!isFn(bidRequest.getFloor)) { + return null; } - if (placementObject.placementKey) { - impressionObject.pkey = placementObject.placementKey; + const floor = bidRequest.getFloor({ + currency: 'USD', + mediaType: '*', + size: '*' + }); + if (isPlainObject(floor) && !isNaN(floor.floor) && floor.currency === 'USD') { + return floor.floor; } - if (placementObject.transactionId) { - impressionObject.tid = placementObject.transactionId; + return null; + }, + + buildSiteOrApp(request, bidderRequest) { + const app = {}; + const configAppSettings = config.getConfig('app') || {}; + const fpdAppSettings = config.getConfig('ortb2.app') || {}; + mergeDeep(app, configAppSettings, fpdAppSettings); + + if (Object.keys(app).length !== 0) { + request.app = app; + } else { + 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 (!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; - } + }, +}; + +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) } - if (!isEmpty(video)) { - impressionObject.video = video; + } else { + // Detect media type for multi-format response + if (bidResponse.adm.search(/^(<\?xml| { + // 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; + }, +}; - impressionObject.banner = {}; - if (placementObject.size && placementObject.size.w && placementObject.size.h) { - impressionObject.banner.w = placementObject.size.w; - impressionObject.banner.h = placementObject.size.h; +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; + }, - // Set of desired creative sizes - // Input Format: array of pairs, i.e. [[300, 250], [250, 250]] - if (placementObject.format && isArray(placementObject.format)) { - const format = placementObject.format - .filter(sizePair => 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; - } + 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 }); + }, +}; + +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); } + }, - 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; + 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 = { + hasPurpose1Consent(gdprConsent) { + if (gdprConsent && gdprConsent.gdprApplies && gdprConsent.apiVersion === 2) { + return (deepAccess(gdprConsent, 'vendorData.purpose.consents.1') === true); } - return outputObject; - }; -} + return true; + } +}; diff --git a/modules/insticatorBidAdapter.js b/modules/insticatorBidAdapter.js index ee7a481e7a2..c967f530e75 100644 --- a/modules/insticatorBidAdapter.js +++ b/modules/insticatorBidAdapter.js @@ -1,14 +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, - isArray, -} 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 @@ -17,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: { @@ -95,6 +90,7 @@ function buildImpression(bidRequest) { } function buildDevice() { + const deviceConfig = config.getConfig('device'); const device = { w: window.innerWidth, h: window.innerHeight, @@ -105,8 +101,6 @@ function buildDevice() { }, }; - const deviceConfig = config.getConfig('device'); - if (typeof deviceConfig === 'object') { Object.assign(device, deviceConfig); } 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..1347fa04bd5 100644 --- a/modules/intentIqIdSystem.js +++ b/modules/intentIqIdSystem.js @@ -6,16 +6,17 @@ */ import { logError, logInfo } from '../src/utils.js'; -import {ajax} from '../src/ajax.js'; -import {submodule} from '../src/hook.js' -import {getStorageManager} from '../src/storageManager.js'; +import { ajax } from '../src/ajax.js'; +import { submodule } from '../src/hook.js' +import { getStorageManager } from '../src/storageManager.js'; const PCID_EXPIRY = 365; const MODULE_NAME = 'intentIqId'; export const FIRST_PARTY_KEY = '_iiq_fdata'; +export var FIRST_PARTY_DATA_KEY = '_iiq_fdata'; -export const storage = getStorageManager(undefined, MODULE_NAME); +export const storage = getStorageManager({ gvlid: undefined, moduleName: MODULE_NAME }); const INVALID_ID = 'INVALID_ID'; @@ -117,6 +118,8 @@ export const intentIqIdSubmodule = { logError('User ID - intentIqId submodule requires a valid partner to be defined'); return; } + if (!FIRST_PARTY_DATA_KEY.includes(configParams.partner)) { FIRST_PARTY_DATA_KEY += '_' + configParams.partner } + let rrttStrtTime = 0; // Read Intent IQ 1st party id or generate it if none exists let firstPartyData = tryParse(readData(FIRST_PARTY_KEY)); @@ -126,12 +129,17 @@ export const intentIqIdSubmodule = { storeData(FIRST_PARTY_KEY, JSON.stringify(firstPartyData)); } + let partnerData = tryParse(readData(FIRST_PARTY_DATA_KEY)); + if (!partnerData) partnerData = {}; + // use protocol relative urls for http or https let url = `https://api.intentiq.com/profiles_engine/ProfilesEngineServlet?at=39&mi=10&dpi=${configParams.partner}&pt=17&dpn=1`; url += configParams.pcid ? '&pcid=' + encodeURIComponent(configParams.pcid) : ''; url += configParams.pai ? '&pai=' + encodeURIComponent(configParams.pai) : ''; url += firstPartyData.pcid ? '&iiqidtype=2&iiqpcid=' + encodeURIComponent(firstPartyData.pcid) : ''; url += firstPartyData.pid ? '&pid=' + encodeURIComponent(firstPartyData.pid) : ''; + url += (partnerData.cttl) ? '&cttl=' + encodeURIComponent(partnerData.cttl) : ''; + url += (partnerData.rrtt) ? '&rrtt=' + encodeURIComponent(partnerData.rrtt) : ''; const resp = function (callback) { const callbacks = { @@ -140,14 +148,30 @@ export const intentIqIdSubmodule = { // If response is a valid json and should save is true if (respJson && respJson.ls) { // Store pid field if found in response json + let shouldUpdateLs = false; if ('pid' in respJson) { firstPartyData.pid = respJson.pid; - storeData(FIRST_PARTY_KEY, JSON.stringify(firstPartyData)); + shouldUpdateLs = true; + } + if ('cttl' in respJson) { + partnerData.cttl = respJson.cttl; + shouldUpdateLs = true; } - // If should save and data is empty, means we should save as INVALID_ID if (respJson.data == '') { respJson.data = INVALID_ID; + } else { + partnerData.data = respJson.data; + shouldUpdateLs = true; + } + if (rrttStrtTime && rrttStrtTime > 0) { + partnerData.rrtt = Date.now() - rrttStrtTime; + shouldUpdateLs = true; + } + if (shouldUpdateLs === true) { + partnerData.date = Date.now() + storeData(FIRST_PARTY_KEY, JSON.stringify(firstPartyData)); + storeData(FIRST_PARTY_DATA_KEY, JSON.stringify(partnerData)); } callback(respJson.data); } else { @@ -159,9 +183,13 @@ export const intentIqIdSubmodule = { callback(); } }; - ajax(url, callbacks, undefined, {method: 'GET', withCredentials: true}); + if (partnerData.date && partnerData.cttl && partnerData.data && + Date.now() - partnerData.date < partnerData.cttl) { callback(partnerData.data); } else { + rrttStrtTime = Date.now(); + ajax(url, callbacks, undefined, { method: 'GET', withCredentials: true }); + } }; - return {callback: resp}; + return { callback: resp }; } }; 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..717a886a1f6 100644 --- a/modules/invibesBidAdapter.js +++ b/modules/invibesBidAdapter.js @@ -9,14 +9,14 @@ const CONSTANTS = { SYNC_ENDPOINT: 'https://k.r66net.com/GetUserSync', TIME_TO_LIVE: 300, DEFAULT_CURRENCY: 'EUR', - PREBID_VERSION: 7, + PREBID_VERSION: 8, METHOD: 'GET', INVIBES_VENDOR_ID: 436, USERID_PROVIDERS: ['pubcid', 'pubProvidedId', 'uid2', 'zeotapIdPlus', 'id5id'], 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, @@ -95,8 +95,6 @@ function buildRequest(bidRequests, bidderRequest) { invibes.optIn = invibes.optIn || readGdprConsent(bidderRequest.gdprConsent); invibes.visitId = invibes.visitId || generateRandomId(); - invibes.noCookies = invibes.noCookies || invibes.getCookie('ivNoCookie'); - let lid = initDomainId(invibes.domainOptions); const currentQueryStringParams = parseQueryStringParams(); let userIdModel = getUserIds(_userId); @@ -113,7 +111,7 @@ function buildRequest(bidRequests, bidderRequest) { location: getDocumentLocation(topWin), videoAdHtmlId: generateRandomId(), showFallback: currentQueryStringParams['advs'] === '0', - ivbsCampIdsLocal: invibes.getCookie('IvbsCampIdsLocal'), + ivbsCampIdsLocal: readFromLocalStorage('IvbsCampIdsLocal'), bidParamsJson: JSON.stringify(bidParamsJson), capCounts: getCappedCampaignsAsString(), @@ -129,9 +127,21 @@ function buildRequest(bidRequests, bidderRequest) { purposes: invibes.purposes.toString(), li: invibes.legitimateInterests.toString(), - tc: invibes.gdpr_consent + tc: invibes.gdpr_consent, + isLocalStorageEnabled: storage.hasLocalStorage(), }; + let lid = readFromLocalStorage('ivbsdid'); + if (!lid) { + let str = invibes.getCookie('ivbsdid'); + if (str) { + try { + let cookieLid = JSON.parse(str); + lid = cookieLid.id ? cookieLid.id : cookieLid; + } catch (e) { + } + } + } if (lid) { data.lId = lid; } @@ -172,6 +182,15 @@ function handleResponse(responseObj, bidRequests) { responseObj = responseObj.body || responseObj; responseObj = responseObj.videoAdContentResult || responseObj; + if (responseObj.ShouldSetLId && responseObj.LId) { + if ((!invibes.optIn || !invibes.purposes[0]) && responseObj.PrivacyPolicyRule && responseObj.TcModel && responseObj.TcModel.PurposeConsents) { + invibes.optIn = responseObj.PrivacyPolicyRule; + invibes.purposes = responseObj.TcModel.PurposeConsents; + } + + setInLocalStorage('ivbsdid', responseObj.LId); + } + if (typeof invibes.bidResponse === 'object') { if (responseObj.MultipositionEnabled === true) { invibes.bidResponse.AdPlacements = invibes.bidResponse.AdPlacements.concat(responseObj.AdPlacements); @@ -411,6 +430,22 @@ function renderCreative(bidModel) { .replace('creativeHtml', bidModel.CreativeHtml); } +function readFromLocalStorage(key) { + if (invibes.GdprModuleInstalled && (!invibes.optIn || !invibes.purposes[0])) { + return; + } + + return storage.getDataFromLocalStorage(key) || ''; +} + +function setInLocalStorage(key, value) { + if (!invibes.optIn || !invibes.purposes[0]) { + return; + } + + storage.setDataInLocalStorage(key, value); +} + function getCappedCampaignsAsString() { const key = 'ivvcap'; @@ -471,14 +506,20 @@ function buildSyncUrl() { syncUrl += '?visitId=' + invibes.visitId; syncUrl += '&optIn=' + invibes.optIn; - const did = invibes.getCookie('ivbsdid'); - if (did) { - syncUrl += '&ivbsdid=' + encodeURIComponent(did); + let did = readFromLocalStorage('ivbsdid'); + if (!did) { + let str = invibes.getCookie('ivbsdid'); + if (str) { + try { + let cookieLid = JSON.parse(str); + did = cookieLid.id ? cookieLid.id : cookieLid; + } catch (e) { + } + } } - const bks = invibes.getCookie('ivvbks'); - if (bks) { - syncUrl += '&ivvbks=' + encodeURIComponent(bks); + if (did) { + syncUrl += '&ivbsdid=' + encodeURIComponent(did); } return syncUrl; @@ -486,6 +527,7 @@ function buildSyncUrl() { function readGdprConsent(gdprConsent) { if (gdprConsent && gdprConsent.vendorData) { + invibes.GdprModuleInstalled = true; invibes.gdpr_consent = getVendorConsentData(gdprConsent.vendorData); if (!gdprConsent.vendorData.gdprApplies || gdprConsent.vendorData.hasGlobalConsent) { @@ -528,6 +570,7 @@ function readGdprConsent(gdprConsent) { return 2; } + invibes.GdprModuleInstalled = false; return 0; } @@ -637,34 +680,13 @@ invibes.getCookie = function (name) { return; } - if (!invibes.optIn || !invibes.purposes[0]) { + if (invibes.GdprModuleInstalled && (!invibes.optIn || !invibes.purposes[0])) { return; } return storage.getCookie(name); }; -let initDomainId = function (options) { - let cookiePersistence = { - cname: 'ivbsdid', - load: function () { - let str = invibes.getCookie(this.cname) || ''; - try { - return JSON.parse(str); - } catch (e) { - } - } - }; - - options = options || {}; - - var persistence = options.persistence || cookiePersistence; - - let state = persistence.load(); - - return state ? (state.id || state.tempId) : undefined; -}; - let keywords = (function () { const cap = 300; let headTag = document.getElementsByTagName('head')[0]; @@ -738,7 +760,6 @@ let keywords = (function () { export function resetInvibes() { invibes.optIn = undefined; - invibes.noCookies = undefined; invibes.dom = undefined; invibes.bidResponse = undefined; invibes.domainOptions = undefined; diff --git a/modules/invisiblyAnalyticsAdapter.js b/modules/invisiblyAnalyticsAdapter.js index 5d15ae55bfc..1f0bbfd46c3 100644 --- a/modules/invisiblyAnalyticsAdapter.js +++ b/modules/invisiblyAnalyticsAdapter.js @@ -6,11 +6,11 @@ import adapter from '../src/AnalyticsAdapter.js'; import adapterManager from '../src/adapterManager.js'; import { generateUUID, logInfo } from '../src/utils.js'; +import CONSTANTS from '../src/constants.json'; const DEFAULT_EVENT_URL = 'https://api.pymx5.com/v1/' + 'sites/events'; const analyticsType = 'endpoint'; const analyticsName = 'Invisibly Analytics Adapter:'; -const CONSTANTS = require('../src/constants.json'); const ajax = ajaxBuilder(0); // Events needed diff --git a/modules/iqzoneBidAdapter.js b/modules/iqzoneBidAdapter.js index 9c58267298b..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 || @@ -177,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/ixBidAdapter.js b/modules/ixBidAdapter.js index 0d6b0ca189e..1a9321b6852 100644 --- a/modules/ixBidAdapter.js +++ b/modules/ixBidAdapter.js @@ -1,14 +1,31 @@ -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, + getGptSlotInfoForAdUnitCode, + 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'; @@ -23,11 +40,12 @@ const VIDEO_TIME_TO_LIVE = 3600; // 1hr const NET_REVENUE = true; const MAX_REQUEST_SIZE = 8000; const MAX_REQUEST_LIMIT = 4; +const OUTSTREAM_MINIMUM_PLAYER_SIZE = [300, 250]; const PRICE_TO_DOLLAR_FACTOR = { JPY: 1 }; const USER_SYNC_URL = 'https://js-sec.indexww.com/um/ixmatch.html'; -const RENDERER_URL = 'https://js-sec.indexww.com/htv/video-player.js'; + const FLOOR_SOURCE = { PBJS: 'p', IX: 'x' }; export const ERROR_CODES = { BID_SIZE_INVALID_FORMAT: 1, @@ -84,7 +102,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 = { @@ -818,7 +836,8 @@ function buildIXDiag(validBidRequests) { allu: 0, ren: false, version: '$prebid.version$', - userIds: _getUserIds(validBidRequests[0]) + userIds: _getUserIds(validBidRequests[0]), + url: window.location.href.split('?')[0] }; // create ad unit map and collect the required diag properties @@ -840,12 +859,10 @@ function buildIXDiag(validBidRequests) { if (deepAccess(bid, 'mediaTypes.video.context') === 'outstream') { ixdiag.ou++; - // renderer only needed for outstream - - const hasRenderer = typeof (deepAccess(bid, 'renderer') || deepAccess(bid, 'mediaTypes.video.renderer')) === 'object'; - // if any one ad unit is missing renderer, set ren status to false in diag - ixdiag.ren = ixdiag.ren && hasRenderer ? (deepAccess(ixdiag, 'ren')) : hasRenderer; + if (isIndexRendererPreferred(bid)) { + ixdiag.ren = true; + } } if (deepAccess(bid, 'mediaTypes.video.context') === 'instream') { @@ -950,7 +967,7 @@ function getPageUrl() { * @returns {string} */ function detectParamsType(validBidRequest) { - if (deepAccess(validBidRequest, 'params.video') && deepAccess(validBidRequest, 'mediaTypes.video')) { + if (deepAccess(validBidRequest, 'mediaTypes.video') && bidToVideoImp(validBidRequest).video) { return VIDEO; } @@ -1112,24 +1129,18 @@ function getCachedErrors() { /** * - * Initialize Outstream Renderer + * Initialize IX Outstream Renderer * @param {Object} bid */ function outstreamRenderer(bid) { - bid.renderer.push(() => { - var config = { - width: bid.width, - height: bid.height, - timeout: 3000 - }; - - // IXOutstreamPlayer supports both vastUrl and vastXml, so we can pass either. - // Since vastUrl is going to be deprecated from exchange response, vastXml takes priority. - if (bid.vastXml) { - window.IXOutstreamPlayer(bid.vastXml, bid.adUnitCode, config); - } else { - window.IXOutstreamPlayer(bid.vastUrl, bid.adUnitCode, config); + bid.renderer.push(function () { + const adUnitCode = bid.adUnitCode; + const divId = document.getElementById(adUnitCode) ? adUnitCode : getGptSlotInfoForAdUnitCode(adUnitCode).divId; + if (!divId) { + logWarn(`IX Bid Adapter: adUnitCode: ${divId} not found on page.`); + return; } + window.createIXPlayer(divId, bid); }); } @@ -1138,10 +1149,10 @@ function outstreamRenderer(bid) { * @param {string} id * @returns {Renderer} */ -function createRenderer(id) { +function createRenderer(id, renderUrl) { const renderer = Renderer.install({ id: id, - url: RENDERER_URL, + url: renderUrl, loaded: false }); @@ -1149,11 +1160,37 @@ function createRenderer(id) { renderer.setRender(outstreamRenderer); } catch (err) { logWarn('Prebid Error calling setRender on renderer', err); + return null; + } + + if (!renderUrl) { + logWarn('Outstream renderer URL not found'); + return null; } return renderer; } +/** + * Returns whether our renderer could potentially be used. + * @param {*} bid bid object + */ +function isIndexRendererPreferred(bid) { + if (deepAccess(bid, 'mediaTypes.video.context') !== 'outstream') { + return false; + } + + // ad unit renderer could be on the adUnit.mediaTypes.video level or adUnit level + let renderer = deepAccess(bid, 'mediaTypes.video.renderer'); + if (!renderer) { + renderer = deepAccess(bid, 'renderer'); + } + + const isValid = !!(typeof (renderer) === 'object' && renderer.url && renderer.render); + // if renderer on the adunit is not valid or it's only a backup, our renderer may be used + return !isValid || renderer.backupOnly; +} + export const spec = { code: BIDDER_CODE, @@ -1237,6 +1274,17 @@ export const spec = { return false; } } + + const videoImp = bidToVideoImp(bid).video; + if (deepAccess(bid, 'mediaTypes.video.context') === OUTSTREAM && isIndexRendererPreferred(bid) && videoImp) { + const outstreamPlayerSize = deepAccess(videoImp, 'playerSize')[0]; + const isValidSize = outstreamPlayerSize[0] >= OUTSTREAM_MINIMUM_PLAYER_SIZE[0] && outstreamPlayerSize[1] >= OUTSTREAM_MINIMUM_PLAYER_SIZE[1]; + if (!isValidSize) { + logError(`IX Bid Adapter: ${mediaTypeVideoPlayerSize} is an invalid size for IX outstream renderer`); + return false; + } + } + return true; }, @@ -1347,8 +1395,12 @@ export const spec = { const bidRequest = getBidRequest(innerBids[j].impid, requestBid.imp, bidderRequest.validBidRequests); bid = parseBid(innerBids[j], responseBody.cur, bidRequest); - if (!deepAccess(bid, 'mediaTypes.video.renderer') && deepAccess(bid, 'mediaTypes.video.context') === 'outstream') { - bid.renderer = createRenderer(innerBids[j].bidId); + if (bid.mediaType === VIDEO && isIndexRendererPreferred(bidRequest)) { + const renderUrl = deepAccess(responseBody, 'ext.videoplayerurl'); + bid.renderer = createRenderer(innerBids[j].bidId, renderUrl); + if (!bid.renderer) { + continue; + } } bids.push(bid); diff --git a/modules/ixBidAdapter.md b/modules/ixBidAdapter.md index 59b699bad2d..415fdc9db65 100644 --- a/modules/ixBidAdapter.md +++ b/modules/ixBidAdapter.md @@ -70,10 +70,10 @@ object are detailed here. | siteId | Required | String | An IX-specific identifier that is associated with this ad unit. It will be associated to the single size, if the size is provided. This is similar to a placement ID or an ad unit ID that some other modules have. Examples: `'3723'`, `'6482'`, `'3639'` | size | Optional (Deprecated)| Number[] | The single size associated with the site ID. It should be one of the sizes listed in the ad unit under `adUnits[].sizes` or `adUnits[].mediaTypes.video.playerSize`. Examples: `[300, 250]`, `[300, 600]` | video | Optional | Hash | The video object will serve as the properties of the video ad. You can create any field under the video object that is mentioned in the `OpenRTB Spec v2.5`. Some fields like `mimes, protocols, minduration, maxduration` are required. Properties not defined at this level, will be pulled from the Adunit level. -|video.w| Required | Integer | The video player size width in pixels that will be passed to demand partners. -|video.h| Required | Integer | The video player size height in pixels that will be passed to demand partners. -|video.playerSize| Optional* | Integer | The video player size that will be passed to demand partners. * In the absence of `video.w` and `video.h`, this field is required. -| video.mimes | Required | String[] | Array list of content MIME types supported. Popular MIME types include, but are not limited to, `"video/x-ms- wmv"` for Windows Media and `"video/x-flv"` for Flash Video. +|video.w| Required | Integer | The width of the video player in pixels that will be passed to demand partners.
*If you are using Index’s outstream player and have placed the `video` object at the `bidder` level, this is a required field. You must define the size of the video player using the `video.w` and `video.h` parameters, with a minimum video player size of 300 x 250. +|video.h| Required | Integer | The height of the video player in pixels that will be passed to demand partners.
*If you are using Index’s outstream player and have placed the `video` object at the `bidder` level, this is a required field. You must define the size of the video player using the `video.w` and `video.h` parameters, with a minimum video player size of 300 x 250. +|video.playerSize| Optional* | Array[Integer,Integer] | The video player size that will be passed to demand partners.
*If you are using Index’s outstream player and have placed the `video` object at the `adUnit` level, this is a required field. You must define the size of the video player using this parameter, with a minimum video player size of 300 x 250. +| video.mimes | Required | String[] | If you are using Index’s outstream video player and want to learn more about what is supported, see [List of supported OpenRTB bid request fields for Sellers](https://kb.indexexchange.com/publishers/openrtb_integration/list_of_supported_openrtb_bid_request_fields_for_sellers.htm#Video). |video.minduration| Required | Integer | Minimum video ad duration in seconds. |video.maxduration| Required | Integer | Maximum video ad duration in seconds. |video.protocol / video.protocols| Required | Integer / Integer[] | Either a single protocol provided as an integer, or protocols provided as a list of integers. `2` - VAST 2.0, `3` - VAST 3.0, `5` - VAST 2.0 Wrapper, `6` - VAST 3.0 Wrapper @@ -111,9 +111,7 @@ Both video and banner params will be read from the `adUnits[].mediaTypes.video` The examples in this guide assume the following starting configuration (you may remove banner or video, if either does not apply). -In regards to video, `context` can either be `'instream'` or `'outstream'`. Note that `outstream` requires additional configuration on the adUnit. - - +In regards to video, `context` can either be `'instream'` or `'outstream'`. ```javascript var adUnits = [{ @@ -195,9 +193,9 @@ var adUnits = [{ context: 'instream', playerSize: [300, 250], mimes: [ - 'video/mp4', - 'video/webm' - ], + 'video/mp4', + 'video/webm' + ], minduration: 0, maxduration: 60, protocols: [6] @@ -224,45 +222,47 @@ Please note that you can re-use the existing `siteId` within the same flex position. **Video (Outstream):** -Note that currently, outstream video rendering must be configured by the publisher. In the adUnit, a `renderer` object must be defined, which includes a `url` pointing to the video rendering script, and a `render` function for creating the video player. See http://prebid.org/dev-docs/show-outstream-video-ads.html for more information. + +Publishers have two options to receive outstream video demand from Index: +* Using Index’s outstream video player +* In an outstream video configuration set up by the publisher. For more information, see [Prebid’s documentation on how to show video ads.](https://docs.prebid.org/dev-docs/show-outstream-video-ads.html) + +**Index’s outstream video player** +Publishers who are using Index as a bidding adapter in Prebid.js can show outstream video ads on their site from us by using Index’s outstream video player. This allows a video ad to display inside of a video player and can be placed anywhere on a publisher’s site, such as in-article, in-feed, and more. + +Define a new `video` object for our outstream video player at either the adUnit level or the `bidder` level. If you are setting it at the bidder level, define the size of the video player using the parameters `video.h` and `video.w`. If you are setting it at the `adUnit` level, define the size using video.playerSize. + +For more information on how to structure the `video` object, refer to the following code example: + ```javascript var adUnits = [{ - code: 'video-div-a', + code: 'div-gpt-ad-1571167646410-1', mediaTypes: { video: { + playerSize: [640, 360], context: 'outstream', - playerSize: [300, 250], - mimes: [ - 'video/mp4', - 'video/webm' - ], - minduration: 0, - maxduration: 60, - protocols: [6] - } - }, - renderer: { - url: 'https://test.com/my-video-player.js', - render: function (bid) { - ... + api: [2], + protocols: [2, 3, 5, 6], + minduration: 5, + maxduration: 30, + mimes: ['video/mp4', 'application/javascript'], + placement: 3 } }, bids: [{ bidder: 'ix', params: { - siteId: '12345', - video: { - // If required, use this to override mediaTypes.video.XX properties - } + siteId: '715964' } }] }]; ``` +Please note that your use of the outstream video player will be governed by and subject to the terms and conditions of i) any master services or license agreement entered into by you and Index Exchange; ii) the information provided on our knowledge base linked [here](https://kb.indexexchange.com/publishers/prebid_integration/outstream_video_prebidjs.htm) and [here](https://kb.indexexchange.com/publishers/guidelines/standard_contractual_clauses.htm), and iii) our [Privacy Policy](https://www.indexexchange.com/privacy/). Your use of Index’s outstream video player constitutes your acknowledgement and acceptance of the foregoing. #### Video Caching -Note that the IX adapter expects a client-side Prebid Cache to be enabled for video bidding. +Note that the IX adapter expects a client-side Prebid Cache to be enabled for instream video bidding. ``` pbjs.setConfig({ @@ -293,21 +293,21 @@ pbjs.setConfig({ By default, the IX bidding adapter bids on all banner sizes available in the ad unit when configured to at least one banner size. If you want the IX bidding adapter to only bid on the banner size it’s configured to, switch off this feature using `detectMissingSizes`. ``` pbjs.setConfig({ - ix: { - detectMissingSizes: false - } - }); + ix: { + detectMissingSizes: false + } +}); ``` OR ``` pbjs.setBidderConfig({ - bidders: ["ix"], - config: { - ix: { - detectMissingSizes: false - } - } - }); + bidders: ["ix"], + config: { + ix: { + detectMissingSizes: false + } + } +}); ``` ### 2. Include `ixBidAdapter` in your build process diff --git a/modules/jixieBidAdapter.js b/modules/jixieBidAdapter.js index 119fcdf142b..90ea17395f7 100644 --- a/modules/jixieBidAdapter.js +++ b/modules/jixieBidAdapter.js @@ -1,4 +1,4 @@ -import { logWarn, parseUrl, deepAccess, isArray } from '../src/utils.js'; +import { logWarn, parseUrl, deepAccess, isArray, getDNT } from '../src/utils.js'; import { config } from '../src/config.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; import { getStorageManager } from '../src/storageManager.js'; @@ -6,9 +6,10 @@ 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(); +import {createEidsArray} from './userId/eids.js'; 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'; @@ -59,9 +60,16 @@ function fetchIds_() { return ret; } +// device in the payload had been a simple string ('desktop', 'mobile') +// Now changed to an object. yes the backend is able to handle it. function getDevice_() { - return ((/(ios|ipod|ipad|iphone|android|blackberry|iemobile|opera mini|webos)/i).test(navigator.userAgent) - ? 'mobile' : 'desktop'); + const device = config.getConfig('device') || {}; + device.w = device.w || window.innerWidth; + device.h = device.h || window.innerHeight; + device.ua = device.ua || navigator.userAgent; + device.dnt = getDNT() ? 1 : 0; + device.language = (navigator && navigator.language) ? navigator.language.split('-')[0] : ''; + return device; } function pingTracking_(endpointOverride, qpobj) { @@ -121,6 +129,17 @@ function getMiscDims_() { return ret; } +/* function addUserId(eids, id, source, rti) { + if (id) { + if (rti) { + eids.push({ source, id, rti_partner: rti }); + } else { + eids.push({ source, id }); + } + } + return eids; +} */ + // easier for replacement in the unit test export const internal = { getDevice: getDevice_, @@ -163,7 +182,22 @@ export const spec = { } let ids = fetchIds_(); + let eids = []; let miscDims = internal.getMiscDims(); + + // all available user ids are sent to our backend in the standard array layout: + if (validBidRequests[0].userId) { + let eids1 = createEidsArray(validBidRequests[0].userId); + if (eids1.length) { + eids = eids1; + } + } + // we want to send this blob of info to our backend: + let pg = config.getConfig('priceGranularity'); + if (!pg) { + pg = {}; + } + let transformedParams = Object.assign({}, { auctionid: bidderRequest.auctionId, timeout: bidderRequest.timeout, @@ -174,6 +208,8 @@ export const spec = { pageurl: miscDims.pageurl, mkeywords: miscDims.mkeywords, bids: bids, + eids: eids, + pricegranularity: pg, cfg: jixieCfgBlob }, ids); return Object.assign({}, { diff --git a/modules/jixieBidAdapter.md b/modules/jixieBidAdapter.md index d9c1f19541d..c0a1a965e87 100644 --- a/modules/jixieBidAdapter.md +++ b/modules/jixieBidAdapter.md @@ -7,6 +7,7 @@ Maintainer: contact@jixie.io # Description Module that connects to Jixie demand source to fetch bids. +All prebid-supported user ids are sent to Jixie endpoint, if available. # Test Parameters ``` diff --git a/modules/justIdSystem.js b/modules/justIdSystem.js index d30b9f3073f..15b1c90da4e 100644 --- a/modules/justIdSystem.js +++ b/modules/justIdSystem.js @@ -8,7 +8,7 @@ import * as utils from '../src/utils.js' import { submodule } from '../src/hook.js' import { loadExternalScript } from '../src/adloader.js' -import includes from 'core-js-pure/features/array/includes.js'; +import {includes} from '../src/polyfill.js'; const MODULE_NAME = 'justId'; const EXTERNAL_SCRIPT_MODULE_CODE = 'justtag'; diff --git a/modules/justpremiumBidAdapter.js b/modules/justpremiumBidAdapter.js index 56f9935ea6e..e2ba92d51d9 100644 --- a/modules/justpremiumBidAdapter.js +++ b/modules/justpremiumBidAdapter.js @@ -4,8 +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 pixels = [] +const JP_ADAPTER_VERSION = '1.8.3' export const spec = { code: BIDDER_CODE, @@ -19,6 +18,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 +32,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 @@ -112,8 +113,10 @@ export const spec = { return bidResponses }, - getUserSyncs: function getUserSyncs(syncOptions, responses, gdprConsent, uspConsent) { + getUserSyncs: (syncOptions, serverResponses, gdprConsent, uspConsent) => { let url = 'https://pre.ads.justpremium.com/v/1.0/t/sync' + '?_c=' + 'a' + Math.random().toString(36).substring(7) + Date.now(); + let pixels = [] + if (gdprConsent && (typeof gdprConsent.gdprApplies === 'boolean') && gdprConsent.gdprApplies && gdprConsent.consentString) { url = url + '&consentString=' + encodeURIComponent(gdprConsent.consentString) } @@ -126,6 +129,10 @@ export const spec = { url: url }) } + if (syncOptions.pixelEnabled && serverResponses.length !== 0) { + const pxsFromResponse = serverResponses.map(res => res?.body?.pxs).reduce((acc, cur) => acc.concat(cur), []).filter((obj) => obj !== undefined); + pixels = [...pixels, ...pxsFromResponse]; + } return pixels }, } @@ -253,4 +260,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/justpremiumBidAdapter.md b/modules/justpremiumBidAdapter.md index 45dcb7b7f99..e107cb80958 100644 --- a/modules/justpremiumBidAdapter.md +++ b/modules/justpremiumBidAdapter.md @@ -2,7 +2,6 @@ **Module Name**: Justpremium Bidder Adapter **Module Type**: Bidder Adapter -**Maintainer**: headerbidding-dev@justpremium.com # Description diff --git a/modules/jwplayerRtdProvider.js b/modules/jwplayerRtdProvider.js index 5a7e9f13686..6cccd660854 100644 --- a/modules/jwplayerRtdProvider.js +++ b/modules/jwplayerRtdProvider.js @@ -9,14 +9,15 @@ * @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 JWPLAYER_DOMAIN = SUBMODULE_NAME + '.com'; const segCache = {}; const pendingRequests = {}; let activeRequestCount = 0; @@ -69,7 +70,7 @@ export function fetchTargetingForMediaId(mediaId) { const ajax = ajaxBuilder(); // TODO: Avoid checking undefined vs null by setting a callback to pendingRequests. pendingRequests[mediaId] = null; - ajax(`https://cdn.jwplayer.com/v2/media/${mediaId}`, { + ajax(`https://cdn.${JWPLAYER_DOMAIN}/v2/media/${mediaId}`, { success: function (response) { const segment = parseSegment(response); cacheSegments(segment, mediaId); @@ -155,10 +156,17 @@ export function enrichAdUnits(adUnits) { if (!vat) { return; } - const contentId = getContentId(vat.mediaID); - const contentData = getContentData(vat.segments); + const mediaId = vat.mediaID; + const contentId = getContentId(mediaId); + const contentSegments = getContentSegments(vat.segments); + const contentData = getContentData(mediaId, contentSegments); const targeting = formatTargetingResponse(vat); enrichBids(adUnit.bids, targeting, contentId, contentData); + let ortb2 = config.getConfig('ortb2'); + ortb2 = getOrtbSiteContent(ortb2, contentId, contentData); + if (ortb2) { + config.setConfig({ ortb2 }); + } }; loadVat(jwTargeting, onVatResponse); }); @@ -263,7 +271,7 @@ export function getContentId(mediaID) { return 'jw_' + mediaID; } -export function getContentData(segments) { +export function getContentSegments(segments) { if (!segments || !segments.length) { return; } @@ -276,21 +284,40 @@ export function getContentData(segments) { return convertedSegments; }, []); - return { - name: 'jwplayer', - ext: { - segtax: 502 - }, - segment: formattedSegments + return formattedSegments; +} + +export function getContentData(mediaId, segments) { + if (!mediaId && !segments) { + return; + } + + const contentData = { + name: JWPLAYER_DOMAIN, + ext: {} }; + + if (mediaId) { + contentData.ext.cids = [mediaId]; + } + + if (segments) { + contentData.segment = segments; + contentData.ext.segtax = 502; + } + + return contentData; } -export function addOrtbSiteContent(bid, contentId, contentData) { +export function getOrtbSiteContent(ortb2, contentId, contentData) { if (!contentId && !contentData) { return; } - let ortb2 = bid.ortb2 || {}; + if (!ortb2) { + ortb2 = {}; + } + let site = ortb2.site = ortb2.site || {}; let content = site.content = site.content || {}; @@ -298,12 +325,17 @@ export function addOrtbSiteContent(bid, contentId, contentData) { content.id = contentId; } + const currentData = content.data = content.data || []; + // remove old jwplayer data + const data = currentData.filter(datum => datum.name !== JWPLAYER_DOMAIN); + if (contentData) { - const data = content.data = content.data || []; data.push(contentData); } - bid.ortb2 = ortb2; + content.data = data; + + return ortb2; } function enrichBids(bids, targeting, contentId, contentData) { @@ -313,7 +345,10 @@ function enrichBids(bids, targeting, contentId, contentData) { bids.forEach(bid => { addTargetingToBid(bid, targeting); - addOrtbSiteContent(bid, contentId, contentData); + const ortb2 = getOrtbSiteContent(bid.ortb2, contentId, contentData); + if (ortb2) { + bid.ortb2 = ortb2; + } }); } @@ -334,7 +369,7 @@ export function addTargetingToBid(bid, targeting) { function getPlayer(playerID) { const jwplayer = window.jwplayer; if (!jwplayer) { - logError('jwplayer.js was not found on page'); + logError(SUBMODULE_NAME + '.js was not found on page'); return; } diff --git a/modules/jwplayerRtdProvider.md b/modules/jwplayerRtdProvider.md index 77f65909040..479829196ed 100644 --- a/modules/jwplayerRtdProvider.md +++ b/modules/jwplayerRtdProvider.md @@ -95,9 +95,10 @@ Example: content: { id: 'jw_abc123', data: [{ - name: 'jwplayer', + name: 'jwplayer.com', ext: { - segtax: 502 + segtax: 502, + cids: ['abc123'] }, segment: [{ id: '123' @@ -117,8 +118,9 @@ where: - `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 + - `name` is the `jwplayer.com` string indicating the provider name - `ext.segtax` whose `502` value is the unique identifier for JW Player's proprietary taxonomy + - `ext.cids` is an array containing the list of extended content ids as defined in [oRTB's community extensions](https://github.com/InteractiveAdvertisingBureau/openrtb/blob/master/extensions/community_extensions/extended-content-ids.md#example---content-id-and-seller-defined-context). - `segment` is an array containing the segment taxonomy values as an object where: - `id` is the string representation of the data segment value. diff --git a/modules/kargoBidAdapter.js b/modules/kargoBidAdapter.js index ff56c97e7b7..1842231721b 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, @@ -70,27 +70,42 @@ export const spec = { const bidResponses = []; for (let bidId in bids) { let adUnit = bids[bidId]; - let meta; + let meta = { + mediaType: BANNER + }; + if (adUnit.metadata && adUnit.metadata.landingPageDomain) { - meta = { - clickUrl: adUnit.metadata.landingPageDomain[0], - advertiserDomains: adUnit.metadata.landingPageDomain - }; + meta.clickUrl = adUnit.metadata.landingPageDomain[0]; + meta.advertiserDomains = adUnit.metadata.landingPageDomain; + } + + if (adUnit.mediaType && SUPPORTED_MEDIA_TYPES.includes(adUnit.mediaType)) { + meta.mediaType = adUnit.mediaType; } - bidResponses.push({ + + const bidResponse = { requestId: bidId, cpm: Number(adUnit.cpm), width: adUnit.width, height: adUnit.height, - ad: adUnit.adm, ttl: 300, creativeId: adUnit.id, dealId: adUnit.targetingCustom, netRevenue: true, - currency: bidRequest.currency, + currency: adUnit.currency || bidRequest.currency, + mediaType: meta.mediaType, meta: meta - }); + }; + + if (meta.mediaType == VIDEO) { + bidResponse.vastXml = adUnit.adm; + } else { + bidResponse.ad = adUnit.adm; + } + + bidResponses.push(bidResponse); } + return bidResponses; }, getUserSyncs: function(syncOptions, responses, gdprConsent, usPrivacy) { @@ -172,28 +187,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 +198,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 +228,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/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..8afeaf80652 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'; @@ -68,7 +67,7 @@ export const spec = { var adRequests = bidRequests.map(bidToAdRequest); if (eids) { - ortb2 = mergeDeep(ortb2 || {}, eids); + ortb2 = mergeDeep(mergeDeep({}, ortb2 || {}), eids); } const payload = { diff --git a/modules/lkqdBidAdapter.js b/modules/lkqdBidAdapter.js index 275ab38915d..e58c643f4f0 100644 --- a/modules/lkqdBidAdapter.js +++ b/modules/lkqdBidAdapter.js @@ -36,7 +36,6 @@ export const spec = { 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; @@ -60,8 +59,7 @@ export const spec = { ua: UA, geo: { utcoffset: UTC_OFFSET - }, - ip: IP + } }, user: { ext: {} diff --git a/modules/loglyliftBidAdapter.js b/modules/loglyliftBidAdapter.js index e1319d08766..dd5f0af1cdf 100644 --- a/modules/loglyliftBidAdapter.js +++ b/modules/loglyliftBidAdapter.js @@ -1,13 +1,13 @@ import { config } from '../src/config.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; -import { NATIVE } from '../src/mediaTypes.js'; +import { BANNER, NATIVE } from '../src/mediaTypes.js'; const BIDDER_CODE = 'loglylift'; const ENDPOINT_URL = 'https://bid.logly.co.jp/prebid/client/v1'; export const spec = { code: BIDDER_CODE, - supportedMediaTypes: [NATIVE], + supportedMediaTypes: [BANNER, NATIVE], isBidRequestValid: function (bid) { return !!(bid.params && bid.params.adspotId); @@ -43,7 +43,8 @@ export const spec = { getUserSyncs: function (syncOptions, serverResponses) { const syncs = []; - if (syncOptions.iframeEnabled && serverResponses.length > 0) { + // sync if mediaType is native because not native ad itself has a function for sync + if (syncOptions.iframeEnabled && serverResponses.length > 0 && serverResponses[0].body.bids[0].native) { syncs.push({ type: 'iframe', url: 'https://sync.logly.co.jp/sync/sync.html' diff --git a/modules/loglyliftBidAdapter.md b/modules/loglyliftBidAdapter.md index 9bca238b03e..5505d66957d 100644 --- a/modules/loglyliftBidAdapter.md +++ b/modules/loglyliftBidAdapter.md @@ -12,6 +12,22 @@ Currently module supports only native mediaType. # Test Parameters ``` var adUnits = [ + // Banner adUnit + { + code: 'test-banner-code', + sizes: [[300, 250], [300, 600]], + mediaTypes: { + banner: { + sizes: [[300, 250], [300, 600]] + } + }, + bids: [{ + bidder: 'loglylift', + params: { + adspotId: 1302078 + } + }] + }, // Native adUnit { code: 'test-native-code', 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/malltvAnalyticsAdapter.js b/modules/malltvAnalyticsAdapter.js index 3431681ef2f..a0e2a208bc9 100644 --- a/modules/malltvAnalyticsAdapter.js +++ b/modules/malltvAnalyticsAdapter.js @@ -184,5 +184,5 @@ malltvAnalyticsAdapter.enableAnalytics = function (config) { adapterManager.registerAnalyticsAdapter({ adapter: malltvAnalyticsAdapter, - code: 'malltvAnalytics' + code: 'malltv' }) diff --git a/modules/malltvBidAdapter.js b/modules/malltvBidAdapter.js index 218bd2f1a5e..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, 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 ac22f04e8db..f38f833f4d3 100644 --- a/modules/mass.js +++ b/modules/mass.js @@ -2,8 +2,8 @@ * This module adds MASS support to Prebid.js. */ -import { config } from '../src/config.js'; -import { getHook } from '../src/hook.js'; +import {config} from '../src/config.js'; +import {getHook} from '../src/hook.js'; import {auctionManager} from '../src/auctionManager.js'; const defaultCfg = { 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 6e7ef08ce50..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'; diff --git a/modules/medianetAnalyticsAdapter.js b/modules/medianetAnalyticsAdapter.js index d484ce643a1..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( 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/mediasniperBidAdapter.js b/modules/mediasniperBidAdapter.js new file mode 100644 index 00000000000..3e57503f7fb --- /dev/null +++ b/modules/mediasniperBidAdapter.js @@ -0,0 +1,335 @@ +import { + deepAccess, + deepClone, + deepSetValue, + getWindowTop, + inIframe, + isArray, + isEmpty, + isFn, + isNumber, + isStr, + logWarn, + logError, + logMessage, + parseUrl, + getBidIdParameter, + triggerPixel, +} from '../src/utils.js'; + +import { registerBidder } from '../src/adapters/bidderFactory.js'; +import { BANNER } from '../src/mediaTypes.js'; + +const BIDDER_CODE = 'mediasniper'; +const DEFAULT_BID_TTL = 360; +const DEFAULT_CURRENCY = 'RUB'; +const DEFAULT_NET_REVENUE = true; +const ENDPOINT = 'https://sapi.bumlam.com/prebid/'; + +export const spec = { + code: BIDDER_CODE, + supportedMediaTypes: [BANNER], + + isBidRequestValid: function (bid) { + logMessage('Hello!! bid: ', JSON.stringify(bid)); + + if (!bid || isEmpty(bid)) { + return false; + } + + if (!bid.params || isEmpty(bid.params)) { + return false; + } + + if (!isStr(bid.params.placementId) && !isNumber(bid.params.placementId)) { + return false; + } + + const banner = deepAccess(bid, 'mediaTypes.banner', {}); + if (!banner || isEmpty(banner)) { + return false; + } + + const sizes = deepAccess(bid, 'mediaTypes.banner.sizes', []); + if (!isArray(sizes) || isEmpty(sizes)) { + return false; + } + + return true; + }, + + buildRequests: function (validBidRequests, bidderRequest) { + const payload = createOrtbTemplate(); + + deepSetValue(payload, 'id', bidderRequest.auctionId); + + validBidRequests.forEach((validBid) => { + let bid = deepClone(validBid); + + const imp = createImp(bid); + payload.imp.push(imp); + }); + + // params + const siteId = getBidIdParameter('siteid', validBidRequests[0].params) + ''; + deepSetValue(payload, 'site.id', siteId); + + // Assign payload.site from refererinfo + if (bidderRequest.refererInfo) { + if (bidderRequest.refererInfo.reachedTop) { + const sitePage = bidderRequest.refererInfo.referer; + deepSetValue(payload, 'site.page', sitePage); + deepSetValue( + payload, + 'site.domain', + parseUrl(sitePage, { + noDecodeWholeURL: true, + }).hostname + ); + + if (canAccessTopWindow()) { + deepSetValue(payload, 'site.ref', getWindowTop().document.referrer); + } + } + } + + const request = { + method: 'POST', + url: ENDPOINT, + data: JSON.stringify(payload), + }; + + return request; + }, + + interpretResponse(serverResponse, bidRequest) { + const bidResponses = []; + + try { + if ( + serverResponse.body && + serverResponse.body.seatbid && + isArray(serverResponse.body.seatbid) + ) { + serverResponse.body.seatbid.forEach((bidderSeat) => { + if (!isArray(bidderSeat.bid) || !bidderSeat.bid.length) { + return; + } + + bidderSeat.bid.forEach((bid) => { + const newBid = { + requestId: bid.impid, + bidderCode: spec.code, + cpm: bid.price || 0, + width: bid.w, + height: bid.h, + creativeId: bid.crid || bid.adid || bid.id, + dealId: bid.dealid || null, + currency: serverResponse.body.cur || DEFAULT_CURRENCY, + netRevenue: DEFAULT_NET_REVENUE, + ttl: DEFAULT_BID_TTL, // seconds. https://docs.prebid.org/dev-docs/faq.html#does-prebidjs-cache-bids + ad: bid.adm, + mediaType: BANNER, + burl: bid.nurl, + meta: { + advertiserDomains: + Array.isArray(bid.adomain) && bid.adomain.length + ? bid.adomain + : [], + mediaType: BANNER, + }, + }; + + logMessage('answer: ', JSON.stringify(newBid)); + + bidResponses.push(newBid); + }); + }); + } + } catch (e) { + logError(BIDDER_CODE, e); + } + + return bidResponses; + }, + + onBidWon: function (bid) { + if (!bid.burl) { + return; + } + + const url = bid.burl.replace(/\$\{AUCTION_PRICE\}/, bid.cpm); + + triggerPixel(url); + }, +}; +registerBidder(spec); + +/** + * Detects the capability to reach window.top. + * + * @returns {boolean} + */ +function canAccessTopWindow() { + try { + return !!getWindowTop().location.href; + } catch (error) { + return false; + } +} + +/** + * Returns an openRTB 2.5 object. + * This one will be populated at each step of the buildRequest process. + * + * @returns {object} + */ +function createOrtbTemplate() { + return { + id: '', + cur: [DEFAULT_CURRENCY], + imp: [], + site: {}, + device: { + ip: '', + js: 1, + ua: navigator.userAgent, + }, + user: {}, + }; +} + +/** + * Create the OpenRTB 2.5 imp object. + * + * @param {*} bid Prebid bid object from request + * @returns + */ +function createImp(bid) { + let placementId = ''; + if (isStr(bid.params.placementId)) { + placementId = bid.params.placementId; + } else if (isNumber(bid.params.placementId)) { + placementId = bid.params.placementId.toString(); + } + + const imp = { + id: bid.bidId, + tagid: placementId, + bidfloorcur: DEFAULT_CURRENCY, + secure: 1, + }; + + // There is no default floor. bidfloor is set only + // if the priceFloors module is activated and returns a valid floor. + const floor = getMinFloor(bid); + if (isNumber(floor)) { + imp.bidfloor = floor; + } + + // Only supports proper mediaTypes definition… + for (let mediaType in bid.mediaTypes) { + switch (mediaType) { + case BANNER: + imp.banner = createBannerImp(bid); + break; + } + } + + // dealid + const dealId = getBidIdParameter('dealid', bid.params); + if (dealId) { + imp.pmp = { + private_auction: 1, + deals: [ + { + id: dealId, + bidfloor: floor || 0, + bidfloorcur: DEFAULT_CURRENCY, + }, + ], + }; + } + + return imp; +} + +/** + * Returns floor from priceFloors module or MediaKey default value. + * + * @param {*} bid a Prebid.js bid (request) object + * @param {string} mediaType the mediaType or the wildcard '*' + * @param {string|array} size the size array or the wildcard '*' + * @returns {number|boolean} + */ +function getFloor(bid, mediaType, size = '*') { + if (!isFn(bid.getFloor)) { + return false; + } + + if (spec.supportedMediaTypes.indexOf(mediaType) === -1) { + logWarn( + `${BIDDER_CODE}: Unable to detect floor price for unsupported mediaType ${mediaType}. No floor will be used.` + ); + return false; + } + + const floor = bid.getFloor({ + currency: DEFAULT_CURRENCY, + mediaType, + size, + }); + + return !isNaN(floor.floor) && floor.currency === DEFAULT_CURRENCY + ? floor.floor + : false; +} + +function getMinFloor(bid) { + const floors = []; + + for (let mediaType in bid.mediaTypes) { + const floor = getFloor(bid, mediaType); + + if (isNumber(floor)) { + floors.push(floor); + } + } + + if (!floors.length) { + return false; + } + + return floors.reduce((a, b) => { + return Math.min(a, b); + }); +} + +/** + * Returns an openRtb 2.5 banner object. + * + * @param {object} bid Prebid bid object from request + * @returns {object} + */ +function createBannerImp(bid) { + let sizes = bid.mediaTypes.banner.sizes; + const params = deepAccess(bid, 'params', {}); + + const banner = {}; + + banner.w = parseInt(sizes[0][0], 10); + banner.h = parseInt(sizes[0][1], 10); + + const format = []; + sizes.forEach(function (size) { + if (size.length && size.length > 1) { + format.push({ w: size[0], h: size[1] }); + } + }); + banner.format = format; + + banner.topframe = inIframe() ? 0 : 1; + banner.pos = params.pos || 0; + + return banner; +} diff --git a/modules/mediasniperBidAdapter.md b/modules/mediasniperBidAdapter.md new file mode 100644 index 00000000000..e47513c7fb2 --- /dev/null +++ b/modules/mediasniperBidAdapter.md @@ -0,0 +1,31 @@ +# Overview + +``` +Module Name: Mediasniper Bid Adapter +Module Type: Bidder Adapter +Maintainer: oleg@rtbtech.org +``` + +# Description + +Connects to Mediasniper demand source to fetch bids. + +# Test Parameters + +``` +var adUnits = [ +{ + code: 'test', + mediaTypes: { + banner: { + sizes: [[300, 250], [300, 600]], + } + }, + bids: [{ + bidder: 'mediasniper', + params: { + placementId: "123456" + } + }] +}, +``` diff --git a/modules/mediasquareBidAdapter.js b/modules/mediasquareBidAdapter.js index 0b6f763988f..427a16f1341 100644 --- a/modules/mediasquareBidAdapter.js +++ b/modules/mediasquareBidAdapter.js @@ -55,7 +55,8 @@ export const spec = { }); 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) { @@ -116,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'; @@ -153,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..604a5dd7ea8 --- /dev/null +++ b/modules/minutemediaBidAdapter.js @@ -0,0 +1,431 @@ +import { logWarn, logInfo, isArray, isFn, deepAccess, isEmpty, contains, timestamp, getBidIdParameter, triggerPixel, isInteger } 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 SUPPORTED_AD_TYPES = [BANNER, VIDEO]; +const BIDDER_CODE = 'minutemedia'; +const ADAPTER_VERSION = '6.0.0'; +const TTL = 360; +const CURRENCY = 'USD'; +const SELLER_ENDPOINT = 'https://hb.minutemedia-prebid.com/'; +const MODES = { + PRODUCTION: 'hb-mm-multi', + TEST: 'hb-multi-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 (validBidRequests, bidderRequest) { + const combinedRequestsObject = {}; + + // use data from the first bid, to create the general params for all bids + const generalObject = validBidRequests[0]; + const testMode = generalObject.params.testMode; + + combinedRequestsObject.params = generateGeneralParams(generalObject, bidderRequest); + combinedRequestsObject.bids = generateBidsParams(validBidRequests, bidderRequest); + + return { + method: 'POST', + url: getEndpoint(testMode), + data: combinedRequestsObject + } + }, + interpretResponse: function ({body}) { + const bidResponses = []; + + 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); + }); + } + + return bidResponses; + }, + getUserSyncs: function (syncOptions, serverResponses) { + const syncs = []; + for (const response of serverResponses) { + if (syncOptions.iframeEnabled && response.body.params.userSyncURL) { + syncs.push({ + type: 'iframe', + url: response.body.params.userSyncURL + }); + } + if (syncOptions.pixelEnabled && isArray(response.body.params.userSyncPixels)) { + const pixels = response.body.params.userSyncPixels.map(pixel => { + return { + type: 'image', + url: pixel + } + }) + syncs.push(...pixels) + } + } + return syncs; + }, + onBidWon: function (bid) { + if (bid == null) { + return; + } + + logInfo('onBidWon:', bid); + if (bid.hasOwnProperty('nurl') && bid.nurl.length > 0) { + triggerPixel(bid.nurl); + } + } +}; + +registerBidder(spec); + +/** + * Get floor price + * @param bid {bid} + * @returns {Number} + */ +function getFloor(bid, mediaType) { + if (!isFn(bid.getFloor)) { + return 0; + } + let floorResult = bid.getFloor({ + currency: CURRENCY, + mediaType: mediaType, + size: '*' + }); + return floorResult.currency === CURRENCY && floorResult.floor ? floorResult.floor : 0; +} + +/** + * Get the the ad sizes array from the bid + * @param bid {bid} + * @returns {Array} + */ +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) { + sizesArray = bid.sizes; + } + + return sizesArray; +} + +/** + * 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'; +} + +function generateBidsParams(validBidRequests, bidderRequest) { + const bidsArray = []; + + if (validBidRequests.length) { + validBidRequests.forEach(bid => { + bidsArray.push(generateBidParameters(bid, bidderRequest)); + }); + } + + return bidsArray; +} + +/** + * Generate bid specific parameters + * @param {bid} bid + * @param {bidderRequest} bidderRequest + * @returns {Object} bid specific params object + */ +function generateBidParameters(bid, bidderRequest) { + const {params} = bid; + 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 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(), + 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), + ua: navigator.userAgent, + session_id: getBidIdParameter('auctionId', generalObject), + tmax: timeout + } + + const userIdsParam = getBidIdParameter('userId', generalObject); + if (userIdsParam) { + generalParams.userIds = JSON.stringify(userIdsParam); + } + + const ortb2Metadata = config.getConfig('ortb2') || {}; + if (ortb2Metadata.site) { + generalParams.site_metadata = JSON.stringify(ortb2Metadata.site); + } + if (ortb2Metadata.user) { + generalParams.user_metadata = JSON.stringify(ortb2Metadata.user); + } + + if (syncEnabled) { + const allowedSyncMethod = getAllowedSyncMethod(filterSettings, bidderCode); + if (allowedSyncMethod) { + generalParams.cs_method = allowedSyncMethod; + } + } + + if (bidderRequest.uspConsent) { + generalParams.us_privacy = bidderRequest.uspConsent; + } + + if (bidderRequest && bidderRequest.gdprConsent && bidderRequest.gdprConsent.gdprApplies) { + generalParams.gdpr = bidderRequest.gdprConsent.gdprApplies; + generalParams.gdpr_consent = bidderRequest.gdprConsent.consentString; + } + + if (generalBidParams.ifa) { + generalParams.ifa = generalBidParams.ifa; + } + + if (generalObject.schain) { + generalParams.schain = getSupplyChain(generalObject.schain); + } + + if (bidderRequest && bidderRequest.refererInfo) { + generalParams.referrer = deepAccess(bidderRequest, 'refererInfo.referer'); + generalParams.page_url = config.getConfig('pageUrl') || deepAccess(window, 'location.href'); + } + + return generalParams +} diff --git a/modules/minutemediaBidAdapter.md b/modules/minutemediaBidAdapter.md new file mode 100644 index 00000000000..70f106a745f --- /dev/null +++ b/modules/minutemediaBidAdapter.md @@ -0,0 +1,76 @@ +#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) & Banner. + +# 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: 'video-test', // Optional + testMode: false // Optional + } + }] + }, + { + code: 'dfp-banner-div', + sizes: [ + [640, 480] + ], + mediaTypes: { + banner: { + sizes: [ + [640, 480] + ] + } + }, + bids: [{ + bidder: 'minutemedia', + params: { + org: '56f91cd4d3e3660002000033', // Required + floorPrice: 2.00, // Optional + placementId: 'banner-test', // 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/mobfoxpbBidAdapter.js b/modules/mobfoxpbBidAdapter.js new file mode 100644 index 00000000000..a4af7133370 --- /dev/null +++ b/modules/mobfoxpbBidAdapter.js @@ -0,0 +1,135 @@ +import { isFn, deepAccess, getWindowTop } from '../src/utils.js'; +import { registerBidder } from '../src/adapters/bidderFactory.js'; +import { BANNER, NATIVE, VIDEO } from '../src/mediaTypes.js'; + +const BIDDER_CODE = 'mobfoxpb'; +const AD_URL = 'https://bes.mobfox.com/pbjs'; + +function isBidResponseValid(bid) { + if (!bid.requestId || !bid.cpm || !bid.creativeId || + !bid.ttl || !bid.currency) { + return false; + } + switch (bid.mediaType) { + case BANNER: + return Boolean(bid.width && bid.height && bid.ad); + case VIDEO: + return Boolean(bid.vastUrl); + case NATIVE: + return Boolean(bid.native && bid.native.impressionTrackers); + default: + return false; + } +} + +function getBidFloor(bid) { + if (!isFn(bid.getFloor)) { + return deepAccess(bid, 'params.bidfloor', 0); + } + + try { + const bidFloor = bid.getFloor({ + currency: 'USD', + mediaType: '*', + size: '*', + }); + return bidFloor.floor; + } catch (_) { + return 0 + } +} + +export const spec = { + code: BIDDER_CODE, + supportedMediaTypes: [BANNER, VIDEO, NATIVE], + + isBidRequestValid: (bid) => { + return Boolean(bid.bidId && bid.params && bid.params.placementId); + }, + + buildRequests: (validBidRequests = [], bidderRequest) => { + const winTop = getWindowTop(); + const location = winTop.location; + const placements = []; + const request = { + 'deviceWidth': winTop.screen.width, + 'deviceHeight': winTop.screen.height, + 'language': (navigator && navigator.language) ? navigator.language.split('-')[0] : '', + 'secure': 1, + 'host': location.host, + 'page': location.pathname, + 'placements': placements + }; + + if (bidderRequest) { + if (bidderRequest.uspConsent) { + request.ccpa = bidderRequest.uspConsent; + } + if (bidderRequest.gdprConsent) { + request.gdpr = bidderRequest.gdprConsent + } + } + + const len = validBidRequests.length; + for (let i = 0; i < len; i++) { + const bid = validBidRequests[i]; + const placement = { + placementId: bid.params.placementId, + bidId: bid.bidId, + schain: bid.schain || {}, + bidfloor: getBidFloor(bid) + }; + const mediaType = bid.mediaTypes + + if (mediaType && mediaType[BANNER] && mediaType[BANNER].sizes) { + placement.traffic = BANNER; + placement.sizes = mediaType[BANNER].sizes; + } else if (mediaType && mediaType[VIDEO] && mediaType[VIDEO].playerSize) { + placement.traffic = VIDEO; + placement.wPlayer = mediaType[VIDEO].playerSize[0]; + placement.hPlayer = mediaType[VIDEO].playerSize[1]; + placement.playerSize = mediaType[VIDEO].playerSize; + placement.minduration = mediaType[VIDEO].minduration; + placement.maxduration = mediaType[VIDEO].maxduration; + placement.mimes = mediaType[VIDEO].mimes; + placement.protocols = mediaType[VIDEO].protocols; + placement.startdelay = mediaType[VIDEO].startdelay; + placement.placement = mediaType[VIDEO].placement; + placement.skip = mediaType[VIDEO].skip; + placement.skipafter = mediaType[VIDEO].skipafter; + placement.minbitrate = mediaType[VIDEO].minbitrate; + placement.maxbitrate = mediaType[VIDEO].maxbitrate; + placement.delivery = mediaType[VIDEO].delivery; + placement.playbackmethod = mediaType[VIDEO].playbackmethod; + placement.api = mediaType[VIDEO].api; + placement.linearity = mediaType[VIDEO].linearity; + } else if (mediaType && mediaType[NATIVE]) { + placement.traffic = NATIVE; + placement.native = mediaType[NATIVE]; + } + placements.push(placement); + } + + return { + method: 'POST', + url: AD_URL, + data: request + }; + }, + + interpretResponse: (serverResponse) => { + let response = []; + for (let i = 0; i < serverResponse.body.length; i++) { + let resItem = serverResponse.body[i]; + if (isBidResponseValid(resItem)) { + resItem.meta = resItem.meta || {}; + resItem.meta.advertiserDomains = resItem.adomain || []; + + response.push(resItem); + } + } + return response; + }, +}; + +registerBidder(spec); diff --git a/modules/mobfoxpbBidAdapter.md b/modules/mobfoxpbBidAdapter.md index 6eb549919d7..f434b2792a9 100644 --- a/modules/mobfoxpbBidAdapter.md +++ b/modules/mobfoxpbBidAdapter.md @@ -24,7 +24,7 @@ Module that connects to mobfox demand sources { bidder: 'mobfoxpb', params: { - placementId: 0 + placementId: 'testBanner' } } ] @@ -41,7 +41,7 @@ Module that connects to mobfox demand sources { bidder: 'mobfoxpb', params: { - placementId: 0 + placementId: 'testVideo' } } ] @@ -63,7 +63,7 @@ Module that connects to mobfox demand sources { bidder: 'mobfoxpb', params: { - placementId: 0 + placementId: 'testNative' } } ] 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/my6senseBidAdapter.js b/modules/my6senseBidAdapter.js index 018baa37461..b4fc1049304 100644 --- a/modules/my6senseBidAdapter.js +++ b/modules/my6senseBidAdapter.js @@ -1,6 +1,6 @@ import { BANNER, NATIVE } from '../src/mediaTypes.js'; +import {registerBidder} from '../src/adapters/bidderFactory.js'; -const {registerBidder} = require('../src/adapters/bidderFactory.js'); const BIDDER_CODE = 'my6sense'; const END_POINT = 'https://hb.mynativeplatform.com/pub2/web/v1.15.0/hbwidget.json'; const END_POINT_METHOD = 'POST'; 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 85537d382c2..91508d38ca0 100644 --- a/modules/nextMillenniumBidAdapter.js +++ b/modules/nextMillenniumBidAdapter.js @@ -1,9 +1,10 @@ -import { isStr, _each, getBidIdParameter } from '../src/utils.js'; +import { isStr, _each, parseUrl, getWindowTop, getBidIdParameter } from '../src/utils.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; import { BANNER } from '../src/mediaTypes.js'; const BIDDER_CODE = 'nextMillennium'; const ENDPOINT = 'https://pbs.nextmillmedia.com/openrtb2/auction'; +const TEST_ENDPOINT = 'https://test.pbs.nextmillmedia.com/openrtb2/auction'; const SYNC_ENDPOINT = 'https://statics.nextmillmedia.com/load-cookie.html?v=4'; const TIME_TO_LIVE = 360; @@ -13,7 +14,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 +29,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 +48,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 } @@ -58,9 +62,12 @@ export const spec = { } } + const urlParameters = parseUrl(getWindowTop().location.href).search; + const isTest = urlParameters['pbs'] && urlParameters['pbs'] === 'test'; + requests.push({ method: 'POST', - url: ENDPOINT, + url: isTest ? TEST_ENDPOINT : ENDPOINT, data: JSON.stringify(postBody), options: { contentType: 'application/json', @@ -91,6 +98,7 @@ export const spec = { meta: { advertiserDomains: bid.adomain || [] }, + ad: bid.adm }); }); @@ -125,4 +133,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 index a59bb635875..814a2f55299 100644 --- a/modules/nexx360BidAdapter.js +++ b/modules/nexx360BidAdapter.js @@ -1,17 +1,21 @@ import {ajax} from '../src/ajax.js'; import {config} from '../src/config.js'; +import { transformBidderParamKeywords } from '../src/utils.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; -import {BANNER} from '../src/mediaTypes.js'; +import {BANNER, VIDEO} 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' +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'; + +const GVLID = 965; export const spec = { code: BIDDER_CODE, + gvlid: GVLID, aliases: ['revenuemaker'], // short code - supportedMediaTypes: [BANNER], + supportedMediaTypes: [BANNER, VIDEO], /** * Determines whether or not the given bid request is valid. * @@ -19,6 +23,9 @@ export const spec = { * @return boolean True if this is a valid bid, and false otherwise. */ isBidRequestValid: function(bid) { + if (!!bid.params.bidfloorCurrency && !['EUR', 'USD'].includes(bid.params.bidfloorCurrency)) return false; + if (!!bid.params.bidfloor && typeof bid.params.bidfloor !== 'number') return false; + if (!!bid.params.keywords && typeof bid.params.keywords !== 'object') return false; return !!(bid.params.account && bid.params.tagId); }, /** @@ -34,15 +41,20 @@ export const spec = { let userEids = null; Object.keys(validBidRequests).forEach(key => { adunitValue = validBidRequests[key]; - adUnits.push({ + const foo = { account: adunitValue.params.account, tagId: adunitValue.params.tagId, + videoExt: adunitValue.params.videoExt, label: adunitValue.adUnitCode, bidId: adunitValue.bidId, auctionId: adunitValue.auctionId, transactionId: adunitValue.transactionId, - mediatypes: adunitValue.mediaTypes - }); + mediatypes: adunitValue.mediaTypes, + bidfloor: adunitValue.params.bidfloor || 0, + bidfloorCurrency: adunitValue.params.bidfloorCurrency || 'USD', + keywords: adunitValue.params.keywords ? transformBidderParamKeywords(adunitValue.params.keywords) : [], + } + adUnits.push(foo); if (adunitValue.userIdAsEids) userEids = adunitValue.userIdAsEids; }); const payload = { @@ -77,20 +89,19 @@ export const spec = { */ 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]; + const url = `${CACHE_URL}?uuid=${value['uuid']}`; 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, @@ -105,6 +116,21 @@ export const spec = { } */ }; + if (value.type === 'banner') bidResponse.adUrl = url; + if (value.type === 'video') { + const params = { + type: 'prebid', + mediatype: 'video', + ssp: value.bidder, + tag_id: value.tagId, + consent: value.consent, + price: value.cpm, + }; + bidResponse.cpm = value.cpm; + bidResponse.mediaType = 'video'; + bidResponse.vastUrl = url; + bidResponse.vastImpUrl = `${METRICS_TRACKER_URL}?${new URLSearchParams(params).toString()}`; + } bidResponses.push(bidResponse); }); } @@ -133,7 +159,7 @@ export const spec = { */ onBidWon: function(bid) { // fires a pixel to confirm a winning bid - const params = { type: 'prebid' }; + const params = { type: 'prebid', mediatype: 'banner' }; 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; diff --git a/modules/nexx360BidAdapter.md b/modules/nexx360BidAdapter.md index 882d83cb24e..532d48418b6 100644 --- a/modules/nexx360BidAdapter.md +++ b/modules/nexx360BidAdapter.md @@ -10,9 +10,13 @@ Maintainer: gabriel@nexx360.io Connects to Nexx360 network for bids. -Nexx360 bid adapter supports Banner only for the time being. +To use us as a bidder you must have an account and an active "tagId" on our Nexx360 platform. # Test Parameters + +## Web + +### Display ``` var adUnits = [ // Banner adUnit @@ -33,3 +37,23 @@ var adUnits = [ }, ]; ``` + +### Video Instream +``` + var videoAdUnit = { + code: 'video1', + mediaTypes: { + video: { + playerSize: [640, 480], + context: 'instream' + } + }, + 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 a2210090c0f..ae9cc4c818f 100644 --- a/modules/novatiqIdSystem.js +++ b/modules/novatiqIdSystem.js @@ -5,10 +5,10 @@ * @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'; +import { getStorageManager } from '../src/storageManager.js'; /** @type {Submodule} */ export const novatiqIdSubmodule = { @@ -40,61 +40,169 @@ export const novatiqIdSubmodule = { * @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); - let partnerhost; - partnerhost = window.location.hostname; - logInfo('NOVATIQ partner hostname: ' + partnerhost); - - const novatiqId = snowflakeId(); + return this.sendSyncRequest(useCallbacks, sharedId, srcId, urlParams); + }, - let url = 'https://spadsync.com/sync?sptoken=' + novatiqId + '&sspid=' + srcId + '&ssphost=' + partnerhost; + sendSyncRequest(useCallbacks, sharedId, sspid, urlParams) { + const syncUrl = this.getSyncUrl(sharedId, sspid, urlParams); + const url = syncUrl.url; + const novatiqId = syncUrl.novatiqId; // for testing - let sharedStatus = 'Not Found'; + 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 } + } + }, + + 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'); + + ajax(url, undefined, undefined, { method: 'GET', withCredentials: false }); + + logInfo('NOVATIQ snowflake: ' + novatiqId); + }, + + 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; - sharedStatus = 'Found'; } - ajax(url, undefined, undefined, { method: 'GET', withCredentials: false }); + return { + url: url, + novatiqId: novatiqId + } + }, - logInfo('NOVATIQ snowflake: ' + novatiqId); - return { 'id': novatiqId, - 'sharedStatus': sharedStatus } + 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; }, 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 = '_pubcid'; - - // Has the cookieOrStorageID been redefined? - if (typeof configParams.sharedIdName != 'undefined' && configParams.sharedIdName != null && configParams.sharedIdName != '') { - cookieOrStorageID = configParams.sharedIdName; - logInfo('NOVATIQ sharedID name redefined: ' + cookieOrStorageID); - } - - const storage = getStorageManager('', 'pubCommonId'); + let cookieOrStorageID = this.getCookieOrStorageID(configParams); + const storage = getStorageManager({moduleName: 'pubCommonId'}); // first check local storage if (storage.hasLocalStorage()) { @@ -114,14 +222,14 @@ export const novatiqIdSubmodule = { return sharedId; }, - getSrcId(configParams) { - logInfo('NOVATIQ Configured sourceid param: ' + configParams.sourceid); - - function isHex(str) { - var a = parseInt(str, 16); - return (a.toString(16) === str) + 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'; @@ -129,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 a6489594d19..f33fc700311 100644 --- a/modules/novatiqIdSystem.md +++ b/modules/novatiqIdSystem.md @@ -27,14 +27,22 @@ pbjs.setConfig({ auctionDelay: 50 } }); -``` +``` ### 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. @@ -86,13 +94,4 @@ pbjs.setConfig({ }); ``` -### 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 | 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"` | - 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 7d2989b2066..295a0042f4b 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.10'; +const ADAPTER_VERSION = '1.2.11'; function isBidRequestValid(bid) { const adUnitSizes = getAdUnitSizes(bid); @@ -74,11 +74,12 @@ function buildRequests(validBidRequests, bidderRequest) { if (bidRequest.mediaTypes && bidRequest.mediaTypes.hasOwnProperty('banner')) { openRtbBidRequestBanner.site.id = bidRequest.params.assetKey; + const floor = getFloor(bidRequest); openRtbBidRequestBanner.imp.push({ id: bidRequest.bidId, tagid: bidRequest.params.adUnitId, - bidfloor: getFloor(bidRequest), + ...(floor && {bidfloor: floor}), banner: { format: sizes }, 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/otmBidAdapter.js b/modules/otmBidAdapter.js index a0e91a480a2..e81bdfa9e6a 100644 --- a/modules/otmBidAdapter.js +++ b/modules/otmBidAdapter.js @@ -1,10 +1,21 @@ import { registerBidder } from '../src/adapters/bidderFactory.js'; -import {logInfo, logError, getBidIdParameter, _each, getValue, isFn, isPlainObject} from '../src/utils.js'; +import { + logInfo, + logError, + getBidIdParameter, + _each, + getValue, + isFn, + isPlainObject, + isArray, + isStr, + isNumber, +} from '../src/utils.js'; import { BANNER } from '../src/mediaTypes.js'; const BIDDER_CODE = 'otm'; const OTM_BID_URL = 'https://ssp.otm-r.com/adjson'; -const DEF_CUR = 'RUB' +const DEFAULT_CURRENCY = 'RUB' export const spec = { @@ -19,7 +30,7 @@ export const spec = { * @return boolean True if this is a valid bid, and false otherwise. */ isBidRequestValid: function (bid) { - return !!bid.params.tid; + return Boolean(bid.params.tid); }, /** @@ -33,43 +44,41 @@ export const spec = { logInfo('validBidRequests', validBidRequests); const bidRequests = []; - let tz = new Date().getTimezoneOffset() - let referrer = ''; - if (bidderRequest && bidderRequest.refererInfo) { - referrer = bidderRequest.refererInfo.referer; - } + const tz = new Date().getTimezoneOffset() + const referrer = bidderRequest && bidderRequest.refererInfo ? bidderRequest.refererInfo.referer : ''; _each(validBidRequests, (bid) => { - let domain = getValue(bid.params, 'domain') || '' - let tid = getValue(bid.params, 'tid') - let cur = getValue(bid.params, 'currency') || DEF_CUR - let bidid = getBidIdParameter('bidId', bid) - let transactionid = getBidIdParameter('transactionId', bid) - let auctionid = getBidIdParameter('auctionId', bid) - let bidfloor = _getBidFloor(bid) + let topOrigin = '' + try { + if (isStr(referrer)) topOrigin = new URL(referrer).host + } catch (e) { /* do nothing */ } + const domain = isStr(bid.params.domain) ? bid.params.domain : topOrigin + const cur = getValue(bid.params, 'currency') || DEFAULT_CURRENCY + const bidid = getBidIdParameter('bidId', bid) + const transactionid = getBidIdParameter('transactionId', bid) + const auctionid = getBidIdParameter('auctionId', bid) + const bidfloor = _getBidFloor(bid) _each(bid.sizes, size => { - let width = 0; - let height = 0; - if (size.length && typeof size[0] === 'number' && typeof size[1] === 'number') { - width = size[0]; - height = size[1]; - } + const hasSizes = isArray(size) && isNumber(size[0]) && isNumber(size[1]) + const width = hasSizes ? size[0] : 0; + const height = hasSizes ? size[1] : 0; + bidRequests.push({ method: 'GET', url: OTM_BID_URL, data: { - tz: tz, + tz, w: width, h: height, - domain: domain, + domain, l: referrer, - s: tid, - cur: cur, - bidid: bidid, - transactionid: transactionid, - auctionid: auctionid, - bidfloor: bidfloor, + s: bid.params.tid, + cur, + bidid, + transactionid, + auctionid, + bidfloor, }, }) }) @@ -81,10 +90,9 @@ export const spec = { * Generate response. * * @param serverResponse - * @param request * @returns {[]|*[]} */ - interpretResponse: function (serverResponse, request) { + interpretResponse: function (serverResponse) { logInfo('serverResponse', serverResponse.body); const responsesBody = serverResponse ? serverResponse.body : {}; @@ -102,7 +110,7 @@ export const spec = { width: bid.w, height: bid.h, creativeId: bid.creativeid, - currency: bid.currency || 'RUB', + currency: bid.currency || DEFAULT_CURRENCY, netRevenue: true, ad: bid.ad, ttl: bid.ttl, @@ -132,12 +140,12 @@ function _getBidFloor(bid) { return bid.params.bidfloor ? bid.params.bidfloor : 0; } - let floor = bid.getFloor({ - currency: DEF_CUR, + const floor = bid.getFloor({ + currency: DEFAULT_CURRENCY, mediaType: '*', size: '*' }); - if (isPlainObject(floor) && !isNaN(floor.floor) && floor.currency === DEF_CUR) { + if (isPlainObject(floor) && !isNaN(floor.floor) && floor.currency === DEFAULT_CURRENCY) { return floor.floor; } return 0; 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..c4674132416 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 @@ -69,11 +70,12 @@ export function setBidderRtb (auctionDetails, customModuleConfig) { const moduleConfig = getModuleConfig(customModuleConfig) const acBidders = deepAccess(moduleConfig, 'params.acBidders') const maxSegs = deepAccess(moduleConfig, 'params.maxSegs') + const transformationConfigs = deepAccess(moduleConfig, 'params.transformations') || [] const segmentData = getSegments(maxSegs) acBidders.forEach(function (bidder) { const currConfig = bidderConfig[bidder] || {} - const nextConfig = mergeOrtbConfig(currConfig, segmentData) + const nextConfig = updateOrtbConfig(currConfig, segmentData.ac, transformationConfigs) // ORTB2 uses the `ac` segment IDs config.setBidderConfig({ bidders: [bidder], @@ -83,23 +85,33 @@ export function setBidderRtb (auctionDetails, customModuleConfig) { } /** - * Merges segments into existing bidder config + * Updates `user.data` object in existing bidder config with Permutive segments * @param {Object} currConfig - Current bidder config - * @param {Object} segmentData - Segment data + * @param {Object[]} transformationConfigs - array of objects with `id` and `config` properties, used to determine + * the transformations on user data to include the ORTB2 object + * @param {string[]} segmentIDs - Permutive segment IDs * @return {Object} Merged ortb2 object */ -function mergeOrtbConfig (currConfig, segmentData) { - const segment = segmentData.ac.map(seg => { - return { id: seg } - }) +function updateOrtbConfig (currConfig, segmentIDs, transformationConfigs) { const name = 'permutive.com' + + const permutiveUserData = { + name, + segment: segmentIDs.map(segmentId => ({ id: segmentId })), + } + + const transformedUserData = transformationConfigs + .filter(({ id }) => ortb2UserDataTransformations.hasOwnProperty(id)) + .map(({ id, config }) => ortb2UserDataTransformations[id](permutiveUserData, config)) + const ortbConfig = mergeDeep({}, currConfig) - const currSegments = deepAccess(ortbConfig, 'ortb2.user.data') || [] - const userSegment = currSegments + const currentUserData = deepAccess(ortbConfig, 'ortb2.user.data') || [] + + const updatedUserData = currentUserData .filter(el => el.name !== name) - .concat({ name, segment }) + .concat(permutiveUserData, transformedUserData) - deepSetValue(ortbConfig, 'ortb2.user.data', userSegment) + deepSetValue(ortbConfig, 'ortb2.user.data', updatedUserData) return ortbConfig } @@ -235,11 +247,11 @@ export function getSegments (maxSegs) { ac: [..._pcrprs, ..._ppam, ...legacySegs], rubicon: readSegments('_prubicons'), appnexus: readSegments('_papns'), - gam: readSegments('_pdfps') + gam: readSegments('_pdfps'), } - for (const type in segments) { - segments[type] = segments[type].slice(0, maxSegs) + for (const bidder in segments) { + segments[bidder] = segments[bidder].slice(0, maxSegs) } return segments @@ -259,6 +271,34 @@ function readSegments (key) { } } +const unknownIabSegmentId = '_unknown_' + +/** + * Functions to apply to ORT2B2 `user.data` objects. + * Each function should return an a new object containing a `name`, (optional) `ext` and `segment` + * properties. The result of the each transformation defined here will be appended to the array + * under `user.data` in the bid request. + */ +const ortb2UserDataTransformations = { + iab: (userData, config) => ({ + name: userData.name, + ext: { segtax: config.segtax }, + segment: (userData.segment || []) + .map(segment => ({ id: iabSegmentId(segment.id, config.iabIds) })) + .filter(segment => segment.id !== unknownIabSegmentId) + }) +} + +/** + * Transform a Permutive segment ID into an IAB audience taxonomy ID. + * @param {string} permutiveSegmentId + * @param {Object} iabIds object of mappings between Permutive and IAB segment IDs (key: permutive ID, value: IAB ID) + * @return {string} IAB audience taxonomy ID associated with the Permutive segment ID + */ +function iabSegmentId(permutiveSegmentId, iabIds) { + return iabIds[permutiveSegmentId] || unknownIabSegmentId +} + /** @type {RtdSubmodule} */ export const permutiveSubmodule = { name: MODULE_NAME, diff --git a/modules/permutiveRtdProvider.md b/modules/permutiveRtdProvider.md index 0acd42405d1..5fa6e14a474 100644 --- a/modules/permutiveRtdProvider.md +++ b/modules/permutiveRtdProvider.md @@ -1,8 +1,11 @@ # Permutive Real-time Data Submodule + This submodule reads cohorts from Permutive and attaches them as targeting keys to bid requests. Using this module will deliver best targeting results, leveraging Permutive's real-time segmentation and modelling capabilities. ## Usage + Compile the Permutive RTD module into your Prebid build: + ``` gulp build --modules=rtdModule,permutiveRtdProvider ``` @@ -29,25 +32,38 @@ pbjs.setConfig({ ``` ## Supported Bidders + The Permutive RTD module sets Audience Connector cohorts as bidder-specific `ortb2.user.data` first-party data, following the Prebid `ortb2` convention, for any bidder included in `acBidders`. The module also supports bidder-specific data locations per ad unit (custom parameters) for the below bidders: -| Bidder | ID | Custom Cohorts | Audience Connector | -| ----------- | ---------- | -------------------- | ------------------ | -| Xandr | `appnexus` | Yes | Yes | -| Magnite | `rubicon` | Yes | No | -| Ozone | `ozone` | No | Yes | +| Bidder | ID | Custom Cohorts | Audience Connector | +| ------- | ---------- | -------------- | ------------------ | +| Xandr | `appnexus` | Yes | Yes | +| Magnite | `rubicon` | Yes | No | +| Ozone | `ozone` | No | Yes | Key-values details for custom parameters: -* **Custom Cohorts:** When enabling the respective Activation for a cohort in Permutive, this module will automatically attach that cohort ID to the bid request. There is no need to enable individual bidders in the module configuration, it will automatically reflect which SSP integrations you have enabled in your Permutive dashboard. Permutive cohorts will be sent in the `permutive` key-value. -* **Audience Connector:** You'll need to define which bidders should receive Audience Connector cohorts. You need to include the `ID` of any bidder in the `acBidders` array. Audience Connector cohorts will be sent in the `p_standard` key-value. +- **Custom Cohorts:** When enabling the respective Activation for a cohort in Permutive, this module will automatically attach that cohort ID to the bid request. There is no need to enable individual bidders in the module configuration, it will automatically reflect which SSP integrations you have enabled in your Permutive dashboard. Permutive cohorts will be sent in the `permutive` key-value. +- **Audience Connector:** You'll need to define which bidders should receive Audience Connector cohorts. You need to include the `ID` of any bidder in the `acBidders` array. Audience Connector cohorts will be sent in the `p_standard` key-value. ## Parameters -| Name | Type | Description | Default | -| ----------------- | -------------------- | ------------------ | ------------------ | -| name | String | This should always be `permutive` | - | -| waitForIt | Boolean | Should be `true` if there's an `auctionDelay` defined (optional) | `false` | -| params | Object | | - | -| params.acBidders | String[] | An array of bidders which should receive AC cohorts. | `[]` | -| params.maxSegs | Integer | Maximum number of cohorts to be included in either the `permutive` or `p_standard` key-value. | `500` | + +| Name | Type | Description | Default | +| ---------------------- | -------- | --------------------------------------------------------------------------------------------- | ------- | +| name | String | This should always be `permutive` | - | +| waitForIt | Boolean | Should be `true` if there's an `auctionDelay` defined (optional) | `false` | +| params | Object | | - | +| params.acBidders | String[] | An array of bidders which should receive AC cohorts. | `[]` | +| params.maxSegs | Integer | Maximum number of cohorts to be included in either the `permutive` or `p_standard` key-value. | `500` | +| params.transformations | Object[] | An array of configurations for ORTB2 user data transformations | | + +### The `transformations` parameter + +This array contains configurations for transformations we'll apply to the Permutive object in the ORTB2 `user.data` array. The results of these transformations will be appended to the `user.data` array that's attached to ORTB2 bid requests. + +#### Supported transformations + +| Name | ID | Config structure | Description | +| -------------- | --- | ------------------------------------------------- | ------------------------------------------------------------------------------------ | +| IAB taxonomies | iab | { segtax: number, iabIds: Object} | Transform segment IDs from Permutive to IAB (note: alpha version, subject to change) | 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 a4c8f6aae59..0ffb16d23a4 100644 --- a/modules/prebidServerBidAdapter/index.js +++ b/modules/prebidServerBidAdapter/index.js @@ -1,22 +1,44 @@ 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, timestamp + 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 { 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'; +import {getGlobal} from '../../src/prebidGlobal.js'; const getConfig = config.getConfig; @@ -78,6 +100,7 @@ let eidPermissions; * @type {S2SDefaultConfig} */ const s2sDefaultConfig = { + bidders: Object.freeze([]), timeout: 1000, syncTimeout: 1000, maxBids: 1, @@ -122,7 +145,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; @@ -517,6 +540,14 @@ Object.assign(ORTB2.prototype, { // 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' @@ -532,13 +563,16 @@ Object.assign(ORTB2.prototype, { const nativeParams = adUnit.nativeParams; let nativeAssets; if (nativeParams) { + let idCounter = -1; try { nativeAssets = nativeAssetCache[impressionId] = Object.keys(nativeParams).reduce((assets, type) => { let params = nativeParams[type]; function newAsset(obj) { + idCounter++; return Object.assign({ - required: params.required ? 1 : 0 + required: params.required ? 1 : 0, + id: (isNumber(params.id)) ? idCounter = params.id : idCounter }, obj ? cleanObj(obj) : {}); } @@ -677,6 +711,7 @@ Object.assign(ORTB2.prototype, { // 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); @@ -685,7 +720,7 @@ Object.assign(ORTB2.prototype, { 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 => { @@ -716,7 +751,7 @@ Object.assign(ORTB2.prototype, { } }); - 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)); @@ -724,21 +759,64 @@ Object.assign(ORTB2.prototype, { deepSetValue(imp, 'ext.prebid.storedauctionresponse.id', storedAuctionResponseBid.storedAuctionResponse.toString()); } - const getFloorBid = find(firstBidRequest.bids, bid => bid.adUnitCode === adUnit.code && typeof bid.getFloor === 'function'); + const floor = (() => { + // we have to pick a floor for the imp - here we attempt to find the minimum floor + // across all bids for this adUnit + + const convertCurrency = typeof getGlobal().convertCurrency !== 'function' + ? (amount) => amount + : (amount, from, to) => { + if (from === to) return amount; + let result = null; + try { + result = getGlobal().convertCurrency(amount, from, to); + } catch (e) { + } + return result; + } + const s2sCurrency = config.getConfig('currency.adServerCurrency') || DEFAULT_S2S_CURRENCY; + + return adUnit.bids + .map((bid) => this.getBidRequest(imp.id, bid.bidder)) + .map((bid) => { + if (!bid || typeof bid.getFloor !== 'function') return; + try { + const {currency, floor} = bid.getFloor({ + currency: s2sCurrency + }); + return { + currency, + floor: parseFloat(floor) + } + } catch (e) { + logError('PBS: getFloor threw an error: ', e); + } + }) + .reduce((min, floor) => { + // if any bid does not have a valid floor, do not attempt to send any to PBS + if (floor == null || floor.currency == null || floor.floor == null || isNaN(floor.floor)) { + min.min = null; + } + if (min.min === null) { + return min; + } + // otherwise, pick the minimum one (or, in some strange confluence of circumstances, the one in the best currency) + if (min.ref == null) { + min.ref = min.min = floor; + } else { + const value = convertCurrency(floor.floor, floor.currency, min.ref.currency); + if (value != null && value < min.ref.floor) { + min.ref.floor = value; + min.min = floor; + } + } + return min; + }, {}).min + })(); - if (getFloorBid) { - let floorInfo; - try { - floorInfo = getFloorBid.getFloor({ - currency: config.getConfig('currency.adServerCurrency') || DEFAULT_S2S_CURRENCY, - }); - } catch (e) { - logError('PBS: getFloor threw an error: ', e); - } - if (floorInfo && floorInfo.currency && !isNaN(parseFloat(floorInfo.floor))) { - imp.bidfloor = parseFloat(floorInfo.floor); - imp.bidfloorcur = floorInfo.currency - } + if (floor) { + imp.bidfloor = floor.floor; + imp.bidfloorcur = floor.currency } if (imp.banner || imp.video || imp.native) { @@ -771,6 +849,11 @@ Object.assign(ORTB2.prototype, { } }; + // If the price floors module is active, then we need to signal to PBS! If floorData obj is present is best way to check + if (typeof deepAccess(firstBidRequest, 'bids.0.floorData') === 'object') { + request.ext.prebid.floors = { enabled: false }; + } + // This is no longer overwritten unless name and version explicitly overwritten by extPrebid (mergeDeep) request.ext.prebid = Object.assign(request.ext.prebid, {channel: {name: 'pbjs', version: $$PREBID_GLOBAL$$.version}}) @@ -885,10 +968,15 @@ Object.assign(ORTB2.prototype, { // a seatbid object contains a `bid` array and a `seat` string response.seatbid.forEach(seatbid => { (seatbid.bid || []).forEach(bid => { - const bidRequest = this.getBidRequest(bid.impid, seatbid.seat); - if (bidRequest == null && !s2sConfig.allowUnknownBidderCodes) { - logWarn(`PBS adapter received bid from unknown bidder (${seatbid.seat}), but 's2sConfig.allowUnknownBidderCodes' is not set. Ignoring bid.`); - return; + 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; @@ -1155,18 +1243,13 @@ export const processPBSRequest = hook('sync', function (s2sBidRequest, bidReques 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 + const requestedBidders = adUnits .map(adUnit => adUnit.bids.map(bid => bid.bidder).filter(uniques)) - .reduce(flatten) + .reduce(flatten, []) .filter(uniques); - const ortb2 = new ORTB2(s2sBidRequest, bidRequests, validAdUnits, requestedBidders); + const ortb2 = new ORTB2(s2sBidRequest, bidRequests, adUnits, requestedBidders); const request = ortb2.buildRequest(); const requestJson = request && JSON.stringify(request); logInfo('BidRequest: ' + requestJson); diff --git a/modules/prebidmanagerAnalyticsAdapter.js b/modules/prebidmanagerAnalyticsAdapter.js index a1a0a636e3c..6235b10fa13 100644 --- a/modules/prebidmanagerAnalyticsAdapter.js +++ b/modules/prebidmanagerAnalyticsAdapter.js @@ -3,16 +3,16 @@ import {ajaxBuilder} from '../src/ajax.js'; import adapter from '../src/AnalyticsAdapter.js'; import adapterManager from '../src/adapterManager.js'; import { getStorageManager } from '../src/storageManager.js'; +import CONSTANTS from '../src/constants.json'; /** * 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: '; -var CONSTANTS = require('../src/constants.json'); let ajax = ajaxBuilder(0); var _VERSION = 1; diff --git a/modules/priceFloors.js b/modules/priceFloors.js index 5bca25671c0..e548de768b4 100644 --- a/modules/priceFloors.js +++ b/modules/priceFloors.js @@ -1,15 +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 {auctionManager} from '../src/auctionManager.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. @@ -123,10 +138,14 @@ 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 }; + // use adUnit floorMin as priority! + if (typeof deepAccess(bidObject, 'ortb2Imp.ext.prebid.floorMin') === 'number') { + matchingData.floorMin = bidObject.ortb2Imp.ext.prebid.floorMin; + } matchingData.matchingFloor = Math.max(matchingData.floorMin, matchingData.floorRuleValue); // save for later lookup if needed deepSetValue(floorData, `matchingInputs.${matchingInput}`, {...matchingData}); 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 93afe96af78..b18e1b73604 100644 --- a/modules/pubmaticBidAdapter.js +++ b/modules/pubmaticBidAdapter.js @@ -3,6 +3,7 @@ import { registerBidder } from '../src/adapters/bidderFactory.js'; import { BANNER, VIDEO, NATIVE } from '../src/mediaTypes.js'; import {config} from '../src/config.js'; import { Renderer } from '../src/Renderer.js'; +import { bidderSettings } from '../src/bidderSettings.js'; const BIDDER_CODE = 'pubmatic'; const LOG_WARN_PREFIX = 'PubMatic: '; @@ -11,7 +12,6 @@ const USER_SYNC_URL_IFRAME = 'https://ads.pubmatic.com/AdServer/js/user_sync.htm 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 UNDEFINED = undefined; const DEFAULT_WIDTH = 0; const DEFAULT_HEIGHT = 0; @@ -976,6 +976,25 @@ function _blockedIabCategoriesValidation(payload, blockedIabCategories) { } } +function _allowedIabCategoriesValidation(payload, allowedIabCategories) { + allowedIabCategories = allowedIabCategories + .filter(function(category) { + if (typeof category === 'string') { // returns only strings + return true; + } else { + logWarn(LOG_WARN_PREFIX + 'acat: Each category should be a string, ignoring category: ' + category); + return false; + } + }) + .map(category => category.trim()) // trim all categories + .filter((category, index, arr) => arr.indexOf(category) === index); // return unique values only + + if (allowedIabCategories.length > 0) { + logWarn(LOG_WARN_PREFIX + 'acat: Selected: ', allowedIabCategories); + payload.ext.acat = allowedIabCategories; + } +} + function _assignRenderer(newBid, request) { let bidParams, context, adUnitCode; if (request.bidderRequest && request.bidderRequest.bids) { @@ -1006,7 +1025,6 @@ export const spec = { code: BIDDER_CODE, gvlid: 76, supportedMediaTypes: [BANNER, VIDEO, NATIVE], - aliases: [GROUPM_ALIAS], /** * Determines whether or not the given bid request is valid. Valid bid request must have placementId and hbid * @@ -1075,6 +1093,7 @@ export const spec = { var dctrArr = []; var bid; var blockedIabCategories = []; + var allowedIabCategories = []; validBidRequests.forEach(originalBid => { bid = deepClone(originalBid); @@ -1106,6 +1125,9 @@ export const spec = { if (bid.params.hasOwnProperty('bcat') && isArray(bid.params.bcat)) { blockedIabCategories = blockedIabCategories.concat(bid.params.bcat); } + if (bid.params.hasOwnProperty('acat') && isArray(bid.params.acat)) { + allowedIabCategories = allowedIabCategories.concat(bid.params.acat); + } var impObj = _createImpressionObject(bid, conf); if (impObj) { payload.imp.push(impObj); @@ -1126,6 +1148,10 @@ export const spec = { payload.ext.wrapper.wv = $$REPO_AND_VERSION$$; payload.ext.wrapper.transactionId = conf.transactionId; payload.ext.wrapper.wp = 'pbjs'; + if (bidderRequest && bidderRequest.bidderCode) { + payload.ext.allowAlternateBidderCodes = bidderSettings.get(bidderRequest.bidderCode, 'allowAlternateBidderCodes'); + payload.ext.allowedAlternateBidderCodes = bidderSettings.get(bidderRequest.bidderCode, 'allowedAlternateBidderCodes'); + } payload.user.gender = (conf.gender ? conf.gender.trim() : UNDEFINED); payload.user.geo = {}; payload.user.geo.lat = _parseSlotParam('lat', conf.lat); @@ -1175,7 +1201,7 @@ export const spec = { } _handleEids(payload, validBidRequests); - _blockedIabCategoriesValidation(payload, blockedIabCategories); + _handleFlocId(payload, validBidRequests); // First Party Data const commonFpd = config.getConfig('ortb2') || {}; @@ -1185,7 +1211,16 @@ export const spec = { if (commonFpd.user) { mergeDeep(payload, {user: commonFpd.user}); } - + if (commonFpd.bcat) { + blockedIabCategories = blockedIabCategories.concat(commonFpd.bcat) + } + if (commonFpd.ext?.prebid?.bidderparams?.[bidderRequest.bidderCode]?.acat) { + const acatParams = commonFpd.ext.prebid.bidderparams[bidderRequest.bidderCode].acat; + _allowedIabCategoriesValidation(payload, acatParams); + } else if (allowedIabCategories.length) { + _allowedIabCategoriesValidation(payload, allowedIabCategories); + } + _blockedIabCategoriesValidation(payload, blockedIabCategories); // Note: Do not move this block up // if site object is set in Prebid config then we need to copy required fields from site into app and unset the site object if (typeof config.getConfig('app') === 'object') { @@ -1287,6 +1322,12 @@ 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) { + newBid.bidderCode = bid.ext.marketplace; + } + bidResponses.push(newBid); }); }); diff --git a/modules/pubnxBidAdapter.md b/modules/pubnxBidAdapter.md deleted file mode 100644 index 6c843322402..00000000000 --- a/modules/pubnxBidAdapter.md +++ /dev/null @@ -1,31 +0,0 @@ -# Overview - -``` -Module Name: PubNX Bidder Adapter -Module Type: Bidder Adapter -Maintainer: prebid-team@pubnx.com -``` - -# Description - -Connects to PubNX exchange for bids. -PubNX Bidder adapter supports Banner ads. -Use bidder code ```pubnx``` for all PubNX traffic. - -# Test Parameters -``` -var adUnits = [ - // Banner adUnit - { - code: 'banner-div', - sizes: [[300, 250], [300,600]], // a display size(s) - bids: [{ - bidder: 'pubnx', - params: { - placementId: 'PNX-HB-G396432V4809F3' - } - }] - }, -]; -``` - 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/pulsepointBidAdapter.js b/modules/pulsepointBidAdapter.js index 7aa3ad6088c..c0280e944ae 100644 --- a/modules/pulsepointBidAdapter.js +++ b/modules/pulsepointBidAdapter.js @@ -419,61 +419,14 @@ function user(bidRequest, bidderRequest) { } } if (bidRequest) { - if (bidRequest.userId) { - ext.eids = []; - addExternalUserId(ext.eids, bidRequest.userId.pubcid, 'pubcid.org'); - addExternalUserId(ext.eids, bidRequest.userId.britepoolid, 'britepool.com'); - addExternalUserId(ext.eids, bidRequest.userId.criteoId, 'criteo.com'); - addExternalUserId(ext.eids, bidRequest.userId.idl_env, 'liveramp.com'); - addExternalUserId(ext.eids, deepAccess(bidRequest, 'userId.id5id.uid'), 'id5-sync.com', deepAccess(bidRequest, 'userId.id5id.ext')); - addExternalUserId(ext.eids, deepAccess(bidRequest, 'userId.parrableId.eid'), 'parrable.com'); - addExternalUserId(ext.eids, bidRequest.userId.fabrickId, 'neustar.biz'); - addExternalUserId(ext.eids, deepAccess(bidRequest, 'userId.haloId.haloId'), 'audigent.com'); - addExternalUserId(ext.eids, bidRequest.userId.merkleId, 'merkleinc.com'); - addExternalUserId(ext.eids, bidRequest.userId.lotamePanoramaId, 'crwdcntrl.net'); - addExternalUserId(ext.eids, bidRequest.userId.connectid, 'verizonmedia.com'); - addExternalUserId(ext.eids, deepAccess(bidRequest, 'userId.uid2.id'), 'uidapi.com'); - // liveintent - if (bidRequest.userId.lipb && bidRequest.userId.lipb.lipbid) { - addExternalUserId(ext.eids, bidRequest.userId.lipb.lipbid, 'liveintent.com'); - } - // TTD - addExternalUserId(ext.eids, bidRequest.userId.tdid, 'adserver.org', { - rtiPartner: 'TDID' - }); - // digitrust - const digitrustResponse = bidRequest.userId.digitrustid; - if (digitrustResponse && digitrustResponse.data) { - var digitrust = {}; - if (digitrustResponse.data.id) { - digitrust.id = digitrustResponse.data.id; - } - if (digitrustResponse.data.keyv) { - digitrust.keyv = digitrustResponse.data.keyv; - } - ext.digitrust = digitrust; - } + let eids = bidRequest.userIdAsEids; + if (eids) { + ext.eids = eids; } } return { ext }; } -/** - * Produces external userid object in ortb 3.0 model. - */ -function addExternalUserId(eids, id, source, uidExt) { - if (id) { - var uid = { id }; - if (uidExt) { - uid.ext = uidExt; - } - eids.push({ - source, - uids: [ uid ] - }); - } -} - /** * Produces the regulations ortb object */ 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/riseBidAdapter.js b/modules/riseBidAdapter.js index 9aca2491386..a8ea023d46a 100644 --- a/modules/riseBidAdapter.js +++ b/modules/riseBidAdapter.js @@ -1,17 +1,17 @@ -import { logWarn, logInfo, isArray, isFn, deepAccess, isEmpty, contains, timestamp, getBidIdParameter, triggerPixel } 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', @@ -36,55 +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}) { 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, - nurl: body.nurl, - 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) { 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 @@ -104,8 +119,8 @@ export const spec = { if (bid.hasOwnProperty('nurl') && bid.nurl.length > 0) { triggerPixel(bid.nurl); } - }, -} + } +}; registerBidder(spec); @@ -114,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; } /** @@ -250,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 a96abed9252..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']; diff --git a/modules/rtdModule/index.js b/modules/rtdModule/index.js index 6c88d66999a..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 */ /** @@ -153,10 +154,10 @@ import {config} from '../../src/config.js'; import {module} from '../../src/hook.js'; import {logError, logInfo, logWarn} from '../../src/utils.js'; -import events from '../../src/events.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} */ @@ -339,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 { @@ -353,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 ed24598e986..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 = []; @@ -321,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, @@ -331,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'; } @@ -359,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); } } @@ -449,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 {}; @@ -459,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); } } @@ -491,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 @@ -508,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 @@ -520,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(), @@ -539,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') { @@ -547,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; @@ -557,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) { @@ -572,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 }) { @@ -586,8 +676,8 @@ let rubiconAdapter = Object.assign({}, baseAdapter, { cacheEntry.bids = {}; cacheEntry.bidsWon = {}; cacheEntry.gamHasRendered = {}; + cacheEntry.referrer = pageReferer = deepAccess(args, 'bidderRequests.0.refererInfo.referer'); cacheEntry.bidderOrder = []; - cacheEntry.referrer = deepAccess(args, 'bidderRequests.0.refererInfo.referer'); const floorData = deepAccess(args, 'bidderRequests.0.bids.0.floorData'); if (floorData) { cacheEntry.floorData = { ...floorData }; @@ -691,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; @@ -716,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); @@ -787,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 @@ -814,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 9f3ba28a27c..48c5ddf813a 100644 --- a/modules/rubiconBidAdapter.js +++ b/modules/rubiconBidAdapter.js @@ -1,10 +1,24 @@ -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'; @@ -239,6 +253,12 @@ export const spec = { if (!isNaN(bidFloor)) { data.imp[0].bidfloor = bidFloor; } + + // If the price floors module is active, then we need to signal to PBS! If floorData obj is present is best way to check + if (typeof bidRequest.floorData === 'object') { + data.ext.prebid.floors = { enabled: false }; + } + // if value is set, will overwrite with same value data.imp[0].ext[bidRequest.bidder].video.size_id = determineRubiconVideoSizeId(bidRequest) @@ -393,6 +413,7 @@ export const spec = { .concat([ 'tk_flint', 'x_source.tid', + 'l_pb_bid_id', 'x_source.pchain', 'p_screen_res', 'rp_floor', @@ -466,6 +487,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, 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 b7aec0f8881..a071cdfea80 100755 --- a/modules/seedingAllianceBidAdapter.js +++ b/modules/seedingAllianceBidAdapter.js @@ -8,7 +8,7 @@ import { config } from '../src/config.js'; const BIDDER_CODE = 'seedingAlliance'; const DEFAULT_CUR = 'EUR'; -const ENDPOINT_URL = 'https://b.nativendo.de/cds/rtb/bid?format=openrtb2.5&ssp=nativendo'; +const ENDPOINT_URL = 'https://b.nativendo.de/cds/rtb/bid?format=openrtb2.5&ssp=pb'; const NATIVE_ASSET_IDS = {0: 'title', 1: 'body', 2: 'sponsoredBy', 3: 'image', 4: 'cta', 5: 'icon'}; @@ -124,7 +124,8 @@ export const spec = { user: {}, regs: { ext: { - gdpr: 0 + gdpr: 0, + pb_ver: '$prebid.version$' } } }; @@ -187,20 +188,23 @@ registerBidder(spec); function parseNative(bid) { const {assets, link, imptrackers} = bid.adm.native; + let clickUrl = link.url.replace(/\$\{AUCTION_PRICE\}/g, bid.price); + if (link.clicktrackers) { link.clicktrackers.forEach(function (clicktracker, index) { - link.clicktrackers[index] = clicktracker.replace(/\$\{AUCTION_PRICE\}/, bid.price); + link.clicktrackers[index] = clicktracker.replace(/\$\{AUCTION_PRICE\}/g, bid.price); }); } + if (imptrackers) { imptrackers.forEach(function (imptracker, index) { - imptrackers[index] = imptracker.replace(/\$\{AUCTION_PRICE\}/, bid.price); + imptrackers[index] = imptracker.replace(/\$\{AUCTION_PRICE\}/g, bid.price); }); } const result = { - url: link.url, - clickUrl: link.url, + url: clickUrl, + clickUrl: clickUrl, clickTrackers: link.clicktrackers || undefined, impressionTrackers: imptrackers || undefined }; 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..84943450e3b 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'; @@ -38,12 +38,13 @@ export function getSegmentsAndCategories(reqBidsConfigObj, onDone, moduleConfig, gdprApplies = null; tcString = ''; } else if (getGlobal().getConfig('consentManagement.gdpr')) { - // Default endpoint is cookieless if gdpr management is set. Needed because the cookie-based endpoint will fail and return error if user is located in Europe and no consent has been given + // Default endpoint is cookieless if gdpr management is set. Needed because the cookie-based endpoint will fail and return error if user is located in Europe and no consent has been given sirdataDomain = 'cookieless-data.com'; sendWithCredentials = false; } // default global endpoint is cookie-based if no rules falls into cookieless or consent has been given or GDPR doesn't apply + if (!sirdataDomain || !gdprApplies || (deepAccess(userConsent, 'gdpr.vendorData.vendor.consents') && userConsent.gdpr.vendorData.vendor.consents[53] && userConsent.gdpr.vendorData.purpose.consents[1] && userConsent.gdpr.vendorData.purpose.consents[4])) { sirdataDomain = 'sddan.com'; sendWithCredentials = true; @@ -51,38 +52,40 @@ export function getSegmentsAndCategories(reqBidsConfigObj, onDone, moduleConfig, var actualUrl = moduleConfig.params.actualUrl || getRefererInfo().referer; - const url = 'https://kvt.' + sirdataDomain + '/api/v1/public/p/' + moduleConfig.params.partnerId + '/d/' + moduleConfig.params.key + '/s?callback=&gdpr=' + gdprApplies + '&gdpr_consent=' + tcString + (actualUrl ? '&url=' + actualUrl : ''); - ajax(url, { - success: function (response, req) { - if (req.status === 200) { - try { - const data = JSON.parse(response); - if (data && data.segments) { - addSegmentData(adUnits, data, moduleConfig, onDone); - } else { + const url = 'https://kvt.' + sirdataDomain + '/api/v1/public/p/' + moduleConfig.params.partnerId + '/d/' + moduleConfig.params.key + '/s?callback=&gdpr=' + gdprApplies + '&gdpr_consent=' + tcString + (actualUrl ? '&url=' + encodeURIComponent(actualUrl) : ''); + + ajax(url, + { + success: function (response, req) { + if (req.status === 200) { + try { + const data = JSON.parse(response); + if (data && data.segments) { + addSegmentData(adUnits, data, moduleConfig, onDone); + } else { + onDone(); + } + } catch (e) { onDone(); + logError('unable to parse Sirdata data' + e); } - } catch (e) { + } else if (req.status === 204) { onDone(); - logError('unable to parse Sirdata data' + e); } - } else if (req.status === 204) { + }, + error: function () { onDone(); + logError('unable to get Sirdata data'); } }, - error: function () { - onDone(); - logError('unable to get Sirdata data'); - } - }, - null, - { - contentType: 'text/plain', - method: 'GET', - withCredentials: sendWithCredentials, - referrerPolicy: 'unsafe-url', - crossOrigin: true - }); + null, + { + contentType: 'text/plain', + method: 'GET', + withCredentials: sendWithCredentials, + referrerPolicy: 'unsafe-url', + crossOrigin: true + }); } export function setGlobalOrtb2(segments, categories) { @@ -118,7 +121,7 @@ export function setBidderOrtb2(bidder, segments, categories) { } if (!isEmpty(addOrtb2)) { let ortb2 = {ortb2: mergeDeep({}, testBidder, addOrtb2)}; - getGlobal().setBidderConfig({ bidders: [bidder], config: ortb2 }); + getGlobal().setBidderConfig({bidders: [bidder], config: ortb2}); } } catch (e) { logError(e) @@ -127,12 +130,14 @@ export function setBidderOrtb2(bidder, segments, categories) { return true; } -export function loadCustomFunction (todo, adUnit, list, data, bid) { +export function loadCustomFunction(todo, adUnit, list, data, bid) { try { if (typeof todo == 'function') { todo(adUnit, list, data, bid); } - } catch (e) { logError(e); } + } catch (e) { + logError(e); + } return true; } @@ -142,20 +147,28 @@ export function getSegAndCatsArray(data, minScore) { try { if (data && data.contextual_categories) { for (let catId in data.contextual_categories) { - let value = data.contextual_categories[catId]; - if (value >= minScore && sirdataData.categories.indexOf(catId) === -1) { - sirdataData.categories.push(catId.toString()); + if (data.contextual_categories.hasOwnProperty(catId)) { + let value = data.contextual_categories[catId]; + if (value >= minScore && sirdataData.categories.indexOf(catId) === -1) { + sirdataData.categories.push(catId.toString()); + } } } } - } catch (e) { logError(e); } + } catch (e) { + logError(e); + } try { if (data && data.segments) { for (let segId in data.segments) { - sirdataData.segments.push(data.segments[segId].toString()); + if (data.segments.hasOwnProperty(segId)) { + sirdataData.segments.push(data.segments[segId].toString()); + } } } - } catch (e) { logError(e); } + } catch (e) { + logError(e); + } return sirdataData; } @@ -165,9 +178,8 @@ export function addSegmentData(adUnits, data, moduleConfig, onDone) { const globalMinScore = moduleConfig.params.hasOwnProperty('contextualMinRelevancyScore') ? moduleConfig.params.contextualMinRelevancyScore : 30; var sirdataData = getSegAndCatsArray(data, globalMinScore); - if (!sirdataData || (sirdataData.segments.length < 1 && sirdataData.categories.length < 1)) { logError('no cats'); onDone(); return adUnits; } - const sirdataList = sirdataData.segments.concat(sirdataData.categories); + var sirdataMergedList = []; var curationData = {'segments': [], 'categories': []}; var curationId = '1'; @@ -186,12 +198,15 @@ export function addSegmentData(adUnits, data, moduleConfig, onDone) { if (data.shared_taxonomy && data.shared_taxonomy[curationId]) { curationData = getSegAndCatsArray(data.shared_taxonomy[curationId], globalMinScore); } - window.googletag.pubads().getSlots().forEach(function(n) { - if (typeof n.setTargeting !== 'undefined') { - n.setTargeting('sd_rtd', sirdataList.concat(curationData.segments).concat(curationData.categories)); + sirdataMergedList = sirdataList.concat(curationData.segments).concat(curationData.categories); + window.googletag.pubads().getSlots().forEach(function (n) { + if (typeof n.setTargeting !== 'undefined' && sirdataMergedList && sirdataMergedList.length > 0) { + n.setTargeting('sd_rtd', sirdataMergedList); } }) - } catch (e) { logError(e); } + } catch (e) { + logError(e); + } } // Bid targeting level for FPD non-generic biders @@ -204,10 +219,14 @@ export function addSegmentData(adUnits, data, moduleConfig, onDone) { } adUnit.hasOwnProperty('bids') && adUnit.bids.forEach(bid => { - bidderIndex = (moduleConfig.params.hasOwnProperty('bidders') ? findIndex(moduleConfig.params.bidders, function(i) { return i.bidder === bid.bidder; }) : false); + bidderIndex = (moduleConfig.params.hasOwnProperty('bidders') ? findIndex(moduleConfig.params.bidders, function (i) { + return i.bidder === bid.bidder; + }) : false); indexFound = (!!(typeof bidderIndex == 'number' && bidderIndex >= 0)); try { curationData = {'segments': [], 'categories': []}; + sirdataMergedList = []; + let minScore = (indexFound && moduleConfig.params.bidders[bidderIndex].hasOwnProperty('contextualMinRelevancyScore') ? moduleConfig.params.bidders[bidderIndex].contextualMinRelevancyScore : globalMinScore) if (!biddersParamsExist || (indexFound && (!moduleConfig.params.bidders[bidderIndex].hasOwnProperty('adUnitCodes') || moduleConfig.params.bidders[bidderIndex].adUnitCodes.indexOf(adUnit.code) !== -1))) { @@ -233,10 +252,13 @@ export function addSegmentData(adUnits, data, moduleConfig, onDone) { if (data.shared_taxonomy && data.shared_taxonomy[curationId]) { curationData = getSegAndCatsArray(data.shared_taxonomy[curationId], minScore); } - if (indexFound && moduleConfig.params.bidders[bidderIndex].hasOwnProperty('customFunction')) { - loadCustomFunction(moduleConfig.params.bidders[bidderIndex].customFunction, adUnit, sirdataList.concat(curationData.segments).concat(curationData.categories), data, bid); - } else { - deepSetValue(bid, 'params.keywords.sd_rtd', sirdataList.concat(curationData.segments).concat(curationData.categories)); + sirdataMergedList = sirdataList.concat(curationData.segments).concat(curationData.categories); + if (sirdataMergedList && sirdataMergedList.length > 0) { + if (indexFound && moduleConfig.params.bidders[bidderIndex].hasOwnProperty('customFunction')) { + loadCustomFunction(moduleConfig.params.bidders[bidderIndex].customFunction, adUnit, sirdataMergedList, data, bid); + } else { + deepSetValue(bid, 'params.keywords.sd_rtd', sirdataMergedList); + } } break; @@ -251,15 +273,18 @@ export function addSegmentData(adUnits, data, moduleConfig, onDone) { if (data.shared_taxonomy && data.shared_taxonomy[curationId]) { curationData = getSegAndCatsArray(data.shared_taxonomy[curationId], minScore); } - if (indexFound && moduleConfig.params.bidders[bidderIndex].hasOwnProperty('customFunction')) { - loadCustomFunction(moduleConfig.params.bidders[bidderIndex].customFunction, adUnit, sirdataList.concat(curationData.segments).concat(curationData.categories), data, bid); - } else { - sirdataList.concat(curationData.segments).concat(curationData.categories).forEach(function(entry) { - if (target.indexOf('sd_rtd=' + entry) === -1) { - target.push('sd_rtd=' + entry); - } - }); - deepSetValue(bid, 'params.target', target.join(';')); + sirdataMergedList = sirdataList.concat(curationData.segments).concat(curationData.categories); + if (sirdataMergedList && sirdataMergedList.length > 0) { + if (indexFound && moduleConfig.params.bidders[bidderIndex].hasOwnProperty('customFunction')) { + loadCustomFunction(moduleConfig.params.bidders[bidderIndex].customFunction, adUnit, sirdataMergedList, data, bid); + } else { + sirdataMergedList.forEach(function (entry) { + if (target.indexOf('sd_rtd=' + entry) === -1) { + target.push('sd_rtd=' + entry); + } + }); + deepSetValue(bid, 'params.target', target.join(';')); + } } break; @@ -269,10 +294,13 @@ export function addSegmentData(adUnits, data, moduleConfig, onDone) { if (data.shared_taxonomy && data.shared_taxonomy[curationId]) { curationData = getSegAndCatsArray(data.shared_taxonomy[curationId], minScore); } - if (indexFound && moduleConfig.params.bidders[bidderIndex].hasOwnProperty('customFunction')) { - loadCustomFunction(moduleConfig.params.bidders[bidderIndex].customFunction, adUnit, sirdataList.concat(curationData.segments).concat(curationData.categories), data, bid); - } else { - setBidderOrtb2(bid.bidder, data.segments.concat(curationData.segments), sirdataList.concat(curationData.segments).concat(curationData.categories)); + sirdataMergedList = sirdataList.concat(curationData.segments).concat(curationData.categories); + if (sirdataMergedList && sirdataMergedList.length > 0) { + if (indexFound && moduleConfig.params.bidders[bidderIndex].hasOwnProperty('customFunction')) { + loadCustomFunction(moduleConfig.params.bidders[bidderIndex].customFunction, adUnit, sirdataMergedList, data, bid); + } else { + setBidderOrtb2(bid.bidder, data.segments.concat(curationData.segments), sirdataMergedList); + } } break; @@ -284,20 +312,23 @@ export function addSegmentData(adUnits, data, moduleConfig, onDone) { if (data.shared_taxonomy && data.shared_taxonomy[curationId]) { curationData = getSegAndCatsArray(data.shared_taxonomy[curationId], minScore); } - if (indexFound && moduleConfig.params.bidders[bidderIndex].hasOwnProperty('customFunction')) { - loadCustomFunction(moduleConfig.params.bidders[bidderIndex].customFunction, adUnit, sirdataList.concat(curationData.segments).concat(curationData.categories), data, bid); - } else { - var cappIxCategories = []; - var ixLength = 0; - var ixLimit = (indexFound && moduleConfig.params.bidders[bidderIndex].hasOwnProperty('sizeLimit') ? moduleConfig.params.bidders[bidderIndex].sizeLimit : 1000); - // Push ids For publisher use and for curation if exists but limit size because the bidder uses GET parameters - sirdataList.concat(curationData.segments).concat(curationData.categories).forEach(function(entry) { - if (ixLength < ixLimit) { - cappIxCategories.push(entry); - ixLength += entry.toString().length; - } - }); - getGlobal().setConfig({ix: {firstPartyData: {sd_rtd: cappIxCategories}}}); + sirdataMergedList = sirdataList.concat(curationData.segments).concat(curationData.categories); + if (sirdataMergedList && sirdataMergedList.length > 0) { + if (indexFound && moduleConfig.params.bidders[bidderIndex].hasOwnProperty('customFunction')) { + loadCustomFunction(moduleConfig.params.bidders[bidderIndex].customFunction, adUnit, sirdataMergedList, data, bid); + } else { + var cappIxCategories = []; + var ixLength = 0; + var ixLimit = (indexFound && moduleConfig.params.bidders[bidderIndex].hasOwnProperty('sizeLimit') ? moduleConfig.params.bidders[bidderIndex].sizeLimit : 1000); + // Push ids For publisher use and for curation if exists but limit size because the bidder uses GET parameters + sirdataMergedList.forEach(function (entry) { + if (ixLength < ixLimit) { + cappIxCategories.push(entry); + ixLength += entry.toString().length; + } + }); + getGlobal().setConfig({ix: {firstPartyData: {sd_rtd: cappIxCategories}}}); + } } } break; @@ -310,10 +341,16 @@ export function addSegmentData(adUnits, data, moduleConfig, onDone) { } else { data.shared_taxonomy[curationId] = {contextual_categories: {}}; } - if (indexFound && moduleConfig.params.bidders[bidderIndex].hasOwnProperty('customFunction')) { - loadCustomFunction(moduleConfig.params.bidders[bidderIndex].customFunction, adUnit, sirdataList.concat(curationData.segments).concat(curationData.categories), data, bid); - } else { - deepSetValue(bid, 'ortb2.user.ext.data', {segments: sirdataData.segments.concat(curationData.segments), contextual_categories: {...data.contextual_categories, ...data.shared_taxonomy[curationId].contextual_categories}}); + sirdataMergedList = sirdataList.concat(curationData.segments).concat(curationData.categories); + if (sirdataMergedList && sirdataMergedList.length > 0) { + if (indexFound && moduleConfig.params.bidders[bidderIndex].hasOwnProperty('customFunction')) { + loadCustomFunction(moduleConfig.params.bidders[bidderIndex].customFunction, adUnit, sirdataMergedList, data, bid); + } else { + deepSetValue(bid, 'ortb2.user.ext.data', { + segments: sirdataData.segments.concat(curationData.segments), + contextual_categories: {...data.contextual_categories, ...data.shared_taxonomy[curationId].contextual_categories} + }); + } } break; @@ -323,10 +360,13 @@ export function addSegmentData(adUnits, data, moduleConfig, onDone) { if (data.shared_taxonomy && data.shared_taxonomy[curationId]) { curationData = getSegAndCatsArray(data.shared_taxonomy[curationId], minScore); } - if (indexFound && moduleConfig.params.bidders[bidderIndex].hasOwnProperty('customFunction')) { - loadCustomFunction(moduleConfig.params.bidders[bidderIndex].customFunction, adUnit, sirdataList.concat(curationData.segments).concat(curationData.categories), data, bid); - } else { - setBidderOrtb2(bid.bidder, sirdataList.concat(curationData.segments).concat(curationData.categories), sirdataList.concat(curationData.segments).concat(curationData.categories)); + sirdataMergedList = sirdataList.concat(curationData.segments).concat(curationData.categories); + if (sirdataMergedList && sirdataMergedList.length > 0) { + if (indexFound && moduleConfig.params.bidders[bidderIndex].hasOwnProperty('customFunction')) { + loadCustomFunction(moduleConfig.params.bidders[bidderIndex].customFunction, adUnit, sirdataMergedList, data, bid); + } else { + setBidderOrtb2(bid.bidder, data.segments.concat(curationData.segments), sirdataMergedList); + } } break; @@ -336,10 +376,13 @@ export function addSegmentData(adUnits, data, moduleConfig, onDone) { if (data.shared_taxonomy && data.shared_taxonomy[curationId]) { curationData = getSegAndCatsArray(data.shared_taxonomy[curationId], minScore); } - if (indexFound && moduleConfig.params.bidders[bidderIndex].hasOwnProperty('customFunction')) { - loadCustomFunction(moduleConfig.params.bidders[bidderIndex].customFunction, adUnit, sirdataList.concat(curationData.segments).concat(curationData.categories), data, bid); - } else { - setBidderOrtb2(bid.bidder, data.segments.concat(curationData.segments), sirdataList.concat(curationData.segments).concat(curationData.categories)); + sirdataMergedList = sirdataList.concat(curationData.segments).concat(curationData.categories); + if (sirdataMergedList && sirdataMergedList.length > 0) { + if (indexFound && moduleConfig.params.bidders[bidderIndex].hasOwnProperty('customFunction')) { + loadCustomFunction(moduleConfig.params.bidders[bidderIndex].customFunction, adUnit, sirdataMergedList, data, bid); + } else { + setBidderOrtb2(bid.bidder, data.segments.concat(curationData.segments), sirdataMergedList); + } } break; @@ -350,10 +393,13 @@ export function addSegmentData(adUnits, data, moduleConfig, onDone) { if (data.shared_taxonomy && data.shared_taxonomy[curationId]) { curationData = getSegAndCatsArray(data.shared_taxonomy[curationId], minScore); } - if (indexFound && moduleConfig.params.bidders[bidderIndex].hasOwnProperty('customFunction')) { - loadCustomFunction(moduleConfig.params.bidders[bidderIndex].customFunction, adUnit, sirdataList.concat(curationData.segments).concat(curationData.categories), data, bid); - } else { - setBidderOrtb2(bid.bidder, data.segments.concat(curationData.segments), sirdataList.concat(curationData.segments).concat(curationData.categories)); + sirdataMergedList = sirdataList.concat(curationData.segments).concat(curationData.categories); + if (sirdataMergedList && sirdataMergedList.length > 0) { + if (indexFound && moduleConfig.params.bidders[bidderIndex].hasOwnProperty('customFunction')) { + loadCustomFunction(moduleConfig.params.bidders[bidderIndex].customFunction, adUnit, sirdataMergedList, data, bid); + } else { + setBidderOrtb2(bid.bidder, data.segments.concat(curationData.segments), sirdataMergedList); + } } break; @@ -363,10 +409,61 @@ export function addSegmentData(adUnits, data, moduleConfig, onDone) { if (data.shared_taxonomy && data.shared_taxonomy[curationId]) { curationData = getSegAndCatsArray(data.shared_taxonomy[curationId], minScore); } - if (indexFound && moduleConfig.params.bidders[bidderIndex].hasOwnProperty('customFunction')) { - loadCustomFunction(moduleConfig.params.bidders[bidderIndex].customFunction, adUnit, sirdataList.concat(curationData.segments).concat(curationData.categories), data, bid); - } else { - setBidderOrtb2(bid.bidder, data.segments.concat(curationData.segments), sirdataList.concat(curationData.segments).concat(curationData.categories)); + sirdataMergedList = sirdataList.concat(curationData.segments).concat(curationData.categories); + if (sirdataMergedList && sirdataMergedList.length > 0) { + if (indexFound && moduleConfig.params.bidders[bidderIndex].hasOwnProperty('customFunction')) { + loadCustomFunction(moduleConfig.params.bidders[bidderIndex].customFunction, adUnit, sirdataMergedList, data, bid); + } else { + setBidderOrtb2(bid.bidder, data.segments.concat(curationData.segments), sirdataMergedList); + } + } + break; + + case 'yahoossp': + // For curation Yahoo is pid 30339 + curationId = (indexFound && moduleConfig.params.bidders[bidderIndex].hasOwnProperty('curationId') ? moduleConfig.params.bidders[bidderIndex].curationId : '30339'); + if (data.shared_taxonomy && data.shared_taxonomy[curationId]) { + curationData = getSegAndCatsArray(data.shared_taxonomy[curationId], minScore); + } + sirdataMergedList = sirdataList.concat(curationData.segments).concat(curationData.categories); + if (sirdataMergedList && sirdataMergedList.length > 0) { + if (indexFound && moduleConfig.params.bidders[bidderIndex].hasOwnProperty('customFunction')) { + loadCustomFunction(moduleConfig.params.bidders[bidderIndex].customFunction, adUnit, sirdataMergedList, data, bid); + } else { + setBidderOrtb2(bid.bidder, data.segments.concat(curationData.segments), sirdataMergedList); + } + } + break; + + case 'openx': + // For curation OpenX is pid 30342 + curationId = (indexFound && moduleConfig.params.bidders[bidderIndex].hasOwnProperty('curationId') ? moduleConfig.params.bidders[bidderIndex].curationId : '30342'); + if (data.shared_taxonomy && data.shared_taxonomy[curationId]) { + curationData = getSegAndCatsArray(data.shared_taxonomy[curationId], minScore); + } + sirdataMergedList = sirdataList.concat(curationData.segments).concat(curationData.categories); + if (sirdataMergedList && sirdataMergedList.length > 0) { + if (indexFound && moduleConfig.params.bidders[bidderIndex].hasOwnProperty('customFunction')) { + loadCustomFunction(moduleConfig.params.bidders[bidderIndex].customFunction, adUnit, sirdataMergedList, data, bid); + } else { + setBidderOrtb2(bid.bidder, data.segments.concat(curationData.segments), sirdataMergedList); + } + } + break; + + case 'pubmatic': + // For curation Pubmatic is pid 30345 + curationId = (indexFound && moduleConfig.params.bidders[bidderIndex].hasOwnProperty('curationId') ? moduleConfig.params.bidders[bidderIndex].curationId : '30345'); + if (data.shared_taxonomy && data.shared_taxonomy[curationId]) { + curationData = getSegAndCatsArray(data.shared_taxonomy[curationId], minScore); + } + sirdataMergedList = sirdataList.concat(curationData.segments).concat(curationData.categories); + if (sirdataMergedList && sirdataMergedList.length > 0) { + if (indexFound && moduleConfig.params.bidders[bidderIndex].hasOwnProperty('customFunction')) { + loadCustomFunction(moduleConfig.params.bidders[bidderIndex].customFunction, adUnit, sirdataMergedList, data, bid); + } else { + setBidderOrtb2(bid.bidder, data.segments.concat(curationData.segments), sirdataMergedList); + } } break; @@ -381,7 +478,9 @@ export function addSegmentData(adUnits, data, moduleConfig, onDone) { } } } - } catch (e) { logError(e) } + } catch (e) { + logError(e) + } }) }); diff --git a/modules/sizeMappingV2.js b/modules/sizeMappingV2.js index b3a6d32b2d8..405799813eb 100644 --- a/modules/sizeMappingV2.js +++ b/modules/sizeMappingV2.js @@ -4,11 +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 { 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 = { @@ -20,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; } /** @@ -167,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! @@ -289,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); } }); @@ -423,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, @@ -475,120 +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 - - bid = Object.assign({}, bid, getDefinedParams(adUnit, ['mediaType', 'renderer', 'nativeParams'])); - - 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/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 afdc354c230..eed9ccb7461 100644 --- a/modules/sovrnBidAdapter.js +++ b/modules/sovrnBidAdapter.js @@ -1,8 +1,8 @@ import { _each, getBidIdParameter, isArray, deepClone, parseUrl, getUniqueIdentifierStr, deepSetValue, logError, deepAccess, isInteger, logWarn } from '../src/utils.js'; import { registerBidder } from '../src/adapters/bidderFactory.js' import { ADPOD, BANNER, VIDEO } from '../src/mediaTypes.js' -import { createEidsArray } from './userId/eids.js'; -import {config} from '../src/config.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'), @@ -12,7 +12,7 @@ const ORTB_VIDEO_PARAMS = { 'w': (value) => isInteger(value), 'h': (value) => isInteger(value), 'startdelay': (value) => isInteger(value), - 'placement': (value) => Array.isArray(value) && value.every(v => v >= 1 && v <= 5), + '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), @@ -25,11 +25,19 @@ const ORTB_VIDEO_PARAMS = { '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) => [1, 2, 3].indexOf(value) !== -1, - 'pos': (value) => Array.isArray(value) && value.every(v => v >= 0 && v <= 7), + '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) } +const REQUIRED_VIDEO_PARAMS = { + context: (value) => value !== ADPOD, + mimes: ORTB_VIDEO_PARAMS.mimes, + minduration: ORTB_VIDEO_PARAMS.minduration, + maxduration: ORTB_VIDEO_PARAMS.maxduration, + protocols: ORTB_VIDEO_PARAMS.protocols +} + export const spec = { code: 'sovrn', supportedMediaTypes: [BANNER, VIDEO], @@ -40,19 +48,25 @@ export const spec = { * @param {object} bid the Sovrn bid to validate * @return boolean for whether or not a bid is valid */ - isBidRequestValid: function(bid) { + isBidRequestValid: function (bid) { + const video = bid?.mediaTypes?.video return !!( bid.params.tagid && !isNaN(parseFloat(bid.params.tagid)) && - isFinite(bid.params.tagid) && - deepAccess(bid, 'mediaTypes.video.context') !== ADPOD + isFinite(bid.params.tagid) && ( + !video || ( + Object.keys(REQUIRED_VIDEO_PARAMS) + .every(key => REQUIRED_VIDEO_PARAMS[key](video[key])) + ) + ) ) }, /** * Format the bid request object for our endpoint - * @param {BidRequest[]} bidRequests Array of Sovrn bidders * @return object of parameters for Prebid AJAX request + * @param bidReqs + * @param bidderRequest */ buildRequests: function(bidReqs, bidderRequest) { try { @@ -179,14 +193,12 @@ export const spec = { * @return {Bid[]} An array of formatted bids. */ interpretResponse: function({ body: {id, seatbid} }) { + if (!id || !seatbid || !Array.isArray(seatbid)) return [] + try { - let sovrnBidResponses = []; - if (id && - seatbid && - seatbid.length > 0 && - seatbid[0].bid && - seatbid[0].bid.length > 0) { - seatbid[0].bid.map(sovrnBid => { + return seatbid + .filter(seat => seat) + .map(seat => seat.bid.map(sovrnBid => { const bid = { requestId: sovrnBid.impid, cpm: parseFloat(sovrnBid.price), @@ -196,23 +208,23 @@ export const spec = { dealId: sovrnBid.dealid || null, currency: 'USD', netRevenue: true, - ttl: sovrnBid.ext ? (sovrnBid.ext.ttl || 90) : 90, + mediaType: sovrnBid.nurl ? BANNER : VIDEO, + ttl: sovrnBid.ext?.ttl || 90, meta: { advertiserDomains: sovrnBid && sovrnBid.adomain ? sovrnBid.adomain : [] } } - if (!sovrnBid.nurl) { - bid.mediaType = VIDEO - bid.vastXml = decodeURIComponent(sovrnBid.adm) - } else { - bid.mediaType = BANNER + if (sovrnBid.nurl) { bid.ad = decodeURIComponent(`${sovrnBid.adm}`) + } else { + bid.vastXml = decodeURIComponent(sovrnBid.adm) } - sovrnBidResponses.push(bid); - }); - } - return sovrnBidResponses + + return bid + })) + .flat() } catch (e) { - logError('Could not intrepret bidresponse, error deatils:', e); + logError('Could not interpret bidresponse, error details:', e) + return e } }, 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 02b786f4fd2..67f806ff792 100644 --- a/modules/sspBCBidAdapter.js +++ b/modules/sspBCBidAdapter.js @@ -1,8 +1,9 @@ -import { isArray, deepAccess, logWarn, parseUrl } from '../src/utils.js'; +import { deepAccess, isArray, logWarn, parseUrl, getWindowTop } from '../src/utils.js'; import { ajax } from '../src/ajax.js'; +import { config } from '../src/config.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 { includes as strIncludes } from '../src/polyfill.js'; const BIDDER_CODE = 'sspBC'; const BIDDER_URL = 'https://ssp.wp.pl/bidder/'; @@ -11,7 +12,8 @@ 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.41'; +const BIDDER_VERSION = '5.6'; +const DEFAULT_CURRENCY = 'PLN'; const W = window; const { navigator } = W; const oneCodeDetection = {}; @@ -20,6 +22,25 @@ const adSizesCalled = {}; const pageView = {}; var consentApiVersion; +/** + * Get preferred language of browser (i.e. user) + * @returns {string} languageCode - ISO language code + */ +const getBrowserLanguage = () => navigator.language || (navigator.languages && navigator.languages[0]); + +/** + * Get language of top level html object + * @returns {string} languageCode - ISO language code + */ +const getContentLanguage = () => { + try { + const topWindow = getWindowTop(); + return topWindow.document.body.parentNode.lang; + } catch (err) { + logWarn('Could not read language form top-level html', err); + } +}; + /** * Get bid parameters for notification * @param {*} bidData - bid (bidWon), or array of bids (timeout) @@ -28,38 +49,50 @@ const getNotificationPayload = bidData => { if (bidData) { const bids = isArray(bidData) ? bidData : [bidData]; if (bids.length > 0) { - const result = { + let result = { requestId: undefined, siteId: [], slotId: [], tagid: [], } bids.forEach(bid => { - let params = isArray(bid.params) ? bid.params[0] : bid.params; + const { adUnitCode, auctionId, cpm, creativeId, meta, params: bidParams, requestId, timeout } = bid; + let params = isArray(bidParams) ? bidParams[0] : bidParams; params = params || {}; - // check for stored detection - if (oneCodeDetection[bid.requestId]) { - params.siteId = oneCodeDetection[bid.requestId][0]; - params.id = oneCodeDetection[bid.requestId][1]; + // basic notification data + const bidBasicData = { + requestId: auctionId || result.requestId, + timeout: timeout || result.timeout, + pvid: pageView.id, } + result = { ...result, ...bidBasicData } + + result.tagid.push(adUnitCode); + // check for stored detection + if (oneCodeDetection[requestId]) { + params.siteId = oneCodeDetection[requestId][0]; + params.id = oneCodeDetection[requestId][1]; + } if (params.siteId) { result.siteId.push(params.siteId); } if (params.id) { result.slotId.push(params.id); } - if (bid.cpm) { - const meta = bid.meta || {}; - result.cpm = bid.cpm; - result.creativeId = bid.creativeId; - result.adomain = meta.advertiserDomains && meta.advertiserDomains[0]; - result.networkName = meta.networkName; + + if (cpm) { + // non-empty bid data + const bidNonEmptyData = { + cpm, + cpmpl: meta && meta.pricepl, + creativeId, + adomain: meta && meta.advertiserDomains && meta.advertiserDomains[0], + networkName: meta && meta.networkName, + } + result = { ...result, ...bidNonEmptyData } } - result.tagid.push(bid.adUnitCode); - result.requestId = bid.auctionId || result.requestId; - result.timeout = bid.timeout || result.timeout; }) return result; } @@ -97,7 +130,7 @@ const applyClientHints = ortbRequest => { */ if (!pageView.id || location.pathname !== pageView.path) { pageView.path = location.pathname; - pageView.id = Math.floor(1E20 * Math.random()); + pageView.id = Math.floor(1E20 * Math.random()).toString(); } Object.keys(hints).forEach(key => { @@ -120,7 +153,7 @@ const applyClientHints = ortbRequest => { name: 'pvid', segment: [ { - value: `${pageView.id}` + value: pageView.id } ] }]; @@ -151,6 +184,12 @@ const applyGdpr = (bidderRequest, ortbRequest) => { } } +/** + * Get currency (either default or adserver) + * @returns {string} currency name + */ +const getCurrency = () => config.getConfig('currency.adServerCurrency') || DEFAULT_CURRENCY; + /** * Get value for first occurence of key within the collection */ @@ -260,12 +299,13 @@ const mapNative = slot => { return assets ? { request: JSON.stringify({ native: { assets } }) } : undefined; } -var mapVideo = slot => { - var video = deepAccess(slot, 'mediaTypes.video'); - var videoParamsUsed = ['api', 'context', 'linearity', 'maxduration', 'mimes', 'protocols']; +var mapVideo = (slot, videoFromBid) => { + var videoFromSlot = deepAccess(slot, 'mediaTypes.video'); + var videoParamsUsed = ['api', 'context', 'linearity', 'maxduration', 'mimes', 'protocols', 'playbackmethod']; var videoAssets; - if (video) { + if (videoFromSlot) { + const video = videoFromBid ? Object.assign(videoFromSlot, videoFromBid) : videoFromSlot; var videoParams = Object.keys(video); var playerSize = video.playerSize; videoAssets = {}; // player width / height @@ -292,7 +332,7 @@ var mapVideo = slot => { const mapImpression = slot => { const { adUnitCode, bidId, params = {}, ortb2Imp = {} } = slot; - const { id, siteId } = params; + const { id, siteId, video } = params; const { ext = {} } = ortb2Imp; /* @@ -313,12 +353,13 @@ const mapImpression = slot => { id: id && siteId ? id.padStart(3, '0') : 'bidid-' + bidId, banner: mapBanner(slot), native: mapNative(slot), - video: mapVideo(slot), + video: mapVideo(slot, video), tagid: adUnitCode, ext, }; // Check floorprices for this imp + const currency = getCurrency(); if (typeof slot.getFloor === 'function') { var bannerFloor = 0; var nativeFloor = 0; @@ -328,20 +369,24 @@ const mapImpression = slot => { bannerFloor = slot.sizes.reduce(function (prev, next) { var currentFloor = slot.getFloor({ mediaType: 'banner', - size: next + size: next, + currency }).floor; return prev > currentFloor ? prev : currentFloor; }, 0); } nativeFloor = slot.getFloor({ - mediaType: 'native' + mediaType: 'native', currency }); videoFloor = slot.getFloor({ - mediaType: 'video' + mediaType: 'video', currency }); imp.bidfloor = Math.max(bannerFloor, nativeFloor, videoFloor); + } else { + imp.bidfloor = 0; } + imp.bidfloorcur = currency; return imp; } @@ -463,13 +508,19 @@ const renderCreative = (site, auctionId, bid, seat, request) => { window.ref = "${site.ref}"; window.adlabel = "${site.adLabel ? site.adLabel : ''}"; window.pubid = "${site.publisherId ? site.publisherId : ''}"; + window.requestPVID = "${pageView.id}"; `; + if (gam) { + adcode += `window.gam = ${JSON.stringify(gam)};`; + } + adcode += `
- + + `; @@ -512,12 +563,15 @@ const spec = { publisher: publisherId ? { id: publisherId } : undefined, page, domain, - ref + ref, + content: { language: getContentLanguage() }, }, imp: validBidRequests.map(slot => mapImpression(slot)), + cur: [getCurrency()], tmax, user: {}, regs: {}, + device: { language: getBrowserLanguage() }, test: testMode, }; @@ -539,6 +593,7 @@ const spec = { const bids = []; const site = JSON.parse(request.data).site; // get page and referer data from request site.sn = response.sn || 'mc_adapter'; // WPM site name (wp_sn) + pageView.sn = site.sn; // store site_name (for syncing and notifications) let seat; if (response.seatbid !== undefined) { @@ -547,6 +602,7 @@ const spec = { 'bidid-' prefix indicates oneCode (parameterless) request and response */ response.seatbid.forEach(seatbid => { + let creativeCache; seat = seatbid.seat; seatbid.bid.forEach(serverBid => { // get data from bid response @@ -572,11 +628,12 @@ const spec = { ext also might contain publisherId and custom ad label */ - const { siteid, slotid, pubid, adlabel } = ext; + const { siteid, slotid, pubid, adlabel, cache } = ext; site.id = siteid || site.id; site.slot = slotid || site.slot; site.publisherId = pubid; site.adLabel = adlabel; + creativeCache = cache; } if (bidRequest && site.id && !strIncludes(site.id, 'bidid')) { @@ -597,6 +654,7 @@ const spec = { meta: { advertiserDomains: adomain, networkName: seat, + pricepl: ext && ext.pricepl, }, netRevenue: true, }; @@ -608,6 +666,7 @@ const spec = { bid.mediaType = 'video'; bid.vastXml = serverBid.adm; bid.vastContent = serverBid.adm; + bid.vastUrl = creativeCache; } else if (isNativeAd(serverBid)) { // native bid.mediaType = 'native'; @@ -662,7 +721,7 @@ const spec = { if (syncOptions.iframeEnabled && consentApiVersion != 1) { mySyncs.push({ type: 'iframe', - url: `${SYNC_URL}?tcf=${consentApiVersion}`, + url: `${SYNC_URL}?tcf=${consentApiVersion}&pvid=${pageView.id}&sn=${pageView.sn}`, }); }; return mySyncs; diff --git a/modules/sspBCBidAdapter.md b/modules/sspBCBidAdapter.md index 0da84857cbf..4ae2e425865 100644 --- a/modules/sspBCBidAdapter.md +++ b/modules/sspBCBidAdapter.md @@ -21,6 +21,7 @@ Optional parameters: - page - tmax - test +- video # Test Parameters ``` diff --git a/modules/synacormediaBidAdapter.js b/modules/synacormediaBidAdapter.js index 96616bb6a48..4cc648a2e04 100644 --- a/modules/synacormediaBidAdapter.js +++ b/modules/synacormediaBidAdapter.js @@ -1,9 +1,9 @@ 'use strict'; -import { getAdUnitSizes, logWarn, deepSetValue, isFn, isPlainObject } from '../src/utils.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 {deepSetValue, getAdUnitSizes, isFn, isPlainObject, logWarn} from '../src/utils.js'; +import {registerBidder} from '../src/adapters/bidderFactory.js'; +import {BANNER, VIDEO} from '../src/mediaTypes.js'; +import {includes} from '../src/polyfill.js'; import {config} from '../src/config.js'; const BID_SCHEME = 'https://'; @@ -14,12 +14,7 @@ const BLOCKED_AD_SIZES = [ '1x1', '1x2' ]; -const SUPPORTED_USER_ID_SOURCES = [ - 'liveramp.com', // Liveramp IdentityLink - 'nextroll.com', // NextRoll XID - 'verizonmedia.com', // Verizon Media ConnectID - 'pubcid.org' // PubCommon ID -]; +const DEFAULT_MAX_TTL = 420; // 7 minutes export const spec = { code: 'synacormedia', supportedMediaTypes: [ BANNER, VIDEO ], @@ -95,7 +90,7 @@ export const spec = { // User ID if (validBidReqs[0] && validBidReqs[0].userIdAsEids && Array.isArray(validBidReqs[0].userIdAsEids)) { - const eids = this.processEids(validBidReqs[0].userIdAsEids); + const eids = validBidReqs[0].userIdAsEids; if (eids.length) { deepSetValue(openRtbBidRequest, 'user.ext.eids', eids); } @@ -114,16 +109,6 @@ export const spec = { } }, - processEids: function(userIdAsEids) { - const eids = []; - userIdAsEids.forEach(function(eid) { - if (SUPPORTED_USER_ID_SOURCES.indexOf(eid.source) > -1) { - eids.push(eid); - } - }); - return eids; - }, - buildBannerImpressions: function (adSizes, bid, tagIdOrPlacementId, pos, videoOrBannerKey) { let format = []; let imps = []; @@ -248,6 +233,19 @@ export const spec = { } }); } + + let maxTtl = DEFAULT_MAX_TTL; + if (bid.ext && bid.ext['imds.tv'] && bid.ext['imds.tv'].ttl) { + const bidTtlMax = parseInt(bid.ext['imds.tv'].ttl, 10); + maxTtl = !isNaN(bidTtlMax) && bidTtlMax > 0 ? bidTtlMax : DEFAULT_MAX_TTL; + } + + let ttl = maxTtl; + if (bid.exp) { + const bidTtl = parseInt(bid.exp, 10); + ttl = !isNaN(bidTtl) && bidTtl > 0 ? Math.min(bidTtl, maxTtl) : maxTtl; + } + const bidObj = { requestId: impid, cpm: parseFloat(bid.price), @@ -258,7 +256,7 @@ export const spec = { netRevenue: true, mediaType: isVideo ? VIDEO : BANNER, ad: creative, - ttl: 60 + ttl, }; if (bid.adomain != undefined || bid.adomain != null) { diff --git a/modules/talkadsBidAdapter.js b/modules/talkadsBidAdapter.js index f95456b5c54..dae452b9a7d 100644 --- a/modules/talkadsBidAdapter.js +++ b/modules/talkadsBidAdapter.js @@ -5,11 +5,12 @@ import {ajax} from '../src/ajax.js'; const CURRENCY = 'EUR'; const BIDDER_CODE = 'talkads'; +const GVLID = 1074; export const spec = { code: BIDDER_CODE, + gvlid: GVLID, supportedMediaTypes: [ NATIVE, BANNER ], - params: null, /** * Determines whether or not the given bid request is valid. @@ -17,7 +18,7 @@ export const spec = { * @param poBid The bid params to validate. * @return boolean True if this is a valid bid, and false otherwise. */ - isBidRequestValid: (poBid) => { + isBidRequestValid: function (poBid) { utils.logInfo('isBidRequestValid : ', poBid); if (poBid.params === undefined) { utils.logError('VALIDATION FAILED : the parameters must be defined'); @@ -31,7 +32,7 @@ export const spec = { utils.logError('VALIDATION FAILED : the parameter "bidder_url" must be defined'); return false; } - this.params = poBid.params; + return !!(poBid.nativeParams || poBid.sizes); }, // isBidRequestValid @@ -42,7 +43,7 @@ export const spec = { * @param poBidderRequest * @return ServerRequest Info describing the request to the server. */ - buildRequests: (paValidBidRequests, poBidderRequest) => { + buildRequests: function (paValidBidRequests, poBidderRequest) { utils.logInfo('buildRequests : ', paValidBidRequests, poBidderRequest); const laBids = paValidBidRequests.map((poBid, piId) => { const loOne = { id: piId, ad_unit: poBid.adUnitCode, bid_id: poBid.bidId, type: '', size: [] }; @@ -54,6 +55,7 @@ export const spec = { } return loOne; }); + let laParams = paValidBidRequests[0].params; const loServerRequest = { cur: CURRENCY, timeout: poBidderRequest.timeout, @@ -71,7 +73,7 @@ export const spec = { loServerRequest.gdpr.consent = poBidderRequest.gdprConsent.consentString; } } - const lsUrl = this.params.bidder_url + '/' + this.params.tag_id; + const lsUrl = laParams.bidder_url + '/' + laParams.tag_id; return { method: 'POST', url: lsUrl, @@ -86,7 +88,7 @@ export const spec = { * @param poPidRequest Request original server request * @return An array of bids which were nested inside the server. */ - interpretResponse: (poServerResponse, poPidRequest) => { + interpretResponse: function (poServerResponse, poPidRequest) { utils.logInfo('interpretResponse : ', poServerResponse); if (!poServerResponse.body) { return []; @@ -118,10 +120,11 @@ export const spec = { * * @param poBid The bid that won the auction */ - onBidWon: (poBid) => { + onBidWon: function (poBid) { utils.logInfo('onBidWon : ', poBid); + let laParams = poBid.params[0]; if (poBid.pbid) { - ajax(this.params.bidder_url + 'won/' + poBid.pbid); + ajax(laParams.bidder_url + 'won/' + poBid.pbid); } }, // onBidWon }; diff --git a/modules/targetVideoBidAdapter.js b/modules/targetVideoBidAdapter.js index 5714916c131..4deb63b6426 100644 --- a/modules/targetVideoBidAdapter.js +++ b/modules/targetVideoBidAdapter.js @@ -1,16 +1,18 @@ -import find from 'core-js-pure/features/array/find.js'; -import { getBidRequest } from '../src/utils.js'; -import { BANNER, VIDEO } from '../src/mediaTypes.js'; -import { registerBidder } from '../src/adapters/bidderFactory.js'; +import {find} from '../src/polyfill.js'; +import {getBidRequest} from '../src/utils.js'; +import {BANNER, VIDEO} from '../src/mediaTypes.js'; +import {registerBidder} from '../src/adapters/bidderFactory.js'; const SOURCE = 'pbjs'; const BIDDER_CODE = 'targetVideo'; const ENDPOINT_URL = 'https://ib.adnxs.com/ut/v3/prebid'; const MARGIN = 1.35; +const GVLID = 786; export const spec = { code: BIDDER_CODE, + gvlid: GVLID, supportedMediaTypes: [BANNER], /** diff --git a/modules/teadsBidAdapter.js b/modules/teadsBidAdapter.js index 4bea8858d98..a8902c896f6 100644 --- a/modules/teadsBidAdapter.js +++ b/modules/teadsBidAdapter.js @@ -12,7 +12,7 @@ const gdprStatus = { CMP_NOT_FOUND_OR_ERROR: 22 }; const FP_TEADS_ID_COOKIE_NAME = '_tfpvi'; -export const storage = getStorageManager(GVL_ID, BIDDER_CODE); +export const storage = getStorageManager({gvlid: GVL_ID, bidderCode: BIDDER_CODE}); export const spec = { code: BIDDER_CODE, diff --git a/modules/telariaBidAdapter.js b/modules/telariaBidAdapter.js index 50a6ccc9fb2..42913414cbc 100644 --- a/modules/telariaBidAdapter.js +++ b/modules/telariaBidAdapter.js @@ -9,7 +9,11 @@ const EVENTS_ENDPOINT = `events.${DOMAIN}/diag`; export const spec = { code: BIDDER_CODE, - aliases: ['tremor', 'tremorvideo'], + gvlid: 52, + aliases: [ + { code: 'tremor', gvlid: 52 }, + { code: 'tremorvideo', gvlid: 52 } + ], supportedMediaTypes: [VIDEO], /** * Determines if the request is valid diff --git a/modules/tpmnBidAdapter.js b/modules/tpmnBidAdapter.js index 006357cd4b9..88e89bcd64b 100644 --- a/modules/tpmnBidAdapter.js +++ b/modules/tpmnBidAdapter.js @@ -1,13 +1,16 @@ /* eslint-disable no-tabs */ import { registerBidder } from '../src/adapters/bidderFactory.js'; import { parseUrl, deepAccess } from '../src/utils.js'; +import { getStorageManager } from '../src/storageManager.js'; import { BANNER } from '../src/mediaTypes.js'; +import { config } from '../src/config.js'; export const ADAPTER_VERSION = '1'; const SUPPORTED_AD_TYPES = [BANNER]; - const BIDDER_CODE = 'tpmn'; const URL = 'https://ad.tpmn.co.kr/prebidhb.tpmn'; +const IFRAMESYNC = 'https://ad.tpmn.co.kr/sync.tpmn?type=iframe'; +export const storage = getStorageManager({bidderCode: BIDDER_CODE}); export const spec = { code: BIDDER_CODE, @@ -18,20 +21,20 @@ export const spec = { * @param {object} bid The bid to validate. * @return boolean True if this is a valid bid, and false otherwise. */ - isBidRequestValid: function(bid) { + isBidRequestValid: function (bid) { return 'params' in bid && - 'inventoryId' in bid.params && - 'publisherId' in bid.params && - !isNaN(Number(bid.params.inventoryId)) && - bid.params.inventoryId > 0 && - (typeof bid.mediaTypes.banner.sizes != 'undefined'); // only accepting appropriate sizes + 'inventoryId' in bid.params && + 'publisherId' in bid.params && + !isNaN(Number(bid.params.inventoryId)) && + bid.params.inventoryId > 0 && + (typeof bid.mediaTypes.banner.sizes != 'undefined'); // only accepting appropriate sizes }, /** - * @param {BidRequest[]} bidRequests - * @param {*} bidderRequest - * @return {ServerRequest} - */ + * @param {BidRequest[]} bidRequests + * @param {*} bidderRequest + * @return {ServerRequest} + */ buildRequests: (bidRequests, bidderRequest) => { if (bidRequests.length === 0) { return []; @@ -49,11 +52,11 @@ export const spec = { }]; }, /** - * Unpack the response from the server into a list of bids. - * - * @param {serverResponse} serverResponse A successful response from the server. - * @return {Bid[]} An array of bids which were nested inside the server. - */ + * Unpack the response from the server into a list of bids. + * + * @param {serverResponse} serverResponse A successful response from the server. + * @return {Bid[]} An array of bids which were nested inside the server. + */ interpretResponse: function (serverResponse, serverRequest) { if (!Array.isArray(serverResponse.body)) { return []; @@ -63,7 +66,48 @@ export const spec = { // our server directly returns the format needed by prebid.js so no more // transformation is needed here. return bidResults; - } + }, + + getUserSyncs: function (syncOptions, serverResponses, gdprConsent, uspConsent) { + const syncArr = []; + if (syncOptions.iframeEnabled) { + let policyParam = ''; + if (gdprConsent && gdprConsent.consentString) { + if (typeof gdprConsent.gdprApplies === 'boolean') { + policyParam += `&gdpr=${Number(gdprConsent.gdprApplies)}&gdpr_consent=${gdprConsent.consentString}`; + } else { + policyParam += `&gdpr=0&gdpr_consent=${gdprConsent.consentString}`; + } + } + if (uspConsent && uspConsent.consentString) { + policyParam += `&ccpa_consent=${uspConsent.consentString}`; + } + const coppa = config.getConfig('coppa') ? 1 : 0; + policyParam += `&coppa=${coppa}`; + syncArr.push({ + type: 'iframe', + url: IFRAMESYNC + policyParam + }) + } else { + syncArr.push({ + type: 'image', + url: 'https://x.bidswitch.net/sync?ssp=tpmn' + }); + syncArr.push({ + type: 'image', + url: 'https://gocm.c.appier.net/tpmn' + }); + syncArr.push({ + type: 'image', + url: 'https://info.mmnneo.com/getGuidRedirect.info?url=https%3A%2F%2Fad.tpmn.co.kr%2Fcookiesync.tpmn%3Ftpmn_nid%3Dbf91e8b3b9d3f1af3fc1d657f090b4fb%26tpmn_buid%3D' + }); + syncArr.push({ + type: 'image', + url: 'https://sync.aralego.com/idSync?redirect=https%3A%2F%2Fad.tpmn.co.kr%2FpixelCt.tpmn%3Ftpmn_nid%3Dde91e8b3b9d3f1af3fc1d657f090b815%26tpmn_buid%3DSspCookieUserId' + }); + } + return syncArr; + }, }; registerBidder(spec); diff --git a/modules/trionBidAdapter.js b/modules/trionBidAdapter.js index dd1624f90d7..5750406116b 100644 --- a/modules/trionBidAdapter.js +++ b/modules/trionBidAdapter.js @@ -2,12 +2,11 @@ import { getBidIdParameter, parseSizesInput, tryAppendQueryString } from '../src import {registerBidder} from '../src/adapters/bidderFactory.js'; import { getStorageManager } from '../src/storageManager.js'; -const storage = getStorageManager(); - const BID_REQUEST_BASE_URL = 'https://in-appadvertising.com/api/bidRequest'; const USER_SYNC_URL = 'https://in-appadvertising.com/api/userSync.html'; const BIDDER_CODE = 'trion'; const BASE_KEY = '_trion_'; +const storage = getStorageManager({bidderCode: BIDDER_CODE}); export const spec = { code: BIDDER_CODE, diff --git a/modules/tripleliftBidAdapter.js b/modules/tripleliftBidAdapter.js index 215769e9812..7e964661db6 100644 --- a/modules/tripleliftBidAdapter.js +++ b/modules/tripleliftBidAdapter.js @@ -1,4 +1,4 @@ -import { tryAppendQueryString, logMessage, isEmpty, isStr, isPlainObject, isArray, logWarn } from '../src/utils.js'; +import { tryAppendQueryString, logMessage, logError, isEmpty, isStr, isPlainObject, isArray, logWarn } from '../src/utils.js'; import { BANNER, VIDEO } from '../src/mediaTypes.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; import { config } from '../src/config.js'; @@ -175,14 +175,18 @@ function _getORTBVideo(bidRequest) { function _getFloor (bid) { let floor = null; if (typeof bid.getFloor === 'function') { - const floorInfo = bid.getFloor({ - currency: 'USD', - mediaType: _isInstreamBidRequest(bid) ? 'video' : 'banner', - size: '*' - }); - if (typeof floorInfo === 'object' && - floorInfo.currency === 'USD' && !isNaN(parseFloat(floorInfo.floor))) { - floor = parseFloat(floorInfo.floor); + try { + const floorInfo = bid.getFloor({ + currency: 'USD', + mediaType: _isInstreamBidRequest(bid) ? 'video' : 'banner', + size: '*' + }); + if (typeof floorInfo === 'object' && + floorInfo.currency === 'USD' && !isNaN(parseFloat(floorInfo.floor))) { + floor = parseFloat(floorInfo.floor); + } + } catch (err) { + logError('Triplelift: getFloor threw an error: ', err); } } return floor !== null ? floor : bid.params.floor; @@ -192,9 +196,9 @@ function _getGlobalFpd() { const fpd = {}; const context = {} const user = {}; - const ortbData = config.getLegacyFpd(config.getConfig('ortb2')) || {}; + const ortbData = config.getConfig('ortb2') || {}; - const fpdContext = Object.assign({}, ortbData.context); + const fpdContext = Object.assign({}, ortbData.site); const fpdUser = Object.assign({}, ortbData.user); _addEntries(context, fpdContext); diff --git a/modules/trustpidSystem.js b/modules/trustpidSystem.js index 74b522e8f1a..27ca4bf6340 100644 --- a/modules/trustpidSystem.js +++ b/modules/trustpidSystem.js @@ -4,7 +4,7 @@ * @module modules/trustpidSystem * @requires module:modules/userId */ -import { logInfo, logError } from '../src/utils.js'; +import { logInfo } from '../src/utils.js'; import { submodule } from '../src/hook.js'; import { getStorageManager } from '../src/storageManager.js'; @@ -13,7 +13,7 @@ const LOG_PREFIX = 'Trustpid module' let mnoAcronym = ''; let mnoDomain = ''; -export const storage = getStorageManager(null, MODULE_NAME); +export const storage = getStorageManager({gvlid: null, moduleName: MODULE_NAME}); /** * Handle an event for an iframe. @@ -22,10 +22,9 @@ export const storage = getStorageManager(null, MODULE_NAME); * @param event */ function messageHandler(event) { - let msg; try { - if (event && event.data && typeof event.data === 'string' && event.data) { - msg = JSON.parse(event.data); + if (event && event.data && typeof event.data === 'string') { + const msg = JSON.parse(event.data); if (msg.msgType === 'MNOSELECTOR' && msg.body && msg.body.url) { let URL = msg.body.url.split('//'); let domainURL = URL[1].split('/'); @@ -35,7 +34,7 @@ function messageHandler(event) { } } } catch (e) { - logError(e); + logInfo(`${LOG_PREFIX}: Unsupported message caught. Origin: ${event.origin}, data: ${event.data}.`); } } @@ -44,31 +43,13 @@ function messageHandler(event) { * @param domain */ function getDomainAcronym(domain) { - let acronym = ''; const prefix = '-'; - switch (domain) { - case 'tmi.mno.link': - acronym = 'ndye'; - break; - case 'tmi.vodafone.de': - acronym = 'pqnx'; - break; - case 'tmi.telekom.de': - acronym = 'avgw'; - break; - case 'tmi.tmid.es': - acronym = 'kjws'; - break; - case 'uat.mno.link': - acronym = 'xxxx'; - break; - case 'es.tmiservice.orange.com': - acronym = 'aplw'; - break; - default: - return 'none'; + const acronym = window.FC_CONF?.TELCO_ACRONYM?.[domain]; + if (!acronym) { + logInfo(`${LOG_PREFIX}: No acronym found for domain: ${domain}`); + return; } - return mnoAcronym = prefix + acronym; + mnoAcronym = prefix + acronym; } // Set a listener to handle the iframe response message. diff --git a/modules/trustpidSystem.md b/modules/trustpidSystem.md index c4309c9d807..c1ad1ab567b 100644 --- a/modules/trustpidSystem.md +++ b/modules/trustpidSystem.md @@ -5,41 +5,18 @@ trustpid User Id Module. First, make sure to add the trustpid submodule to your Prebid.js package with: ``` -gulp build --modules=userId,adfBidAdapter,trustpidSystem -``` - -The following configuration parameters are available: - -``` -pbjs.setConfig({ - userSync: { - userIds: [ - { - name: 'trustpid', - params: { - maxDelayTime: 1000, - }, - bidders: ["adf"], - storage: { - type: "html5", - name: "trustpid", - expires: 1, //days - }, - } - ], - } -}); +gulp build --modules=userId,adfBidAdapter,ixBidAdapter,prebidServerBidAdapter,trustpidSystem ``` ## Parameter Descriptions -| Param under userSync.userIds[] | Scope | Type | Description | Example | -| --- | --- | --- | --- | --- | -| name | Required | String | The name of the module | `"trustpid"` -| params | Required | Object | Object with configuration parameters for trustpid User Id submodule | - | -| params.maxDelayTime | Required | Integer | Max amount of time (in seconds) before looking into storage for data | 2500 | -| bidders | Required | Array of Strings | An array of bidder codes to which this user ID may be sent. Currently required and supporting AdformOpenRTB | `["adf"]` | -| storage | Required | Object | Local storage configuration object | - | -| storage.type | Required | String | Type of the storage that would be used to store user ID. Must be `"html5"` to utilise HTML5 local storage. | `"html5"` | -| storage.name | Required | String | The name of the key in local storage where the user ID will be stored. | `"trustpid"` | -| storage.expires | Required | Integer | How long (in days) the user ID information will be stored. For safety reasons, this information is required.| `1` | \ No newline at end of file +| Params under userSync.userIds[] | Type | Description | Example | +| --- | --- | --- | --- | +| name | String | The name of the module | `"trustpid"` | +| params | Object | Object with configuration parameters for trustpid User Id submodule | - | +| params.maxDelayTime | Integer | Max amount of time (in seconds) before looking into storage for data | 2500 | +| bidders | Array of Strings | An array of bidder codes to which this user ID may be sent. Currently required and supporting AdformOpenRTB | [`"adf"`, `"adformPBS"`, `"ix"`] | +| storage | Object | Local storage configuration object | - | +| storage.type | String | Type of the storage that would be used to store user ID. Must be `"html5"` to utilise HTML5 local storage. | `"html5"` | +| storage.name | String | The name of the key in local storage where the user ID will be stored. | `"trustpid"` | +| storage.expires | Integer | How long (in days) the user ID information will be stored. For safety reasons, this information is required.| `1` | \ No newline at end of file diff --git a/modules/trustxBidAdapter.js b/modules/trustxBidAdapter.js index 70cae0f50f1..7d40a0b0452 100644 --- a/modules/trustxBidAdapter.js +++ b/modules/trustxBidAdapter.js @@ -1,4 +1,4 @@ -import { isEmpty, deepAccess, logError, logWarn, parseGPTSingleSizeArrayToRtbSize } from '../src/utils.js'; +import {isEmpty, deepAccess, logError, logWarn, parseGPTSingleSizeArrayToRtbSize, mergeDeep} from '../src/utils.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; import { Renderer } from '../src/Renderer.js'; import { VIDEO, BANNER } from '../src/mediaTypes.js'; @@ -21,6 +21,7 @@ const LOG_ERROR_MESS = { hasEmptySeatbidArray: 'Response has empty seatbid array', hasNoArrayOfBids: 'Seatbid from response has no array of bid objects - ' }; + export const spec = { code: BIDDER_CODE, supportedMediaTypes: [ BANNER, VIDEO ], @@ -173,16 +174,25 @@ export const spec = { request.site.content = content; } - const userData = []; - addSegments('iow_labs_pub_data', 'jwpseg', jwpseg, userData); - addSegments('permutive', 'p_standard', permutiveseg, userData, 'permutive.com'); - - if (userData.length) { + if (jwpseg && jwpseg.length) { user = { - data: userData + data: [{ + name: 'iow_labs_pub_data', + segment: segmentProcessing(jwpseg, 'jwpseg'), + }] }; } + const ortb2UserData = config.getConfig('ortb2.user.data'); + if (ortb2UserData && ortb2UserData.length) { + if (!user) { + user = { data: [] }; + } + user = mergeDeep(user, { + data: [...ortb2UserData] + }); + } + if (gdprConsent && gdprConsent.consentString) { userExt = {consent: gdprConsent.consentString}; } @@ -431,34 +441,20 @@ function createBannerRequest(bid, mediaType) { return result; } -function addSegments(name, segName, segments, data, bidConfigName) { - if (segments && segments.length) { - data.push({ - name: name, - segment: segments - .map((seg) => seg && (seg.id || seg)) - .filter((seg) => seg && (typeof seg === 'string' || typeof seg === 'number')) - .map((seg) => ({ name: segName, value: seg.toString() })) - }); - } else if (bidConfigName) { - const configData = config.getConfig('ortb2.user.data'); - let segData = null; - configData && configData.some(({name, segment}) => { - if (name === bidConfigName) { - segData = segment; - return true; +function segmentProcessing(segment, forceSegName) { + return segment + .map((seg) => { + const value = seg && (seg.value || seg.id || seg); + if (typeof value === 'string' || typeof value === 'number') { + return { + value: value.toString(), + ...(forceSegName && { name: forceSegName }), + ...(seg.name && { name: seg.name }), + }; } - }); - if (segData && segData.length) { - data.push({ - name: name, - segment: segData - .map((seg) => seg && (seg.id || seg)) - .filter((seg) => seg && (typeof seg === 'string' || typeof seg === 'number')) - .map((seg) => ({ name: segName, value: seg.toString() })) - }); - } - } + return null; + }) + .filter((seg) => !!seg); } function reformatKeywords(pageKeywords) { diff --git a/modules/ttdBidAdapter.js b/modules/ttdBidAdapter.js new file mode 100644 index 00000000000..4919442336f --- /dev/null +++ b/modules/ttdBidAdapter.js @@ -0,0 +1,519 @@ +import * as utils from '../src/utils.js'; +import { config } from '../src/config.js'; +import { createEidsArray } from './userId/eids.js'; +import { registerBidder } from '../src/adapters/bidderFactory.js'; +import { BANNER, VIDEO } from '../src/mediaTypes.js'; + +const BIDADAPTERVERSION = 'TTD-PREBID-2022.02.18'; +const BIDDER_CODE = 'ttd'; +const BIDDER_CODE_LONG = 'thetradedesk'; +const BIDDER_ENDPOINT = 'https://direct.adsrvr.org/bid/bidder/'; +const USER_SYNC_ENDPOINT = 'https://match.adsrvr.org'; + +const MEDIA_TYPE = { + BANNER: 1, + VIDEO: 2 +}; + +function getExt(firstPartyData) { + const ext = { + ver: BIDADAPTERVERSION, + pbjs: '$prebid.version$', + keywords: firstPartyData.site?.keywords ? firstPartyData.site.keywords.split(',').map(k => k.trim()) : [] + } + return { + ttdprebid: ext + }; +} + +function getRegs(bidderRequest) { + let regs = {}; + + if (bidderRequest.gdprConsent && typeof bidderRequest.gdprConsent.gdprApplies === 'boolean') { + utils.deepSetValue(regs, 'ext.gdpr', bidderRequest.gdprConsent.gdprApplies ? 1 : 0); + } + if (bidderRequest.uspConsent) { + utils.deepSetValue(regs, 'ext.us_privacy', bidderRequest.uspConsent); + } + if (config.getConfig('coppa') === true) { + regs.coppa = 1; + } + return regs; +} + +function getBidFloor(bid) { + if (!utils.isFn(bid.getFloor)) { + return null; + } + + let floor = bid.getFloor({ + currency: 'USD', + mediaType: '*', + size: '*' + }); + if (utils.isPlainObject(floor) && !isNaN(floor.floor) && floor.currency === 'USD') { + return floor.floor; + } + return null; +} + +function getSource(validBidRequests) { + let source = {}; + if (validBidRequests[0].schain) { + utils.deepSetValue(source, 'ext.schain', validBidRequests[0].schain); + } + return source; +} + +function getDevice() { + const language = navigator.language || navigator.browserLanguage || navigator.userLanguage || navigator.systemLanguage; + let device = { + ua: navigator.userAgent, + dnt: utils.getDNT() ? 1 : 0, + language: language, + connectiontype: getConnectionType() + }; + + return device; +}; + +function getConnectionType() { + const connection = navigator.connection || navigator.webkitConnection; + if (!connection) { + return 0; + } + switch (connection.type) { + case 'ethernet': + return 1; + case 'wifi': + return 2; + case 'cellular': + switch (connection.effectiveType) { + case 'slow-2g': + case '2g': + return 4; + case '3g': + return 5; + case '4g': + return 6; + default: + return 3; + } + default: + return 0; + } +} + +function getUser(bidderRequest) { + let user = {}; + if (bidderRequest.gdprConsent) { + utils.deepSetValue(user, 'ext.consent', bidderRequest.gdprConsent.consentString); + } + + if (utils.isStr(utils.deepAccess(bidderRequest, 'bids.0.userId.tdid'))) { + user.buyeruid = bidderRequest.bids[0].userId.tdid; + } + + var eids = createEidsArray(utils.deepAccess(bidderRequest, 'bids.0.userId')) + if (eids.length) { + utils.deepSetValue(user, 'ext.eids', eids); + } + + return user; +} + +function getSite(bidderRequest, firstPartyData) { + var site = { + id: utils.deepAccess(bidderRequest, 'bids.0.params.siteId'), + page: utils.deepAccess(bidderRequest, 'refererInfo.referer'), + publisher: { + id: utils.deepAccess(bidderRequest, 'bids.0.params.publisherId'), + }, + ...firstPartyData.site + }; + + var publisherDomain = config.getConfig('publisherDomain'); + if (publisherDomain) { + utils.deepSetValue(site, 'publisher.domain', publisherDomain); + } + return site; +} + +function getImpression(bidRequest) { + let impression = { + id: bidRequest.bidId, + tagid: bidRequest.params.placementId + }; + + let gpid = utils.deepAccess(bidRequest, 'ortb2Imp.ext.gpid'); + if (gpid) { + impression.ext = { + gpid: gpid + } + } + + const mediaTypesVideo = utils.deepAccess(bidRequest, 'mediaTypes.video'); + const mediaTypesBanner = utils.deepAccess(bidRequest, 'mediaTypes.banner'); + + let mediaTypes = {}; + if (mediaTypesBanner) { + mediaTypes[BANNER] = banner(bidRequest); + } + if (mediaTypesVideo) { + mediaTypes[VIDEO] = video(bidRequest); + } + + Object.assign(impression, mediaTypes); + + let bidfloor = getBidFloor(bidRequest); + if (bidfloor) { + impression.bidfloor = parseFloat(bidfloor); + impression.bidfloorcur = 'USD'; + } + + return impression; +} + +function getSizes(sizes) { + const sizeStructs = utils.parseSizesInput(sizes) + .filter(x => x) // sizes that don't conform are returned as null, which we want to ignore + .map(x => x.split('x')) + .map(size => { + return { + width: parseInt(size[0]), + height: parseInt(size[1]), + } + }); + + return sizeStructs; +} + +function banner(bid) { + const sizes = getSizes(bid.mediaTypes.banner.sizes).map(x => { + return { + w: x.width, + h: x.height, + } + }); + const pos = parseInt(utils.deepAccess(bid, 'mediaTypes.banner.pos')); + const expdir = utils.deepAccess(bid, 'params.banner.expdir'); + let optionalParams = {}; + if (pos) { + optionalParams.pos = pos; + } + if (expdir && Array.isArray(expdir)) { + optionalParams.expdir = expdir; + } + + const banner = Object.assign( + { + w: sizes[0].w, + h: sizes[0].h, + format: sizes, + }, + optionalParams); + return banner; +} + +function video(bid) { + let minduration = utils.deepAccess(bid, 'mediaTypes.video.minduration'); + const maxduration = utils.deepAccess(bid, 'mediaTypes.video.maxduration'); + const playerSize = utils.deepAccess(bid, 'mediaTypes.video.playerSize'); + const api = utils.deepAccess(bid, 'mediaTypes.video.api'); + const mimes = utils.deepAccess(bid, 'mediaTypes.video.mimes'); + const placement = utils.deepAccess(bid, 'mediaTypes.video.placement'); + const protocols = utils.deepAccess(bid, 'mediaTypes.video.protocols'); + const playbackmethod = utils.deepAccess(bid, 'mediaTypes.video.playbackmethod'); + const pos = utils.deepAccess(bid, 'mediaTypes.video.pos'); + const startdelay = utils.deepAccess(bid, 'mediaTypes.video.startdelay'); + const skip = utils.deepAccess(bid, 'mediaTypes.video.skip'); + const skipmin = utils.deepAccess(bid, 'mediaTypes.video.skipmin'); + const skipafter = utils.deepAccess(bid, 'mediaTypes.video.skipafter'); + const minbitrate = utils.deepAccess(bid, 'mediaTypes.video.minbitrate'); + const maxbitrate = utils.deepAccess(bid, 'mediaTypes.video.maxbitrate'); + + if (!minduration || !utils.isInteger(minduration)) { + minduration = 0 + } + let video = { + minduration: minduration, + maxduration: maxduration, + api: api, + mimes: mimes, + placement: placement, + protocols: protocols + }; + + if (typeof playerSize !== 'undefined') { + if (utils.isArray(playerSize[0])) { + video.w = parseInt(playerSize[0][0]); + video.h = parseInt(playerSize[0][1]); + } else if (utils.isNumber(playerSize[0])) { + video.w = parseInt(playerSize[0]); + video.h = parseInt(playerSize[1]); + } + } + + if (playbackmethod) { + video.playbackmethod = playbackmethod; + } + if (pos) { + video.pos = pos; + } + if (startdelay && utils.isInteger(startdelay)) { + video.startdelay = startdelay; + } + if (skip && (skip === 0 || skip === 1)) { + video.skip = skip; + } + if (skipmin && utils.isInteger(skipmin)) { + video.skipmin = skipmin; + } + if (skipafter && utils.isInteger(skipafter)) { + video.skipafter = skipafter; + } + if (minbitrate && utils.isInteger(minbitrate)) { + video.minbitrate = minbitrate; + } + if (maxbitrate && utils.isInteger(maxbitrate)) { + video.maxbitrate = maxbitrate; + } + + return video; +} + +export const spec = { + code: BIDDER_CODE, + gvlid: 21, + aliases: [BIDDER_CODE_LONG], + supportedMediaTypes: [BANNER, VIDEO], + + /** + * Determines whether or not the given bid request is valid. + * + * @param {BidRequest} bid The bid params to validate. + * @return boolean True if this is a valid bid, and false otherwise. + */ + isBidRequestValid: function (bid) { + const alphaRegex = /^[\w+]+$/; + + // required parameters + if (!bid || !bid.params) { + utils.logWarn(BIDDER_CODE + ': Missing bid parameters'); + return false; + } + if (!bid.params.supplySourceId) { + utils.logWarn(BIDDER_CODE + ': Missing required parameter params.supplySourceId'); + return false; + } + if (!alphaRegex.test(bid.params.supplySourceId)) { + utils.logWarn(BIDDER_CODE + ': supplySourceId must only contain alphabetic characters'); + return false; + } + if (!bid.params.publisherId) { + utils.logWarn(BIDDER_CODE + ': Missing required parameter params.publisherId'); + return false; + } + if (bid.params.publisherId.length > 32) { + utils.logWarn(BIDDER_CODE + ': params.publisherId must be 32 characters or less'); + return false; + } + if (!bid.params.siteId) { + utils.logWarn(BIDDER_CODE + ': Missing required parameter params.siteId'); + return false; + } + if (bid.params.siteId.length > 50) { + utils.logWarn(BIDDER_CODE + ': params.siteId must be 50 characters or less'); + return false; + } + if (!bid.params.placementId) { + utils.logWarn(BIDDER_CODE + ': Missing required parameter params.placementId'); + return false; + } + if (bid.params.placementId.length > 128) { + utils.logWarn(BIDDER_CODE + ': params.placementId must be 128 characters or less'); + return false; + } + + const mediaTypesBanner = utils.deepAccess(bid, 'mediaTypes.banner'); + const mediaTypesVideo = utils.deepAccess(bid, 'mediaTypes.video'); + + if (!mediaTypesBanner && !mediaTypesVideo) { + utils.logWarn(BIDDER_CODE + ': one of mediaTypes.banner or mediaTypes.video must be passed'); + return false; + } + + if (mediaTypesVideo) { + if (!mediaTypesVideo.maxduration || !utils.isInteger(mediaTypesVideo.maxduration)) { + utils.logWarn(BIDDER_CODE + ': mediaTypes.video.maxduration must be set to the maximum video ad duration in seconds'); + return false; + } + if (!mediaTypesVideo.api || mediaTypesVideo.api.length === 0) { + utils.logWarn(BIDDER_CODE + ': mediaTypes.video.api should be an array of supported api frameworks. See the Open RTB v2.5 spec for valid values'); + return false; + } + if (!mediaTypesVideo.mimes || mediaTypesVideo.mimes.length === 0) { + utils.logWarn(BIDDER_CODE + ': mediaTypes.video.mimes should be an array of supported mime types'); + return false; + } + if (!mediaTypesVideo.protocols) { + utils.logWarn(BIDDER_CODE + ': mediaTypes.video.protocols should be an array of supported protocols. See the Open RTB v2.5 spec for valid values') + return false; + } + } + + return true; + }, + + /** + * Make a server request from the list of BidRequests. + * + * @param {BidRequest[]} an array of validBidRequests + * @param {*} bidderRequest + * @return {ServerRequest} Info describing the request to the server. + */ + buildRequests: function (validBidRequests, bidderRequest) { + const firstPartyData = config.getConfig('ortb2') || {}; + let topLevel = { + id: bidderRequest.auctionId, + imp: validBidRequests.map(bidRequest => getImpression(bidRequest)), + site: getSite(bidderRequest, firstPartyData), + device: getDevice(), + user: getUser(bidderRequest), + at: 1, + cur: ['USD'], + regs: getRegs(bidderRequest), + source: getSource(validBidRequests), + ext: getExt(firstPartyData) + } + + let url = BIDDER_ENDPOINT + bidderRequest.bids[0].params.supplySourceId; + + let serverRequest = { + method: 'POST', + url: url, + data: topLevel, + options: { + withCredentials: true + } + }; + + return serverRequest; + }, + + /** + * Format responses as Prebid bid responses + * + * Each bid can have the following elements: + * - requestId (required) + * - cpm (required) + * - width (required) + * - height (required) + * - ad (required) + * - ttl (required) + * - creativeId (required) + * - netRevenue (required) + * - currency (required) + * - vastUrl + * - vastImpUrl + * - vastXml + * - dealId + * + * @param {ttdResponseObj} bidResponse A successful response from ttd. + * @param {ServerRequest} serverRequest The result of buildRequests() that lead to this response. + * @return {Bid[]} An array of formatted bids. + */ + interpretResponse: function (response, serverRequest) { + let seatBidsInResponse = utils.deepAccess(response, 'body.seatbid'); + const currency = utils.deepAccess(response, 'body.cur'); + if (!seatBidsInResponse || seatBidsInResponse.length === 0) { + return []; + } + let bidResponses = []; + let requestedImpressions = utils.deepAccess(serverRequest, 'data.imp'); + + seatBidsInResponse.forEach(seatBid => { + seatBid.bid.forEach(bid => { + let matchingRequestedImpression = requestedImpressions.find(imp => imp.id === bid.impid); + + const cpm = bid.price || 0; + let bidResponse = { + requestId: bid.impid, + cpm: cpm, + creativeId: bid.crid, + dealId: bid.dealid || null, + currency: currency || 'USD', + netRevenue: true, + ttl: bid.ttl || 360, + meta: {}, + }; + + if (bid.adomain && bid.adomain.length > 0) { + bidResponse.meta.advertiserDomains = bid.adomain; + } + + if (bid.ext.mediatype === MEDIA_TYPE.BANNER) { + Object.assign( + bidResponse, + { + width: bid.w, + height: bid.h, + ad: utils.replaceAuctionPrice(bid.adm, cpm), + mediaType: BANNER + } + ); + } else if (bid.ext.mediatype === MEDIA_TYPE.VIDEO) { + Object.assign( + bidResponse, + { + width: matchingRequestedImpression.video.w, + height: matchingRequestedImpression.video.h, + mediaType: VIDEO + } + ); + if (bid.nurl) { + bidResponse.vastUrl = utils.replaceAuctionPrice(bid.nurl, cpm); + } else { + bidResponse.vastXml = utils.replaceAuctionPrice(bid.adm, cpm); + } + } + + 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. + * @param {gdprConsent} gdprConsent GDPR consent object + * @param {uspConsent} uspConsent USP consent object + * @return {UserSync[]} The user syncs which should be dropped. + */ + getUserSyncs: function(syncOptions, serverResponses, gdprConsent = {}, uspConsent = '') { + const syncs = []; + + let gdprParams = `&gdpr=${gdprConsent.gdprApplies ? 1 : 0}&gdpr_consent=${encodeURIComponent(gdprConsent.consentString)}`; + + let url = `${USER_SYNC_ENDPOINT}/track/usersync?us_privacy=${encodeURIComponent(uspConsent)}${gdprParams}`; + + if (syncOptions.pixelEnabled) { + syncs.push({ + type: 'image', + url: url + '&ust=image' + }); + } else if (syncOptions.iframeEnabled) { + syncs.push({ + type: 'iframe', + url: url + '&ust=iframe' + }); + } + return syncs; + }, +}; + +registerBidder(spec) diff --git a/modules/ttdBidAdapter.md b/modules/ttdBidAdapter.md new file mode 100644 index 00000000000..9c67f3267cb --- /dev/null +++ b/modules/ttdBidAdapter.md @@ -0,0 +1,122 @@ +# Overview + +``` +Module Name: The Trade Desk Bidder Adapter +Module Type: Bidder Adapter +Maintainer: prebid-maintainers@thetradedesk.com +``` + +# Description + +Module that connects to The Trade Desk's demand sources to fetch bids. + +The Trade Desk bid adapter supports Banner and Video. + +# Test Parameters + +```js + var adUnits = [ + // Banner adUnit with only required parameters + { + code: 'test-div-minimal', + mediaTypes: { + banner: { + sizes: [[300, 250]] + } + }, + bids: [ + { + bidder: 'ttd', + params: { + supplySourceId: 'supplier', + publisherId: '1427ab10f2e448057ed3b422', + siteId: 'site-123', + placementId: 'footer1' + } + } + ] + }, + // Banner adUnit with all optional parameters provided + { + code: 'test-div-banner-optional-params', + mediaTypes: { + banner: { + sizes: [[728, 90]], + pos: 1 + } + }, + bids: [ + { + bidder: 'ttd', + params: { + supplySourceId: 'supplier', + publisherId: '1427ab10f2e448057ed3b422', + siteId: 'site-123', + placementId: 'footer1', + banner: { + expdir: [1, 3] + }, + } + } + ] + }, + // Video adUnit with only required parameters + { + code: 'test-div-video-minimal', + mediaTypes: { + video: { + maxduration: 30, + api: [1, 3], + mimes: ['video/mp4'], + placement: 3, + protocols: [2,3,5,6] + } + }, + bids: [ + { + bidder: 'ttd', + params: { + supplySourceId: 'supplier', + publisherId: '1427ab10f2e448057ed3b422', + siteId: 'site-123', + placementId: 'footer1' + } + } + ] + }, + // Video adUnit with all optional parameters provided + { + code: 'test-div-video-full', + mediaTypes: { + video: { + minduration: 1, + maxduration: 10, + playerSize: [640, 480], + api: [1, 3], + mimes: ['video/mp4'], + placement: 3, + protocols: [2, 3, 5, 6], + startdelay: 1, + playbackmethod: [1], + pos: 1, + minbitrate: 100, + maxbitrate: 500, + skip: 1, + skipmin: 5, + skipafter: 10 + } + }, + bids: [ + { + bidder: 'ttd', + params: { + supplySourceId: 'supplier', + publisherId: '1427ab10f2e448057ed3b422', + siteId: 'site-123', + placementId: 'footer1' + } + } + ] + } + ]; +``` diff --git a/modules/ucfunnelBidAdapter.js b/modules/ucfunnelBidAdapter.js index 8b85f1ebad3..ec087d005d6 100644 --- a/modules/ucfunnelBidAdapter.js +++ b/modules/ucfunnelBidAdapter.js @@ -3,7 +3,7 @@ import {registerBidder} from '../src/adapters/bidderFactory.js'; import {BANNER, VIDEO, NATIVE} from '../src/mediaTypes.js'; import { getStorageManager } from '../src/storageManager.js'; import { config } from '../src/config.js'; -const storage = getStorageManager(); + const COOKIE_NAME = 'ucf_uid'; const VER = 'ADGENT_PREBID-2018011501'; const BIDDER_CODE = 'ucfunnel'; @@ -13,6 +13,7 @@ const VIDEO_CONTEXT = { INSTREAM: 0, OUSTREAM: 2 } +const storage = getStorageManager({bidderCode: BIDDER_CODE}); export const spec = { code: BIDDER_CODE, diff --git a/modules/uid2IdSystem.js b/modules/uid2IdSystem.js index c0cd9166784..23656639532 100644 --- a/modules/uid2IdSystem.js +++ b/modules/uid2IdSystem.js @@ -23,7 +23,7 @@ function readFromLocalStorage() { } function getStorage() { - return getStorageManager(GVLID, MODULE_NAME); + return getStorageManager({gvlid: GVLID, moduleName: MODULE_NAME}); } const storage = getStorage(); diff --git a/modules/unicornBidAdapter.js b/modules/unicornBidAdapter.js index 0209c808979..977e694acf7 100644 --- a/modules/unicornBidAdapter.js +++ b/modules/unicornBidAdapter.js @@ -3,12 +3,12 @@ import {BANNER} from '../src/mediaTypes.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; import {getStorageManager} from '../src/storageManager.js'; -const storage = getStorageManager(); const BIDDER_CODE = 'unicorn'; const UNICORN_ENDPOINT = 'https://ds.uncn.jp/pb/0/bid.json'; const UNICORN_DEFAULT_CURRENCY = 'JPY'; const UNICORN_PB_COOKIE_KEY = '__pb_unicorn_aud'; const UNICORN_PB_VERSION = '1.1'; +const storage = getStorageManager({bidderCode: BIDDER_CODE}); /** * Placement ID and Account ID are required. diff --git a/modules/unrulyBidAdapter.js b/modules/unrulyBidAdapter.js index 99fbe63aeb4..77160bb01e6 100644 --- a/modules/unrulyBidAdapter.js +++ b/modules/unrulyBidAdapter.js @@ -193,6 +193,7 @@ const isBannerMediaTypeValid = (mediaTypeBannerData) => { export const adapter = { code: 'unruly', supportedMediaTypes: [VIDEO, BANNER], + gvlid: 36, isBidRequestValid: function (bid) { let siteId = deepAccess(bid, 'params.siteId'); let isBidValid = siteId && isMediaTypesValid(bid); diff --git a/modules/userId/eids.js b/modules/userId/eids.js index e78cffdcbac..9c995a52fe3 100644 --- a/modules/userId/eids.js +++ b/modules/userId/eids.js @@ -1,7 +1,7 @@ import { pick, isFn, isStr, isPlainObject, deepAccess } from '../../src/utils.js'; // Each user-id sub-module is expected to mention respective config here -const USER_IDS_CONFIG = { +export const USER_IDS_CONFIG = { // key-name : {config} @@ -63,6 +63,20 @@ const USER_IDS_CONFIG = { } }, + // ftrack + 'ftrackId': { + source: 'flashtalking.com', + atype: 1, + getValue: function(data) { + return data.uid + }, + getUidExt: function(data) { + if (data.ext) { + return data.ext; + } + } + }, + // parrableId 'parrableId': { source: 'parrable.com', @@ -285,6 +299,21 @@ const USER_IDS_CONFIG = { source: 'adquery.io', atype: 1 }, + + // DAC ID + 'dacId': { + source: 'impact-ad.jp', + atype: 1 + }, + + // 33across ID + '33acrossId': { + source: '33across.com', + atype: 1, + getValue: function(data) { + return data.envelope; + } + }, }; // this function will create an eid object for the given UserId sub-module diff --git a/modules/userId/eids.md b/modules/userId/eids.md index 4c516d5441c..45237ac5f26 100644 --- a/modules/userId/eids.md +++ b/modules/userId/eids.md @@ -2,6 +2,13 @@ ``` userIdAsEids = [ + { + source: '33across.com', + uids: [{ + id: 'some-random-id-value', + atype: 1 + }] + }, { source: 'trustpid.com', uids: [{ @@ -65,6 +72,13 @@ userIdAsEids = [ }] }, + { + source: 'flashtalking.com', + uids: [{ + id: 'some-random-id-value', + atype: 1 + }, + { source: 'parrable.com', uids: [{ diff --git a/modules/userId/index.js b/modules/userId/index.js index 42fff2cd16c..809ca624748 100644 --- a/modules/userId/index.js +++ b/modules/userId/index.js @@ -125,25 +125,34 @@ * @property {(function|undefined)} callback - function that will return an id */ -/** - * @typedef {Object} RefreshUserIdsOptions - * @property {(string[]|undefined)} submoduleNames - submodules to refresh - */ - -import find from 'core-js-pure/features/array/find.js'; -import { config } from '../../src/config.js'; -import events from '../../src/events.js'; -import { getGlobal } from '../../src/prebidGlobal.js'; -import { gdprDataHandler } from '../../src/adapterManager.js'; +import {find, includes} from '../../src/polyfill.js'; +import {config} from '../../src/config.js'; +import * as events from '../../src/events.js'; +import {getGlobal} from '../../src/prebidGlobal.js'; +import {gdprDataHandler} from '../../src/adapterManager.js'; import CONSTANTS from '../../src/constants.json'; -import { module, hook } from '../../src/hook.js'; -import { createEidsArray, buildEidPermissions } from './eids.js'; -import { getCoreStorageManager } from '../../src/storageManager.js'; +import {hook, module, ready as hooksReady} from '../../src/hook.js'; +import {buildEidPermissions, createEidsArray, USER_IDS_CONFIG} from './eids.js'; +import {getCoreStorageManager} from '../../src/storageManager.js'; import { - getPrebidInternal, isPlainObject, logError, isArray, cyrb53Hash, deepAccess, timestamp, delayExecution, logInfo, isFn, - logWarn, isEmptyStr, isNumber, isGptPubadsDefined + cyrb53Hash, + deepAccess, + delayExecution, + getPrebidInternal, + isArray, + isEmptyStr, + isFn, + isGptPubadsDefined, + isNumber, + isPlainObject, + logError, + logInfo, + logWarn, + timestamp, + isEmpty } from '../../src/utils.js'; -import includes from 'core-js-pure/features/array/includes.js'; +import {getPPID as coreGetPPID} from '../../src/adserver.js'; +import {promiseControls} from '../../src/utils/promise.js'; const MODULE_NAME = 'User ID'; const COOKIE = 'cookie'; @@ -187,6 +196,8 @@ export let auctionDelay; /** @type {(string|undefined)} */ let ppidSource; +let configListener; + /** @param {Submodule[]} submodules */ export function setSubmoduleRegistry(submodules) { submoduleRegistry = submodules; @@ -414,7 +425,7 @@ function processSubmoduleCallbacks(submodules, cb) { }, submodules.length); } submodules.forEach(function (submodule) { - submodule.callback(function callbackCompleted(idObj) { + function callbackCompleted(idObj) { // if valid, id data should be saved to cookie/html storage if (idObj) { if (submodule.config.storage) { @@ -426,8 +437,13 @@ function processSubmoduleCallbacks(submodules, cb) { logInfo(`${MODULE_NAME}: ${submodule.submodule.name} - request id responded with an empty value`); } done(); - }); - + } + try { + submodule.callback(callbackCompleted); + } catch (e) { + logError(`Error in userID module '${submodule.submodule.name}':`, e); + done(); + } // clear callback, this prop is used to test if all submodule callbacks are complete below submodule.callback = undefined; }); @@ -451,6 +467,20 @@ function getCombinedSubmoduleIds(submodules) { return combinedSubmoduleIds; } +/** + * This function will return a submodule ID object for particular source name + * @param {SubmoduleContainer[]} submodules + * @param {string} sourceName + */ +function getSubmoduleId(submodules, sourceName) { + if (!Array.isArray(submodules) || !submodules.length) { + return {}; + } + const submodule = submodules.filter(sub => isPlainObject(sub.idObj) && + Object.keys(sub.idObj).length && USER_IDS_CONFIG[Object.keys(sub.idObj)[0]]?.source === sourceName); + return !isEmpty(submodule) ? submodule[0].idObj : []; +} + /** * This function will create a combined object for bidder with allowed subModule Ids * @param {SubmoduleContainer[]} submodules @@ -493,56 +523,111 @@ function addIdDataToAdUnitBids(adUnits, submodules) { }); } -/** - * This is a common function that will initialize subModules if not already done and it will also execute subModule callbacks - */ -function initializeSubmodulesAndExecuteCallbacks(continueAuction) { - let delayed = false; +function delayFor(ms) { + return new Promise((resolve) => setTimeout(resolve, ms)); +} - // initialize submodules only when undefined - if (typeof initializedSubmodules === 'undefined') { - initializedSubmodules = initSubmodules(submodules, gdprDataHandler.getConsentData()); - if (initializedSubmodules.length) { - setPrebidServerEidPermissions(initializedSubmodules); - // list of submodules that have callbacks that need to be executed - const submodulesWithCallbacks = initializedSubmodules.filter(item => isFn(item.callback)); +const INIT_CANCELED = {}; - if (submodulesWithCallbacks.length) { - if (continueAuction && auctionDelay > 0) { - // delay auction until ids are available - delayed = true; - let continued = false; - const continueCallback = function () { - if (!continued) { - continued = true; - continueAuction(); - } - } - logInfo(`${MODULE_NAME} - auction delayed by ${auctionDelay} at most to fetch ids`); - - timeoutID = setTimeout(continueCallback, auctionDelay); - processSubmoduleCallbacks(submodulesWithCallbacks, continueCallback); - } else { - // wait for auction complete before processing submodule callbacks - events.on(CONSTANTS.EVENTS.AUCTION_END, function auctionEndHandler() { - events.off(CONSTANTS.EVENTS.AUCTION_END, auctionEndHandler); - - // when syncDelay is zero, process callbacks now, otherwise delay process with a setTimeout - if (syncDelay > 0) { - setTimeout(function () { - processSubmoduleCallbacks(submodulesWithCallbacks); - }, syncDelay); - } else { - processSubmoduleCallbacks(submodulesWithCallbacks); - } - }); - } +function idSystemInitializer({delay = delayFor} = {}) { + const startInit = promiseControls(); + const startCallbacks = promiseControls(); + let cancel; + let initialized = false; + + function cancelAndTry(promise) { + if (cancel != null) { + cancel.reject(INIT_CANCELED); + } + cancel = promiseControls(); + return Promise.race([promise, cancel.promise]); + } + + // grab a reference to global vars so that the promise chains remain isolated; + // multiple calls to `init` (from tests) might otherwise cause them to interfere with each other + let initModules = initializedSubmodules; + let allModules = submodules; + + function checkRefs(fn) { + // unfortunately tests have their own global state that needs to be guarded, so even if we keep ours tidy, + // we cannot let things like submodule callbacks run (they pollute things like the global `server` XHR mock) + return function(...args) { + if (initModules === initializedSubmodules && allModules === submodules) { + return fn(...args); } } } - if (continueAuction && !delayed) { - continueAuction(); + let done = cancelAndTry( + Promise.all([hooksReady, startInit.promise]) + .then(() => gdprDataHandler.promise) + .then(checkRefs((consentData) => { + initSubmodules(initModules, allModules, consentData); + })) + .then(() => startCallbacks.promise) + .then(checkRefs(() => { + const modWithCb = initModules.filter(item => isFn(item.callback)); + if (modWithCb.length) { + return new Promise((resolve) => processSubmoduleCallbacks(modWithCb, resolve)); + } + })) + ); + + /** + * with `ready` = true, starts initialization; with `refresh` = true, reinitialize submodules (optionally + * filtered by `submoduleNames`). + */ + return function ({refresh = false, submoduleNames = null, ready = false} = {}) { + if (ready && !initialized) { + initialized = true; + startInit.resolve(); + // submodule callbacks should run immediately if `auctionDelay` > 0, or `syncDelay` ms after the + // auction ends otherwise + if (auctionDelay > 0) { + startCallbacks.resolve(); + } else { + events.on(CONSTANTS.EVENTS.AUCTION_END, function auctionEndHandler() { + events.off(CONSTANTS.EVENTS.AUCTION_END, auctionEndHandler); + delay(syncDelay).then(startCallbacks.resolve); + }); + } + } + if (refresh) { + done = cancelAndTry( + done + .catch(() => null) + .then(() => gdprDataHandler.promise) // fetch again in case a refresh was forced before this was resolved + .then(checkRefs((consentData) => { + const cbModules = initSubmodules( + initModules, + allModules.filter((sm) => submoduleNames == null || submoduleNames.includes(sm.submodule.name)), + consentData, + true + ).filter((sm) => { + return sm.callback != null; + }); + if (cbModules.length) { + return new Promise((resolve) => processSubmoduleCallbacks(cbModules, resolve)); + } + })) + ); + } + return done; + }; +} + +let initIdSystem; + +function getPPID() { + // userSync.ppid should be one of the 'source' values in getUserIdsAsEids() eg pubcid.org or id5-sync.com + const matchingUserId = ppidSource && (getUserIdsAsEids() || []).find(userID => userID.source === ppidSource); + if (matchingUserId && typeof deepAccess(matchingUserId, 'uids.0.id') === 'string') { + const ppidValue = matchingUserId.uids[0].id.replace(/[\W_]/g, ''); + if (ppidValue.length >= 32 && ppidValue.length <= 150) { + return ppidValue; + } else { + logWarn(`User ID - Googletag Publisher Provided ID for ${ppidSource} is not between 32 and 150 characters - ${ppidValue}`); + } } } @@ -555,28 +640,23 @@ function initializeSubmodulesAndExecuteCallbacks(continueAuction) { * @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) { - // initialize submodules only when undefined - initializeSubmodulesAndExecuteCallbacks(function () { +export function requestBidsHook(fn, reqBidsConfigObj, {delay = delayFor} = {}) { + Promise.race([ + getUserIdsAsync(), + delay(auctionDelay) + ]).then(() => { // pass available user id data to bid adapters addIdDataToAdUnitBids(reqBidsConfigObj.adUnits || getGlobal().adUnits, initializedSubmodules); - - // userSync.ppid should be one of the 'source' values in getUserIdsAsEids() eg pubcid.org or id5-sync.com - const matchingUserId = ppidSource && (getUserIdsAsEids() || []).find(userID => userID.source === ppidSource); - if (matchingUserId && typeof deepAccess(matchingUserId, 'uids.0.id') === 'string') { - const ppidValue = matchingUserId.uids[0].id.replace(/[\W_]/g, ''); - if (ppidValue.length >= 32 && ppidValue.length <= 150) { - if (isGptPubadsDefined()) { - window.googletag.pubads().setPublisherProvidedId(ppidValue); - } else { - window.googletag = window.googletag || {}; - window.googletag.cmd = window.googletag.cmd || []; - window.googletag.cmd.push(function() { - window.googletag.pubads().setPublisherProvidedId(ppidValue); - }); - } + const ppid = getPPID(); + if (ppid) { + if (isGptPubadsDefined()) { + window.googletag.pubads().setPublisherProvidedId(ppid); } else { - logWarn(`User ID - Googletag Publisher Provided ID for ${ppidSource} is not between 32 and 150 characters - ${ppidValue}`); + window.googletag = window.googletag || {}; + window.googletag.cmd = window.googletag.cmd || []; + window.googletag.cmd.push(function() { + window.googletag.pubads().setPublisherProvidedId(ppid); + }); } } @@ -590,9 +670,7 @@ export function requestBidsHook(fn, reqBidsConfigObj) { * Simple use case will be passing these UserIds to A9 wrapper solution */ function getUserIds() { - // initialize submodules only when undefined - initializeSubmodulesAndExecuteCallbacks(); - return getCombinedSubmoduleIds(initializedSubmodules); + return getCombinedSubmoduleIds(initializedSubmodules) } /** @@ -600,63 +678,115 @@ function getUserIds() { * Simple use case will be passing these UserIds to A9 wrapper solution */ function getUserIdsAsEids() { - // initialize submodules only when undefined - initializeSubmodulesAndExecuteCallbacks(); - return createEidsArray(getCombinedSubmoduleIds(initializedSubmodules)); + return createEidsArray(getUserIds()) } /** -* This function will be exposed in the global-name-space so that userIds can be refreshed after initialization. -* @param {RefreshUserIdsOptions} options -*/ -function refreshUserIds(options, callback) { - let submoduleNames = options ? options.submoduleNames : null; - if (!submoduleNames) { - submoduleNames = []; - } - - initializeSubmodulesAndExecuteCallbacks(function() { - let consentData = gdprDataHandler.getConsentData() - - // gdpr consent with purpose one is required, otherwise exit immediately - let {userIdModules, hasValidated} = validateGdprEnforcement(submodules, consentData); - if (!hasValidated && !hasGDPRConsent(consentData)) { - logWarn(`${MODULE_NAME} - gdpr permission not valid for local storage or cookies, exit module`); - return; - } + * This function will be exposed in global-name-space so that userIds stored by Prebid UserId module can be used by external codes as well. + * Simple use case will be passing these UserIds to A9 wrapper solution + */ - // we always want the latest consentData stored, even if we don't execute any submodules - const storedConsentData = getStoredConsentData(); - setStoredConsentData(consentData); +function getUserIdsAsEidBySource(sourceName) { + return createEidsArray(getSubmoduleId(initializedSubmodules, sourceName))[0]; +} - let callbackSubmodules = []; - for (let submodule of userIdModules) { - if (submoduleNames.length > 0 && - submoduleNames.indexOf(submodule.submodule.name) === -1) { - continue; +/** + * This function will be exposed in global-name-space so that userIds for a source can be exposed + * Sample use case is exposing this function to ESP + */ +function getEncryptedEidsForSource(source, encrypt, customFunction) { + return initIdSystem().then(() => { + let eidsSignals = {}; + + if (isFn(customFunction)) { + logInfo(`${MODULE_NAME} - Getting encrypted signal from custom function : ${customFunction.name} & source : ${source} `); + // Publishers are expected to define a common function which will be proxy for signal function. + const customSignals = customFunction(source); + eidsSignals[source] = customSignals ? encryptSignals(customSignals) : null; // by default encrypt using base64 to avoid JSON errors + } else { + // initialize signal with eids by default + const eid = getUserIdsAsEidBySource(source); + logInfo(`${MODULE_NAME} - Getting encrypted signal for eids :${JSON.stringify(eid)}`); + if (!isEmpty(eid)) { + eidsSignals[eid.source] = encrypt === true ? encryptSignals(eid) : eid.uids[0].id; // If encryption is enabled append version (1||) and encrypt entire object } + } + logInfo(`${MODULE_NAME} - Fetching encrypted eids: ${eidsSignals[source]}`); + return eidsSignals[source]; + }) +} - logInfo(`${MODULE_NAME} - refreshing ${submodule.submodule.name}`); - populateSubmoduleId(submodule, consentData, storedConsentData, true); - updateInitializedSubmodules(submodule); +function encryptSignals(signals, version = 1) { + let encryptedSig = ''; + switch (version) { + case 1: // Base64 Encryption + encryptedSig = typeof signals === 'object' ? window.btoa(JSON.stringify(signals)) : window.btoa(signals); // Test encryption. To be replaced with better algo + break; + default: + break; + } + return `${version}||${encryptedSig}`; +} - if (initializedSubmodules.length) { - setPrebidServerEidPermissions(initializedSubmodules); - } +/** +* This function will be exposed in the global-name-space so that publisher can register the signals-ESP. +*/ +function registerSignalSources() { + if (!isGptPubadsDefined()) { + return; + } + window.googletag.encryptedSignalProviders = window.googletag.encryptedSignalProviders || []; + const encryptedSignalSources = config.getConfig('userSync.encryptedSignalSources'); + if (encryptedSignalSources) { + const registerDelay = encryptedSignalSources.registerDelay || 0; + setTimeout(() => { + encryptedSignalSources['sources'] && encryptedSignalSources['sources'].forEach(({ source, encrypt, customFunc }) => { + source.forEach((src) => { + window.googletag.encryptedSignalProviders.push({ + id: src, + collectorFunction: () => getEncryptedEidsForSource(src, encrypt, customFunc) + }); + }); + }) + }, registerDelay) + } else { + logWarn(`${MODULE_NAME} - ESP : encryptedSignalSources config not defined under userSync Object`); + } +} - if (isFn(submodule.callback)) { - callbackSubmodules.push(submodule); +/** + * Force (re)initialization of ID submodules. + * + * This will force a refresh of the specified ID submodules regardless of `auctionDelay` / `syncDelay` settings, and + * return a promise that resolves to the same value as `getUserIds()` when the refresh is complete. + * If a refresh is already in progress, it will be canceled (rejecting promises returned by previous calls to `refreshUserIds`). + * + * @param submoduleNames? submodules to refresh. If omitted, refresh all submodules. + * @param callback? called when the refresh is complete + */ +function refreshUserIds({submoduleNames} = {}, callback) { + return initIdSystem({refresh: true, submoduleNames}) + .then(() => { + if (callback && isFn(callback)) { + callback(); } - } + return getUserIds(); + }); +} - if (callbackSubmodules.length > 0) { - processSubmoduleCallbacks(callbackSubmodules); - } +/** + * @returns a promise that resolves to the same value as `getUserIds()`, but only once all ID submodules have completed + * initialization. This can also be used to synchronize calls to other ID accessors, e.g. + * + * ``` + * pbjs.getUserIdsAsync().then(() => { + * const eids = pbjs.getUserIdsAsEids(); // guaranteed to be completely initialized at this point + * }); + * ``` + */ - if (callback) { - callback(); - } - }); +function getUserIdsAsync() { + return initIdSystem().then(() => getUserIds(), (e) => e === INIT_CANCELED ? getUserIdsAsync() : Promise.reject(e)); } /** @@ -717,12 +847,7 @@ function populateSubmoduleId(submodule, consentData, storedConsentData, forceRef } } -/** - * @param {SubmoduleContainer[]} submodules - * @param {ConsentData} consentData - * @returns {SubmoduleContainer[]} initialized submodules - */ -function initSubmodules(submodules, consentData) { +function initSubmodules(dest, submodules, consentData, forceRefresh = false) { // gdpr consent with purpose one is required, otherwise exit immediately let { userIdModules, hasValidated } = validateGdprEnforcement(submodules, consentData); if (!hasValidated && !hasGDPRConsent(consentData)) { @@ -734,25 +859,34 @@ function initSubmodules(submodules, consentData) { const storedConsentData = getStoredConsentData(); setStoredConsentData(consentData); - return userIdModules.reduce((carry, submodule) => { - populateSubmoduleId(submodule, consentData, storedConsentData, false); - carry.push(submodule); + const initialized = userIdModules.reduce((carry, submodule) => { + try { + populateSubmoduleId(submodule, consentData, storedConsentData, forceRefresh); + carry.push(submodule); + } catch (e) { + logError(`Error in userID module '${submodule.submodule.name}':`, e); + } return carry; }, []); + if (initialized.length) { + setPrebidServerEidPermissions(initialized); + } + initialized.forEach(updateInitializedSubmodules.bind(null, dest)); + return initialized; } -function updateInitializedSubmodules(submodule) { +function updateInitializedSubmodules(dest, submodule) { let updated = false; - for (let i = 0; i < initializedSubmodules.length; i++) { - if (submodule.config.name.toLowerCase() === initializedSubmodules[i].config.name.toLowerCase()) { + for (let i = 0; i < dest.length; i++) { + if (submodule.config.name.toLowerCase() === dest[i].config.name.toLowerCase()) { updated = true; - initializedSubmodules[i] = submodule; + dest[i] = submodule; break; } } if (!updated) { - initializedSubmodules.push(submodule); + dest.push(submodule); } } @@ -801,8 +935,9 @@ function updateSubmodules() { // do this to avoid reprocessing submodules const addedSubmodules = submoduleRegistry.filter(i => !find(submodules, j => j.name === i.name)); + submodules.splice(0, submodules.length); // find submodule and the matching configuration, if found create and append a SubmoduleContainer - submodules = addedSubmodules.map(i => { + addedSubmodules.map(i => { const submoduleConfig = find(configs, j => j.name && (j.name.toLowerCase() === i.name.toLowerCase() || (i.aliasName && j.name.toLowerCase() === i.aliasName.toLowerCase()))); if (submoduleConfig && i.name !== submoduleConfig.name) submoduleConfig.name = i.name; @@ -813,11 +948,13 @@ function updateSubmodules() { callback: undefined, idObj: undefined } : null; - }).filter(submodule => submodule !== null); + }).filter(submodule => submodule !== null) + .forEach((sm) => submodules.push(sm)); if (!addedUserIdHook && submodules.length) { // priority value 40 will load after consentManagement with a priority of 50 getGlobal().requestBids.before(requestBidsHook, 40); + coreGetPPID.after((next) => next(getPPID())); logInfo(`${MODULE_NAME} - usersync config updated for ${submodules.length} submodules: `, submodules.map(a => a.submodule.name)); addedUserIdHook = true; } @@ -839,12 +976,17 @@ export function attachIdSystem(submodule) { * so a callback is added to fire after the consentManagement module. * @param {{getConfig:function}} config */ -export function init(config) { +export function init(config, {delay = delayFor} = {}) { ppidSource = undefined; submodules = []; configRegistry = []; addedUserIdHook = false; - initializedSubmodules = undefined; + initializedSubmodules = []; + initIdSystem = idSystemInitializer({delay}); + if (configListener != null) { + configListener(); + } + submoduleRegistry = []; // list of browser enabled storage types validStorageTypes = [ @@ -863,7 +1005,7 @@ export function init(config) { } // listen for config userSyncs to be set - config.getConfig('userSync', conf => { + configListener = config.getConfig('userSync', conf => { // Note: support for 'usersync' was dropped as part of Prebid.js 4.0 const userSync = conf.userSync; ppidSource = userSync.ppid; @@ -872,13 +1014,18 @@ export function init(config) { syncDelay = isNumber(userSync.syncDelay) ? userSync.syncDelay : DEFAULT_SYNC_DELAY; auctionDelay = isNumber(userSync.auctionDelay) ? userSync.auctionDelay : NO_AUCTION_DELAY; updateSubmodules(); + initIdSystem({ready: true}); } }); // exposing getUserIds function in global-name-space so that userIds stored in Prebid can be used by external codes. (getGlobal()).getUserIds = getUserIds; (getGlobal()).getUserIdsAsEids = getUserIdsAsEids; + (getGlobal()).getEncryptedEidsForSource = getEncryptedEidsForSource; + (getGlobal()).registerSignalSources = registerSignalSources; (getGlobal()).refreshUserIds = refreshUserIds; + (getGlobal()).getUserIdsAsync = getUserIdsAsync; + (getGlobal()).getUserIdsAsEidBySource = getUserIdsAsEidBySource; } // init config update listener to start the application diff --git a/modules/userId/userId.md b/modules/userId/userId.md index 44ed7003f0c..16f56ae7b9d 100644 --- a/modules/userId/userId.md +++ b/modules/userId/userId.md @@ -6,6 +6,17 @@ Example showing `cookie` storage for user id data for each of the submodules pbjs.setConfig({ userSync: { userIds: [{ + name: "33acrossId", + storage: { + type: "cookie", + name: "33acrossId", + expires: 90, + refreshInSeconds: 8*3600 + }, + params: { + pid: "0010b00002GYU4eBAH" // Example ID + } + }, { name: "pubCommonId", storage: { type: "cookie", @@ -45,6 +56,17 @@ pbjs.setConfig({ expires: 90, // Expiration in days refreshInSeconds: 8*3600 // User Id cache lifetime in seconds, defaulting to 'expires' }, + }, { + name: "ftrackId", + storage: { + type: "html5", + name: "ftrackId", + expires: 90, + refreshInSeconds: 8*3600 + }, + params: { + url: 'https://d9.flashtalking.com/d9core', // required, if not populated ftrack will not run + } }, { name: 'parrableId', params: { @@ -89,6 +111,8 @@ pbjs.setConfig({ name: '_criteoId', expires: 1 } + }, { + name: "cpexId" }, { name: 'mwOpenLinkId', params: { @@ -140,6 +164,9 @@ pbjs.setConfig({ name: "knssoId", expires: 30 }, + { + name: "dacId" + } ], syncDelay: 5000, auctionDelay: 1000 @@ -152,17 +179,16 @@ Example showing `localStorage` for user id data for some submodules ``` pbjs.setConfig({ userSync: { - userIds: [ - { - name: 'trustpid', - params: { - maxDelayTime: 2500 - }, - bidders: ['adform'], + userIds: [{ + name: "33acrossId", storage: { - type: 'html5', - name: 'trustpid', - expires: 60 + type: "html5", + name: "33acrossId", + expires: 90, + refreshInSeconds: 8*3600 + }, + params: { + pid: "0010b00002GYU4eBAH" // Example ID } }, { name: "unifiedId", diff --git a/modules/userIdTargeting.js b/modules/userIdTargeting.js index e15c9ddaca2..b7fd137779b 100644 --- a/modules/userIdTargeting.js +++ b/modules/userIdTargeting.js @@ -1,7 +1,7 @@ import {config} from '../src/config.js'; import {getGlobal} from '../src/prebidGlobal.js'; import CONSTANTS from '../src/constants.json'; -import events from '../src/events.js'; +import * as events from '../src/events.js'; import { isStr, isPlainObject, isBoolean, isFn, hasOwn, logInfo } from '../src/utils.js'; const MODULE_NAME = 'userIdTargeting'; diff --git a/modules/ventesBidAdapter.js b/modules/ventesBidAdapter.js index a9de52a86ba..42292ddaed3 100644 --- a/modules/ventesBidAdapter.js +++ b/modules/ventesBidAdapter.js @@ -1,20 +1,7 @@ -import { - BANNER, - NATIVE, - VIDEO -} from '../src/mediaTypes.js'; -import { - convertCamelToUnderscore, - isStr, - isArray, - isNumber, - isPlainObject, - replaceAuctionPrice -} from '../src/utils.js'; -import find from 'core-js-pure/features/array/find.js'; -import { - registerBidder -} from '../src/adapters/bidderFactory.js'; +import {BANNER, NATIVE, VIDEO} from '../src/mediaTypes.js'; +import {convertCamelToUnderscore, isArray, isNumber, isPlainObject, isStr, replaceAuctionPrice} from '../src/utils.js'; +import {find} from '../src/polyfill.js'; +import {registerBidder} from '../src/adapters/bidderFactory.js'; const BID_METHOD = 'POST'; const BIDDER_URL = 'http://13.234.201.146:8088/va/ad'; diff --git a/modules/verizonMediaIdSystem.js b/modules/verizonMediaIdSystem.js index 280a6c47894..27577ad0de4 100644 --- a/modules/verizonMediaIdSystem.js +++ b/modules/verizonMediaIdSystem.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 = 'verizonMediaId'; const VENDOR_ID = 25; diff --git a/modules/vibrantmediaBidAdapter.js b/modules/vibrantmediaBidAdapter.js index b6fe51c43bc..0613f722af8 100644 --- a/modules/vibrantmediaBidAdapter.js +++ b/modules/vibrantmediaBidAdapter.js @@ -6,13 +6,14 @@ * Note: Only BANNER and VIDEO are currently supported by the prebid server. */ -import {logError, logInfo} from '../src/utils.js'; +import {logError, triggerPixel} from '../src/utils.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; import {BANNER, NATIVE, VIDEO} from '../src/mediaTypes.js'; import {OUTSTREAM} from '../src/video.js'; const BIDDER_CODE = 'vibrantmedia'; const VIBRANT_MEDIA_PREBID_URL = 'https://prebid.intellitxt.com/prebid'; +const VALID_PIXEL_URL_REGEX = /^https?:\/\/[a-zA-Z0-9-]+(\.[a-zA-Z0-9-]+)+([/?].*)?$/; const SUPPORTED_MEDIA_TYPES = [BANNER, NATIVE, VIDEO]; /** @@ -51,6 +52,10 @@ const isBaseUrl = function(url) { return (endOfDomain === -1) || (endOfDomain === (urlMinusScheme.length - 1)); }; +const isValidPixelUrl = function (candidateUrl) { + return VALID_PIXEL_URL_REGEX.test(candidateUrl); +}; + /** * Returns transformed bid requests that are in a format native to the prebid server. * @@ -213,7 +218,9 @@ export const spec = { * @param {*} bidData the data associated with the won bid. See example above for data format. */ onBidWon: function(bidData) { - logInfo('Bid won: ' + JSON.stringify(bidData)); + if (bidData && bidData.meta && isValidPixelUrl(bidData.meta.wp)) { + triggerPixel(`${bidData.meta.wp}${bidData.status}`); + } } }; diff --git a/modules/vidazooBidAdapter.js b/modules/vidazooBidAdapter.js index b6ab42499b9..cf252bda2dc 100644 --- a/modules/vidazooBidAdapter.js +++ b/modules/vidazooBidAdapter.js @@ -24,7 +24,7 @@ export const SUPPORTED_ID_SYSTEMS = { 'pubcid': 1, 'tdid': 1, }; -const storage = getStorageManager(GVLID); +const storage = getStorageManager({gvlid: GVLID, bidderCode: BIDDER_CODE}); export function createDomain(subDomain = DEFAULT_SUB_DOMAIN) { return `https://${subDomain}.cootlogix.com`; diff --git a/modules/vidoomyBidAdapter.js b/modules/vidoomyBidAdapter.js index d268f7a9d64..ffa4a290072 100644 --- a/modules/vidoomyBidAdapter.js +++ b/modules/vidoomyBidAdapter.js @@ -12,13 +12,9 @@ const GVLID = 380; const COOKIE_SYNC_FALLBACK_URLS = [ 'https://x.bidswitch.net/sync?ssp=vidoomy', 'https://ib.adnxs.com/getuid?https%3A%2F%2Fa-prebid.vidoomy.com%2Fsetuid%3Fbidder%3Dadnxs%26gdpr%3D{{GDPR}}%26gdpr_consent%3D{{GDPR_CONSENT}}%26uid%3D%24UID', - 'https://pixel-sync.sitescout.com/dmp/pixelSync?nid=120&redir=https%3A%2F%2Fa.vidoomy.com%2Fapi%2Frtbserver%2Fcookie%3Fi%3DCEN%26uid%3D%7BuserId%7D', - 'https://sync.1rx.io/usersync2/vidoomy?redir=https%3A%2F%2Fa.vidoomy.com%2Fapi%2Frtbserver%2Fcookie%3Fi%3DUN%26uid%3D%5BRX_UUID%5D', - 'https://rtb.openx.net/sync/prebid?gdpr={{GDPR}}&gdpr_consent={{GDPR_CONSENT}}&r=https%3A%2F%2Fa-prebid.vidoomy.com%2Fsetuid%3Fbidder%3Dopenx%26uid%3D$%7BUID%7D', - 'https://ads.pubmatic.com/AdServer/js/user_sync.html?gdpr={{GDPR}}&gdpr_consent={{GDPR_CONSENT}}&us_privacy=&predirect=https%3A%2F%2Fa-prebid.vidoomy.com%2Fsetuid%3Fbidder%3Dpubmatic%26gdpr%3D{{GDPR}}%26gdpr_consent%3D{{GDPR_CONSENT}}%26uid%3D', + 'https://pixel-sync.sitescout.com/dmp/pixelSync?nid=120&gdpr={{GDPR}}&gdpr_consent={{GDPR_CONSENT}}&redir=https%3A%2F%2Fa.vidoomy.com%2Fapi%2Frtbserver%2Fcookie%3Fi%3DCEN%26uid%3D%7BuserId%7D', 'https://cm.adform.net/cookie?redirect_url=https%3A%2F%2Fa-prebid.vidoomy.com%2Fsetuid%3Fbidder%3Dadf%26gdpr%3D{{GDPR}}%26gdpr_consent%3D{{GDPR_CONSENT}}%26uid%3D%24UID', - 'https://ups.analytics.yahoo.com/ups/58531/occ?gdpr={{GDPR}}&gdpr_consent={{GDPR_CONSENT}}', - 'https://ap.lijit.com/pixel?redir=https%3A%2F%2Fa-prebid.vidoomy.com%2Fsetuid%3Fbidder%3Dsovrn%26gdpr%3D{{GDPR}}%26gdpr_consent%3D{{GDPR_CONSENT}}%26uid%3D%24UID' + 'https://ups.analytics.yahoo.com/ups/58531/occ?gdpr={{GDPR}}&gdpr_consent={{GDPR_CONSENT}}' ]; const isBidRequestValid = bid => { diff --git a/modules/viewability.js b/modules/viewability.js index b12c53b7f59..39b2ee3da16 100644 --- a/modules/viewability.js +++ b/modules/viewability.js @@ -1,6 +1,6 @@ -import { logWarn, logInfo, isStr, isFn, triggerPixel, insertHtmlIntoIframe } from '../src/utils.js'; -import { getGlobal } from '../src/prebidGlobal.js'; -import find from 'core-js-pure/features/array/find.js'; +import {insertHtmlIntoIframe, isFn, isStr, logInfo, logWarn, triggerPixel} from '../src/utils.js'; +import {getGlobal} from '../src/prebidGlobal.js'; +import {find} from '../src/polyfill.js'; export const MODULE_NAME = 'viewability'; 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 ae9c22928e8..64cdd6508bb 100644 --- a/modules/weboramaRtdProvider.js +++ b/modules/weboramaRtdProvider.js @@ -7,57 +7,96 @@ * @requires module:modules/realTimeData */ +/** + * @typedef dataCallbackMetadata + * @property {Boolean} user if true it is user-centric data + * @property {String} source describe the source of data, if "contextual" or "wam" + * @property {Boolean} isDefault if true it the default profile defined in the configuration + */ + /** onData callback type * @callback dataCallback * @param {Object} data profile data - * @param {Boolean} site true if site, else it is user + * @param {dataCallbackMetadata} meta metadata * @returns {void} */ +/** setPrebidTargeting callback type + * @callback setPrebidTargetingCallback + * @param {String} adUnitCode + * @param {Object} data + * @param {dataCallbackMetadata} metadata + * @returns {Boolean} + */ + +/** sendToBidders callback type + * @callback sendToBiddersCallback + * @param {Object} bid + * @param {String} adUnitCode + * @param {Object} data + * @param {dataCallbackMetadata} metadata + * @returns {Boolean} + */ + /** * @typedef {Object} ModuleParams - * @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 {?setPrebidTargetingCallback|?Boolean|?Object} setPrebidTargeting if true, will set the GAM targeting (default undefined) + * @property {?sendToBiddersCallback|?Boolean|?Object} sendToBidders if true, will send the contextual profile to all bidders, else expects a list of allowed bidders (default undefined) * @property {?dataCallback} onData callback - * @property {?WeboCtxConf} weboCtxConf - * @property {?WeboUserDataConf} weboUserDataConf + * @property {?WeboCtxConf} weboCtxConf site-centric contextual configuration + * @property {?WeboUserDataConf} weboUserDataConf user-centric wam configuration + * @property {?SfbxLiteDataConf} sfbxLiteDataConf site-centric lite configuration */ /** * @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 params.setPrebidTargeting or true) - * @property {?Boolean} sendToBidders if true, will send the contextual profile to all bidders (default params.sendToBidders or true) + * @property {?setPrebidTargetingCallback|?Boolean|?Object} setPrebidTargeting if true, will set the GAM targeting (default undefined) + * @property {?sendToBiddersCallback|?Boolean|?Object} sendToBidders if true, will send the contextual profile to all bidders, else expects a list of allowed bidders (default undefined) * @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 + * @property {?string} baseURLProfileAPI to be used to point to a different domain than ctx.weborama.com */ /** * @typedef {Object} WeboUserDataConf * @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 {?setPrebidTargetingCallback|?Boolean|?Object} setPrebidTargeting if true, will set the GAM targeting (default undefined) + * @property {?sendToBiddersCallback|?Boolean|?Object} sendToBidders if true, will send the contextual profile to all bidders, else expects a list of allowed bidders (default undefined) * @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 */ +/** + * @typedef {Object} SfbxLiteDataConf + * @property {?setPrebidTargetingCallback|?Boolean|?Object} setPrebidTargeting if true, will set the GAM targeting (default undefined) + * @property {?sendToBiddersCallback|?Boolean|?Object} sendToBidders if true, will send the contextual profile to all bidders, else expects a list of allowed bidders (default undefined) + * @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 '_lite') + * @property {?Boolean} enabled if false, will ignore this configuration + */ import { getGlobal } from '../src/prebidGlobal.js'; import { deepSetValue, - deepAccess, isEmpty, mergeDeep, logError, logWarn, tryAppendQueryString, logMessage, - isFn + isFn, + isArray, + isStr, + isBoolean, + isPlainObject, + deepClone, } from '../src/utils.js'; import { submodule @@ -68,21 +107,36 @@ import { import { getStorageManager } from '../src/storageManager.js'; - -const adapterManager = require('../src/adapterManager.js').default; +import adapterManager from '../src/adapterManager.js'; /** @type {string} */ const MODULE_NAME = 'realTimeData'; /** @type {string} */ const SUBMODULE_NAME = 'weborama'; /** @type {string} */ +const BASE_URL_CONTEXTUAL_PROFILE_API = 'ctx.weborama.com'; +/** @type {string} */ export const DEFAULT_LOCAL_STORAGE_USER_PROFILE_KEY = 'webo_wam2gam_entry'; /** @type {string} */ const LOCAL_STORAGE_USER_TARGETING_SECTION = 'targeting'; +/** @type {string} */ +export const DEFAULT_LOCAL_STORAGE_LITE_PROFILE_KEY = '_lite'; +/** @type {string} */ +const LOCAL_STORAGE_LITE_TARGETING_SECTION = 'webo'; +/** @type {string} */ +const WEBO_CTX_CONF_SECTION = 'weboCtxConf'; +/** @type {string} */ +const WEBO_USER_DATA_CONF_SECTION = 'weboUserDataConf'; +/** @type {string} */ +const SFBX_LITE_DATA_CONF_SECTION = 'sfbxLiteDataConf'; + /** @type {number} */ const GVLID = 284; -/** @type {object} */ -export const storage = getStorageManager(GVLID, SUBMODULE_NAME); +/** @type {?Object} */ +export const storage = getStorageManager({ + gvlid: GVLID, + moduleName: SUBMODULE_NAME +}); /** @type {null|Object} */ let _weboContextualProfile = null; @@ -90,78 +144,67 @@ let _weboContextualProfile = null; /** @type {Boolean} */ let _weboCtxInitialized = false; -/** @type {null|Object} */ +/** @type {?Object} */ let _weboUserDataUserProfile = null; /** @type {Boolean} */ let _weboUserDataInitialized = false; +/** @type {?Object} */ +let _sfbxLiteDataProfile = null; + +/** @type {Boolean} */ +let _sfbxLiteDataInitialized = false; + /** Initialize module * @param {object} moduleConfig * @return {Boolean} true if module was initialized with success */ function init(moduleConfig) { - moduleConfig = moduleConfig || {}; - const moduleParams = moduleConfig.params || {}; - const weboCtxConf = moduleParams.weboCtxConf; - const weboUserDataConf = moduleParams.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(moduleParams, weboCtxConf) { - if (!weboCtxConf || weboCtxConf.enabled === false) { - moduleParams.weboCtxConf = null; + const moduleParams = moduleConfig?.params || {}; - return false - } - - normalizeConf(moduleParams, weboCtxConf); - - _weboCtxInitialized = false; _weboContextualProfile = null; + _weboUserDataUserProfile = null; + _sfbxLiteDataProfile = null; - if (!weboCtxConf.token) { - logWarn('missing param "token" for weborama contextual sub module initialization'); - return false; - } - - logMessage('weborama contextual intialized with success'); + _weboCtxInitialized = initSubSection(moduleParams, WEBO_CTX_CONF_SECTION, 'token'); + _weboUserDataInitialized = initSubSection(moduleParams, WEBO_USER_DATA_CONF_SECTION); + _sfbxLiteDataInitialized = initSubSection(moduleParams, SFBX_LITE_DATA_CONF_SECTION); - return true; + return _weboCtxInitialized || _weboUserDataInitialized || _sfbxLiteDataInitialized; } -/** Initialize weboUserData sub module - * @param {ModuleParams} moduleParams - * @param {WeboUserDataConf} weboUserDataConf - * @return {Boolean} true if sub module was initialized with success +/** Initialize subsection module + * @param {Object} moduleParams + * @param {string} subSection subsection name to initialize + * @param {[]string} requiredFields + * @return {Boolean} true if module subsection was initialized with success */ -function initWeboUserData(moduleParams, weboUserDataConf) { - if (!weboUserDataConf || weboUserDataConf.enabled === false) { - moduleParams.weboUserDataConf = null; +function initSubSection(moduleParams, subSection, ...requiredFields) { + const weboSectionConf = moduleParams[subSection] || {enabled: false}; + + if (weboSectionConf.enabled === false) { + delete moduleParams[subSection]; return false; } - normalizeConf(moduleParams, weboUserDataConf); + requiredFields ||= []; - _weboUserDataInitialized = false; - _weboUserDataUserProfile = null; + try { + normalizeConf(moduleParams, weboSectionConf); - let message = 'weborama user-centric intialized with success'; - if (weboUserDataConf.hasOwnProperty('accountId')) { - message = `weborama user-centric intialized with success for account: ${weboUserDataConf.accountId}`; + requiredFields.forEach(field => { + if (!weboSectionConf[field]) { + throw `missing required field "{field}" on {section}`; + } + }); + } catch (e) { + logError(`unable to initialize: error on ${subSection} configuration: ${e}`); + return false } - logMessage(message); + logMessage(`weborama ${subSection} initialized with success`); return true; } @@ -170,21 +213,172 @@ function initWeboUserData(moduleParams, weboUserDataConf) { const globalDefaults = { setPrebidTargeting: true, sendToBidders: true, - onData: (data, kind, def) => logMessage('onData(data,kind,default)', data, kind, def), + onData: () => { + /* do nothing */ }, } /** normalize submodule configuration * @param {ModuleParams} moduleParams - * @param {WeboCtxConf|WeboUserDataConf} submoduleParams + * @param {WeboCtxConf|WeboUserDataConf|SfbxLiteDataConf} submoduleParams * @return {void} */ function normalizeConf(moduleParams, submoduleParams) { + // handle defaults Object.entries(globalDefaults).forEach(([propertyName, globalDefaultValue]) => { if (!submoduleParams.hasOwnProperty(propertyName)) { const hasModuleParam = moduleParams.hasOwnProperty(propertyName); submoduleParams[propertyName] = (hasModuleParam) ? moduleParams[propertyName] : globalDefaultValue; } }) + + // handle setPrebidTargeting + coerceSetPrebidTargeting(submoduleParams) + + // handle sendToBidders + coerceSendToBidders(submoduleParams) + + if (!isFn(submoduleParams.onData)) { + throw 'onData parameter should be a callback'; + } + + submoduleParams.defaultProfile = submoduleParams.defaultProfile || {}; + + if (!isValidProfile(submoduleParams.defaultProfile)) { + throw 'defaultProfile is not valid'; + } +} + +/** coerce set prebid targeting to function + * @param {WeboCtxConf|WeboUserDataConf|SfbxLiteDataConf} submoduleParams + * @return {void} + */ +function coerceSetPrebidTargeting(submoduleParams) { + const setPrebidTargeting = submoduleParams.setPrebidTargeting; + + if (isFn(setPrebidTargeting)) { + return + } + + if (isBoolean(setPrebidTargeting)) { + const shouldSetPrebidTargeting = setPrebidTargeting; + + submoduleParams.setPrebidTargeting = () => shouldSetPrebidTargeting; + + return + } + + if (isStr(setPrebidTargeting)) { + const allowedAdUnitCode = setPrebidTargeting; + + submoduleParams.setPrebidTargeting = (adUnitCode) => allowedAdUnitCode == adUnitCode; + + return + } + + if (isArray(setPrebidTargeting)) { + const allowedAdUnitCodes = setPrebidTargeting; + + submoduleParams.setPrebidTargeting = (adUnitCode) => allowedAdUnitCodes.includes(adUnitCode); + + return + } + + throw `unexpected format for setPrebidTargeting: ${typeof setPrebidTargeting}`; +} + +/** coerce send to bidders to function + * @param {WeboCtxConf|WeboUserDataConf|SfbxLiteDataConf} submoduleParams + * @return {void} + */ +function coerceSendToBidders(submoduleParams) { + const sendToBidders = submoduleParams.sendToBidders; + + if (isFn(sendToBidders)) { + return + } + + if (isBoolean(sendToBidders)) { + const shouldSendToBidders = sendToBidders; + + submoduleParams.sendToBidders = () => shouldSendToBidders; + + return + } + + if (isStr(sendToBidders)) { + const allowedBidder = sendToBidders; + + submoduleParams.sendToBidders = (bid) => allowedBidder == bid.bidder; + + return + } + + if (isArray(sendToBidders)) { + const allowedBidders = sendToBidders; + + submoduleParams.sendToBidders = (bid) => allowedBidders.includes(bid.bidder); + + return + } + + if (isPlainObject(sendToBidders)) { + const sendToBiddersMap = sendToBidders; + submoduleParams.sendToBidders = (bid, adUnitCode) => { + const bidder = bid.bidder; + if (!sendToBiddersMap.hasOwnProperty(bidder)) { + return false + } + + const value = sendToBiddersMap[bidder]; + + if (isBoolean(value)) { + return value + } + + if (isStr(value)) { + return value == adUnitCode + } + + if (isArray(value)) { + return value.includes(adUnitCode) + } + + throw `unexpected format for sendToBidders[${bidder}]: ${typeof value}`; + }; + + return + } + + throw `unexpected format for sendToBidders: ${typeof sendToBidders}`; +} +/** + * check if profile is valid + * @param {*} profile + * @returns {Boolean} + */ +function isValidProfile(profile) { + if (!isPlainObject(profile)) { + return false; + } + + const keys = Object.keys(profile); + + for (var i in keys) { + const key = keys[i]; + const value = profile[key]; + if (!isArray(value)) { + return false; + } + + for (var j in value) { + const elem = value[j] + if (!isStr(elem)) { + return false; + } + } + } + + return true; } /** function that provides ad server targeting data to RTD-core @@ -193,24 +387,27 @@ function normalizeConf(moduleParams, submoduleParams) { * @returns {Object} target data */ function getTargetingData(adUnitsCodes, moduleConfig) { - moduleConfig = moduleConfig || {}; - const moduleParams = moduleConfig.params || {}; - const weboCtxConf = moduleParams.weboCtxConf || {}; - const weboUserDataConf = moduleParams.weboUserDataConf || {}; - const weboCtxConfTargeting = weboCtxConf.setPrebidTargeting; - const weboUserDataConfTargeting = weboUserDataConf.setPrebidTargeting; + const moduleParams = moduleConfig?.params || {}; - try { - const profile = getCompleteProfile(moduleParams, weboCtxConfTargeting, weboUserDataConfTargeting); + const profileHandlers = buildProfileHandlers(moduleParams); - if (isEmpty(profile)) { - return {}; - } + if (isEmpty(profileHandlers)) { + logMessage('no data to set targeting'); + return {}; + } + try { const td = adUnitsCodes.reduce((data, adUnitCode) => { - if (adUnitCode) { - data[adUnitCode] = profile; - } + data[adUnitCode] = profileHandlers.reduce((targeting, ph) => { + // logMessage(`check if should set targeting for adunit '${adUnitCode}'`); + const cph = copyProfileHandler(ph); + if (ph.setTargeting(adUnitCode, cph.data, cph.metadata)) { + // logMessage(`set targeting for adunit '${adUnitCode}', source '${ph.metadata.source}'`); + + mergeDeep(targeting, cph.data); + } + return targeting; + }, {}); return data; }, {}); @@ -221,57 +418,155 @@ function getTargetingData(adUnitsCodes, moduleConfig) { } } -/** function that provides complete profile formatted to be used +/** function that provides data handlers based on the configuration * @param {ModuleParams} moduleParams - * @param {Boolean} weboCtxConfTargeting - * @param {Boolean} weboUserDataConfTargeting - * @returns {Object} complete profile + * @returns {Array} handlers */ -function getCompleteProfile(moduleParams, weboCtxConfTargeting, weboUserDataConfTargeting) { - const profile = {}; +function buildProfileHandlers(moduleParams) { + const profileHandlers = []; + + if (_weboCtxInitialized && moduleParams?.weboCtxConf) { + const weboCtxConf = moduleParams.weboCtxConf; + const [data, isDefault] = getContextualProfile(weboCtxConf); + if (!isEmpty(data)) { + profileHandlers.push({ + data: data, + metadata: { + user: false, + source: 'contextual', + isDefault: !!isDefault, + }, + setTargeting: weboCtxConf.setPrebidTargeting, + sendToBidders: weboCtxConf.sendToBidders, + onData: weboCtxConf.onData, + }) + } else { + logMessage('skip contextual profile: no data'); + } + } - if (weboCtxConfTargeting) { - const contextualProfile = getContextualProfile(moduleParams.weboCtxConf || {}); - mergeDeep(profile, contextualProfile); + if (_weboUserDataInitialized && moduleParams?.weboUserDataConf) { + const weboUserDataConf = moduleParams.weboUserDataConf; + const [data, isDefault] = getWeboUserDataProfile(weboUserDataConf); + if (!isEmpty(data)) { + profileHandlers.push({ + data: data, + metadata: { + user: true, + source: 'wam', + isDefault: !!isDefault, + }, + setTargeting: weboUserDataConf.setPrebidTargeting, + sendToBidders: weboUserDataConf.sendToBidders, + onData: weboUserDataConf.onData, + }) + } else { + logMessage('skip wam profile: no data'); + } } - if (weboUserDataConfTargeting) { - const weboUserDataProfile = getWeboUserDataProfile(moduleParams.weboUserDataConf || {}); - mergeDeep(profile, weboUserDataProfile); + if (_sfbxLiteDataInitialized && moduleParams?.sfbxLiteDataConf) { + const sfbxLiteDataConf = moduleParams.sfbxLiteDataConf; + const [data, isDefault] = getSfbxLiteDataProfile(sfbxLiteDataConf); + if (!isEmpty(data)) { + profileHandlers.push({ + data: data, + metadata: { + user: false, + source: 'lite', + isDefault: !!isDefault, + }, + setTargeting: sfbxLiteDataConf.setPrebidTargeting, + sendToBidders: sfbxLiteDataConf.sendToBidders, + onData: sfbxLiteDataConf.onData, + }) + } else { + logMessage('skip sfbx lite profile: no data'); + } } - return profile; + return profileHandlers; } /** return contextual profile * @param {WeboCtxConf} weboCtxConf - * @returns {Object} contextual profile + * @returns {Array} contextual profile + isDefault boolean flag */ function getContextualProfile(weboCtxConf) { + if (_weboContextualProfile) { + return [_weboContextualProfile, false]; + } + const defaultContextualProfile = weboCtxConf.defaultProfile || {}; - return _weboContextualProfile || defaultContextualProfile; + + return [defaultContextualProfile, true]; } /** return weboUserData profile * @param {WeboUserDataConf} weboUserDataConf - * @returns {Object} weboUserData profile + * @returns {Array} weboUserData profile + isDefault boolean flag */ function getWeboUserDataProfile(weboUserDataConf) { - const weboUserDataDefaultUserProfile = weboUserDataConf.defaultProfile || {}; + return getDataFromLocalStorage(weboUserDataConf, + () => _weboUserDataUserProfile, + (data) => _weboUserDataUserProfile = data, + DEFAULT_LOCAL_STORAGE_USER_PROFILE_KEY, + LOCAL_STORAGE_USER_TARGETING_SECTION, + 'wam'); +} + +/** return weboUserData profile + * @param {SfbxLiteDataConf} sfbxLiteDataConf + * @returns {Array} sfbxLiteData profile + isDefault boolean flag + */ +function getSfbxLiteDataProfile(sfbxLiteDataConf) { + return getDataFromLocalStorage(sfbxLiteDataConf, + () => _sfbxLiteDataProfile, + (data) => _sfbxLiteDataProfile = data, + DEFAULT_LOCAL_STORAGE_LITE_PROFILE_KEY, + LOCAL_STORAGE_LITE_TARGETING_SECTION, + 'lite'); +} + +/** return generic webo data profile + * @param {WeboUserDataConf|SfbxLiteDataConf} weboDataConf + * @param {cacheGetCallback} cacheGet + * @param {cacheSetCallback} cacheSet + * @param {String} defaultLocalStorageProfileKey + * @param {String} targetingSection + * @param {String} source + * @returns {Array} webo (user|lite) data profile + isDefault boolean flag + */ +function getDataFromLocalStorage(weboDataConf, cacheGet, cacheSet, defaultLocalStorageProfileKey, targetingSection, source) { + const defaultProfile = weboDataConf.defaultProfile || {}; - if (storage.localStorageIsEnabled() && !_weboUserDataUserProfile) { - const localStorageProfileKey = weboUserDataConf.localStorageProfileKey || DEFAULT_LOCAL_STORAGE_USER_PROFILE_KEY; + if (storage.localStorageIsEnabled() && !cacheGet()) { + const localStorageProfileKey = weboDataConf.localStorageProfileKey || defaultLocalStorageProfileKey; const entry = storage.getDataFromLocalStorage(localStorageProfileKey); if (entry) { const data = JSON.parse(entry); - if (data && Object.keys(data).length > 0) { - _weboUserDataUserProfile = data[LOCAL_STORAGE_USER_TARGETING_SECTION]; + if (data && isPlainObject(data) && data.hasOwnProperty(targetingSection)) { + const profile = data[targetingSection]; + const valid = isValidProfile(profile); + if (!valid) { + logWarn(`found invalid ${source} profile on local storage key ${localStorageProfileKey}, section ${targetingSection}`); + } + + if (valid && !isEmpty(data)) { + cacheSet(profile); + } } } } - return _weboUserDataUserProfile || weboUserDataDefaultUserProfile; + const profile = cacheGet() + + if (profile) { + return [profile, false]; + } + + return [defaultProfile, true]; } /** function that will allow RTD sub-modules to modify the AdUnit object for each auction @@ -281,102 +576,83 @@ function getWeboUserDataProfile(weboUserDataConf) { * @returns {void} */ export function getBidRequestData(reqBidsConfigObj, onDone, moduleConfig) { - moduleConfig = moduleConfig || {}; - const moduleParams = moduleConfig.params || {}; - const weboCtxConf = moduleParams.weboCtxConf || {}; - - const adUnits = reqBidsConfigObj.adUnits || getGlobal().adUnits; + const moduleParams = moduleConfig?.params || {}; if (!_weboCtxInitialized) { - handleBidRequestData(adUnits, moduleParams); + handleBidRequestData(reqBidsConfigObj, moduleParams); onDone(); return; } + const weboCtxConf = moduleParams.weboCtxConf || {}; + fetchContextualProfile(weboCtxConf, (data) => { logMessage('fetchContextualProfile on getBidRequestData is done'); setWeboContextualProfile(data); }, () => { - handleBidRequestData(adUnits, moduleParams); + handleBidRequestData(reqBidsConfigObj, moduleParams); onDone(); }); } /** function that handles bid request data - * @param {Object[]} adUnits + * @param {Object} reqBids * @param {ModuleParams} moduleParams * @returns {void} */ +function handleBidRequestData(reqBids, moduleParams) { + const profileHandlers = buildProfileHandlers(moduleParams); -function handleBidRequestData(adUnits, moduleParams) { - const weboCtxConf = moduleParams.weboCtxConf || {}; - const weboUserDataConf = moduleParams.weboUserDataConf || {}; - const weboCtxConfTargeting = weboCtxConf.sendToBidders; - const weboUserDataConfTargeting = weboUserDataConf.sendToBidders; - - 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); - } + if (isEmpty(profileHandlers)) { + logMessage('no data to send to bidders'); + return; } - handleOnData(weboCtxConf, weboUserDataConf); -} - -/** function that handle with onData callbacks - * @param {WeboCtxConf} weboCtxConf - * @param {WeboUserDataConf} weboUserDataConf - */ + const adUnits = reqBids.adUnits || getGlobal().adUnits; -function handleOnData(weboCtxConf, weboUserDataConf) { - const callbacks = [{ - onData: weboCtxConf.onData, - fetchData: () => getContextualProfile(weboCtxConf), - site: true, - }, { - onData: weboUserDataConf.onData, - fetchData: () => getWeboUserDataProfile(weboUserDataConf), - site: false, - }]; + try { + adUnits.filter( + adUnit => adUnit.hasOwnProperty('bids') + ).forEach( + adUnit => adUnit.bids.forEach( + bid => profileHandlers.forEach(ph => { + // logMessage(`check if bidder '${bid.bidder}' and adunit '${adUnit.code} are share ${ph.metadata.source} data`); + + const cph = copyProfileHandler(ph); + if (ph.sendToBidders(bid, adUnit.code, cph.data, cph.metadata)) { + // logMessage(`handling bidder '${bid.bidder}' with ${ph.metadata.source} data`); + + handleBid(bid, cph.data, ph.metadata); + } + }) + ) + ); + } catch (e) { + logError('unable to send data to bidders:', e); + } - callbacks.filter(obj => isFn(obj.onData)).forEach(obj => { + profileHandlers.forEach(ph => { try { - const data = obj.fetchData(); - obj.onData(data, obj.site); + const cph = copyProfileHandler(ph); + ph.onData(cph.data, cph.metadata); } catch (e) { - const kind = (obj.site) ? 'site' : 'user'; - logError(`error while executure onData callback with ${kind}-based data:`, e); + logError(`error while executure onData callback with ${ph.metadata.source}-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 that handles bid request data + * @param {Object} ph profile handler + *@returns {Object} of deeply copy data and metadata */ -function setBidRequestProfile(adUnits, profile, site) { - setGlobalOrtb2(profile, site); - - adUnits.forEach(adUnit => { - if (adUnit.hasOwnProperty('bids')) { - const adUnitCode = adUnit.code || 'no code'; - adUnit.bids.forEach(bid => handleBid(adUnitCode, profile, site, bid)); - } - }); +function copyProfileHandler(ph) { + return { + data: deepClone(ph.data), + metadata: deepClone(ph.metadata), + }; } /** @type {string} */ @@ -395,115 +671,61 @@ const SMARTADSERVER = 'smartadserver'; const bidderAliasRegistry = adapterManager.aliasRegistry || {}; /** handle individual bid - * @param {string} adUnitCode - * @param {Object} profile - * @param {Boolean} site true if site centric, else it is user centric * @param {Object} bid + * @param {Object} profile + * @param {Object} metadata * @returns {void} */ -function handleBid(adUnitCode, profile, site, bid) { +function handleBid(bid, profile, metadata) { const bidder = bidderAliasRegistry[bid.bidder] || bid.bidder; - logMessage(`handling on adunit '${adUnitCode}', bidder '${bidder}' and bid`, bid); - switch (bidder) { case APPNEXUS: - handleAppnexusBid(profile, bid); + handleAppnexusBid(bid, profile); break; case PUBMATIC: - handlePubmaticBid(profile, bid); + handlePubmaticBid(bid, profile); break; case SMARTADSERVER: - handleSmartadserverBid(profile, bid); + handleSmartadserverBid(bid, profile); break; case RUBICON: - handleRubiconBid(profile, site, bid); + handleRubiconBid(bid, profile, metadata); 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); + handleBidViaORTB2(bid, profile, metadata); } } -/** - * 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 + * @param {Object} profile * @returns {void} */ -function handleAppnexusBid(profile, bid) { +function handleAppnexusBid(bid, profile) { const base = 'params.keywords'; assignProfileToObject(bid, base, profile); } /** handle pubmatic bid - * @param {Object} profile * @param {Object} bid + * @param {Object} profile * @returns {void} */ -function handlePubmaticBid(profile, bid) { +function handlePubmaticBid(bid, profile) { const sep = '|'; const subsep = ','; - const bidKey = 'params.dctr'; const target = []; - const data = deepAccess(bid, bidKey); + bid.params ||= {}; + + const data = bid.params.dctr; if (data) { data.split(sep).forEach(t => target.push(t)); } @@ -516,20 +738,21 @@ function handlePubmaticBid(profile, bid) { } }); - deepSetValue(bid, bidKey, target.join(sep)); + bid.params.dctr = target.join(sep); } /** handle smartadserver bid - * @param {Object} profile * @param {Object} bid + * @param {Object} profile * @returns {void} */ -function handleSmartadserverBid(profile, bid) { +function handleSmartadserverBid(bid, profile) { const sep = ';'; - const bidKey = 'params.target'; const target = []; - const data = deepAccess(bid, bidKey); + bid.params ||= {}; + + const data = bid.params.target; if (data) { data.split(sep).forEach(t => target.push(t)); } @@ -542,7 +765,56 @@ function handleSmartadserverBid(profile, bid) { } }); }); - deepSetValue(bid, bidKey, target.join(sep)); + + bid.params.target = target.join(sep); +} + +/** handle rubicon bid + * @param {Object} bid + * @param {Object} profile + * @param {Object} metadata + * @returns {void} + */ +function handleRubiconBid(bid, profile, metadata) { + if (isBoolean(metadata.user)) { + const section = (metadata.user) ? 'visitor' : 'inventory'; + const base = `params.${section}`; + assignProfileToObject(bid, base, profile); + } else { + logMessage(`SKIP bidder '${bid.bidder}', data from '${metadata.source}' is not defined as user or site-centric`); + } +} + +/** handle generic bid via ortb2 arbitrary data + * @param {Object} bid + * @param {Object} profile + * @param {Object} metadata + * @returns {void} + */ +function handleBidViaORTB2(bid, profile, metadata) { + if (isBoolean(metadata.user)) { + logMessage(`bidder '${bid.bidder}' is not directly supported, trying set data via bidder ortb2 fpd`); + const section = ((metadata.user) ? 'user' : 'site'); + const base = `ortb2.${section}.ext.data`; + + assignProfileToObject(bid, base, profile); + } else { + logMessage(`SKIP unsupported bidder '${bid.bidder}', data from '${metadata.source}' is not defined as user or site-centric`); + } +} + +/** + * 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]) + }) } /** set bigsea contextual profile on module state @@ -550,7 +822,7 @@ function handleSmartadserverBid(profile, bid) { * @returns {void} */ export function setWeboContextualProfile(data) { - if (data && Object.keys(data).length > 0) { + if (data && isPlainObject(data) && isValidProfile(data) && !isEmpty(data)) { _weboContextualProfile = data; } } @@ -575,15 +847,16 @@ export function setWeboContextualProfile(data) { function fetchContextualProfile(weboCtxConf, onSuccess, onDone) { const targetURL = weboCtxConf.targetURL || document.URL; const token = weboCtxConf.token; + const baseURLProfileAPI = weboCtxConf.baseURLProfileAPI || BASE_URL_CONTEXTUAL_PROFILE_API; let queryString = ''; queryString = tryAppendQueryString(queryString, 'token', token); queryString = tryAppendQueryString(queryString, 'url', targetURL); - const url = `https://ctx.weborama.com/api/profile?${queryString}`; + const urlProfileAPI = `https://${baseURLProfileAPI}/api/profile?${queryString}`; - ajax(url, { - success: function(response, req) { + ajax(urlProfileAPI, { + success: (response, req) => { if (req.status === 200) { try { const data = JSON.parse(response); @@ -598,7 +871,7 @@ function fetchContextualProfile(weboCtxConf, onSuccess, onDone) { onDone(); } }, - error: function() { + error: () => { onDone(); logError('unable to get weborama data'); } diff --git a/modules/weboramaRtdProvider.md b/modules/weboramaRtdProvider.md index 732944c6e1c..88ec907c9a1 100644 --- a/modules/weboramaRtdProvider.md +++ b/modules/weboramaRtdProvider.md @@ -6,11 +6,17 @@ Module Type: Rtd Provider Maintainer: prebid-support@weborama.com ``` -# Description +## Description -Weborama provides a Semantic AI Contextual API that classifies in Real-time a web page seen by a web user within generic and custom topics. It enables publishers to better monetize their inventory and unlock it to programmatic. +Weborama provides a Real-Time Data Submodule for `Prebid.js`, allowing to easy integrate different products such as: -Contact prebid-support@weborama.com for information. +* Semantic AI Contextual API that classifies in Real-time a web page seen by a web user within generic and custom topics. It enables publishers to better monetize their inventory and unlock it to programmatic. + +* Weborama Audience Manager (WAM) is a DMP (Data Management Platform) used by over 60 companies in the world. This platform distinguishes itself particularly by a high level interconnexion with the adtech & martech ecosystem and a transparent access to the database intelligence. + +* LiTE by SFBX® (Local inApp Trust Engine) provides “Zero Party Data” given by users, stored and calculated only on the user’s device. Through a unique cohorting system, it enables better monetization in a consent/consentless and identity-less mode. + +Contact prebid-support@weborama.com for more information. ### Publisher Usage @@ -18,7 +24,7 @@ Compile the Weborama RTD module into your Prebid build: `gulp build --modules=rtdModule,weboramaRtdProvider` -Add the Weborama RTD provider to your Prebid config. +Add the Weborama RTD provider to your Prebid config, use the configuration template below: ```javascript var pbjs = pbjs || {}; @@ -26,94 +32,525 @@ pbjs.que = pbjs.que || []; pbjs.que.push(function () { pbjs.setConfig({ - debug: true, + debug: true, // Output debug messages to the web console, *should* be disabled in production realTimeData: { auctionDelay: 1000, dataProviders: [{ name: "weborama", waitForIt: true, params: { - setPrebidTargeting: true, // optional - sendToBidders: true, // optional - onData: function(data, site){ // optional - var kind = (site)? 'site' : 'user'; - console.log('onData', kind, data); - }, - 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, ...) { ...} - } - } - }] + /* add weborama rtd submodule configuration here */ + }, + }, + // other modules... + ] } }); }); ``` +The module configuration has 3 independent sections (`weboCtxConf`, `weboUserDataConf` and `sfbxLiteDataConf`), each one mapped to a single product (`contextual`, `wam` and `lite`). No section is enabled by default, we must be explicit like in the minimal example below: + +```javascript +pbjs.setConfig({ + debug: true, + realTimeData: { + auctionDelay: 1000, + dataProviders: [{ + name: "weborama", + waitForIt: true, + params: { + weboCtxConf: { // contextual site-centric configuration, *omit if not needed* + token: "<>", // mandatory + }, + weboUserDataConf: { // wam user-centric configuration, *omit if not needed* + enabled: true, + }, + sfbxLiteDataConf: { // sfbx-lite site-centric configuration, *omit if not needed* + enabled: true, + }, + } + }, + // other modules... + ] + } +}); +``` + +Each module can perform two actions: + +* set targeting on [GPT](https://docs.prebid.org/dev-docs/publisher-api-reference/setTargetingForGPTAsync.html) / [AST](https://docs.prebid.org/dev-docs/publisher-api-reference/setTargetingForAst.html]) via `prebid.js` + +* send data to other `prebid.js` bidder modules (check the complete list at the end of this page) + ### Parameter Descriptions for the Weborama Configuration Section +This is the main configuration section + | Name |Type | Description | Notes | | :------------ | :------------ | :------------ |:------------ | | 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.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 | +| 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`, `weboUserDataConf` and `sfbxLiteDataConf` sections | +| params.sendToBidders | Boolean or Array | If true, may send the profile to all bidders. If an array, will specify the bidders to send data | Optional. Affects the `weboCtxConf`, `weboUserDataConf` and `sfbxLiteDataConf` sections | +| params.weboCtxConf | Object | Weborama Contextual Site-Centric Configuration | Optional | +| params.weboUserDataConf | Object | Weborama WAM User-Centric Configuration | Optional | +| params.sfbxLiteDataConf | Object | Sfbx LiTE Site-Centric Configuration | Optional | +| params.onData | Callback | If set, will receive the profile and metadata | Optional. Affects the `weboCtxConf`, `weboUserDataConf` and `sfbxLiteDataConf` sections | + +#### Contextual Site-Centric Configuration -#### Contextual Configuration +To be possible use the integration with Weborama Contextual Service you must be a client with a valid API token. Please contact weborama if you don't have it. + +On this section we will explain the `params.weboCtxConf` subconfiguration: | 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**.| +| setPrebidTargeting|Various|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|Various|If true, will send the contextual profile to all bidders. If an array, will specify the bidders to send data| 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 metadata | Optional. Default is `params.onData` (if any) or log via prebid debug | +| enabled | Boolean| if false, will ignore this configuration| Default is `true` if this section is present| +| baseURLProfileAPI | String| if present, update the domain of the contextual api| Optional. Default is `ctx.weborama.com` | + +#### WAM User-Centric Configuration + +To be possible use the integration with Weborama Audience Manager (WAM) you must be a client with an account id and you lust include the `wamfactory` script in your pages with `wam2gam` feature activated. +Please contact weborama if you don't have it. + +On this section we will explain the `params.weboUserDataConf` subconfiguration: + +| Name |Type | Description | Notes | +| :------------ | :------------ | :------------ |:------------ | +| accountId|Number|WAM account id. If you don't have it, please contact weborama. | Recommended.| +| setPrebidTargeting|Various|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|Various|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 | -| enabled | Boolean| if false, will ignore this configuration| default 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 `{}` | +| localStorageProfileKey| String | can be used to customize the local storage key | Optional | +| enabled | Boolean| if false, will ignore this configuration| Default is `true` if this section is present| + +#### Sfbx LiTE Site-Centric Configuration + +To be possible use the integration between Weborama and Sfbx LiTE you should also contact SFBX® to setup this product. -#### User-Centric Configuration +On this section we will explain the `params.sfbxLiteDataConf` subconfiguration: | 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**.| +| setPrebidTargeting|Various|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|Varios|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| +| localStorageProfileKey| String | can be used to customize the local storage key | Optional | +| enabled | Boolean| if false, will ignore this configuration| Default is `true` if this section is present| + +##### Property setPrebidTargeting supported types + +This property support the following types + +| Type | Description | Example | Notes | +| :------------ | :------------ | :------------ |:------------ | +| Boolean|If true, set prebid targeting for all adunits, or not in case of false| `true` | default value | +| String|Will set prebid targeting only for one adunit | `'adUnitCode1'` | | +| Array of Strings|Will set prebid targeting only for some adunits| `['adUnitCode1','adUnitCode2']` | | +| Callback |Will be executed for each adunit, expects return a true value to set prebid targeting or not| `function(adUnitCode){return adUnitCode == 'adUnitCode';}` | | + +The complete callback function signature is: + +```javascript +setPrebidTargeting: function(adUnitCode, data, metadata){ + return true; // or false, depending on the logic +} +``` + +This callback will be executed with the adUnitCode, profile and a metadata with the following fields + +| Name |Type | Description | Notes | +| :------------ | :------------ | :------------ |:------------ | +| user | Boolean | If true, it contains user-centric data | | +| source | String | Represent the source of data | can be `contextual`, `wam` or `lite` | +| isDefault | Boolean | If true, it contains the default profile defined in the configuration | | + +It is possible customize the targeting based on the parameters: + +```javascript +setPrebidTargeting: function(adUnitCode, data, metadata){ + // check metadata.source can be omitted if defined in params.weboUserDataConf + if (adUnitCode == 'adUnitCode1' && metadata.source == 'wam'){ + data['foo']=['bar']; // add this section only for adUnitCode1 + delete data['other']; // remove this section + } + return true; +} +``` + +##### Property sendToBidders supported types + +This property support the following types + +| Type | Description | Example | Notes | +| :------------ | :------------ | :------------ |:------------ | +| Boolean|If true, send data to all bidders, or not in case of false| `true` | default value | +| String|Will send data to only one bidder | `'appnexus'` | | +| Array of Strings|Will send data to only some bidders | `['appnexus','pubmatic']` | | +| Object |Will send data to only some bidders and some ad units | `{appnexus: true, pubmatic:['adUnitCode1']}` | | +| Callback |Will be executed for each adunit, expects return a true value to set prebid targeting or not| `function(bid, adUnitCode){return bid.bidder == 'appnexus' && adUnitCode == 'adUnitCode';}` | | + +A better look on the `Object` type + +```javascript +sendToBidders: { + appnexus: true, // send profile to appnexus on all ad units + pubmatic: ['adUnitCode1'],// send profile to pubmatic on this ad units +} +``` + +The complete callback function signature is: + +```javascript +sendToBidders: function(bid, adUnitCode, data, metadata){ + return true; // or false, depending on the logic +} +``` + +This callback will be executed with the bid object (contains a field `bidder` with name), adUnitCode, profile and a metadata with the following fields + +| Name |Type | Description | Notes | +| :------------ | :------------ | :------------ |:------------ | +| user | Boolean | If true, it contains user-centric data | | +| source | String | Represent the source of data | can be `contextual`, `wam` or `lite` | +| isDefault | Boolean | If true, it contains the default profile defined in the configuration | | + +It is possible customize the targeting based on the parameters: + +```javascript +sendToBidders: function(bid, adUnitCode, data, metadata){ + if (bid.bidder == 'appnexus' && adUnitCode == 'adUnitCode1'){ + data['foo']=['bar']; // add this section only for appnexus + adUnitCode1 + delete data['other']; // remove this section + } + return true; +} +``` + +To be possible customize the way we send data to bidders via this callback: + +```javascript +sendToBidders: function(bid, adUnitCode, data, metadata){ + if (bid.bidder == 'other'){ + /* use bid object to store data based on this specific logic, like in the example below */ + + bid.params = bid.params || {}; + bid.params['some_specific_key'] = data; + + return false; // will prevent the module to follow the pre-defined logic per bidder + } + // others + return true; +} +``` + +In case of using bid _aliases_, we should match the same string used in the adUnit configuration. + +```javascript +pbjs.aliasBidder('appnexus', 'foo'); +pbjs.aliasBidder('criteo', 'bar'); +pbjs.aliasBidder('pubmatic', 'baz'); +pbjs.setConfig({ + realTimeData: { + dataProviders: [{ + name: "weborama", + waitForIt: true, + params: { + weboCtxConf: { + token: "to-be-defined", // mandatory + sendToBidders: ['foo','bar'], // will share site-centric data with bidders foo and bar + }, + weboUserDataConf: { + accountId: 12345, // recommended, + sendToBidders: ['baz'], // will share user-centric data with only bidder baz + } + } + }] + } +}); +``` + +##### Using onData callback + +We can specify a callback to handle the profile data from site-centric or user-centric data. + +This callback will be executed with the profile and a metadata with the following fields + +| Name |Type | Description | Notes | +| :------------ | :------------ | :------------ |:------------ | +| user | Boolean | If true, it contains user-centric data | | +| source | String | Represent the source of data | can be `contextual`, `wam` or `lite` | +| isDefault | Boolean | If true, it contains the default profile defined in the configuration | | + +The metadata maybe not useful if we define the callback on site-centric of user-centric configuration, but if defined in the global level: + +```javascript +params: { + onData: function(data, metadata){ + var hasUserCentricData = metadata.user; + var dataSource = metadata.source; + console.log('onData', data, hasUserCentricData, dataSource); + } +} +``` + +an interesting example is to set GAM targeting in global level instead in slot level only for contextual data: + +```javascript +params: { + weboCtxConf: { + token: 'to-be-defined', + setPrebidTargeting: false, + onData: function(data, metadata){ + var googletag = googletag || {}; + googletag.cmd = googletag.cmd || []; + googletag.cmd.push(function () { + for(var key in data){ + googletag.pubads().setTargeting(key, data[key]); + } + }); + }, + } +} +``` + +### More configuration examples + +A more complete example can be found below. We can define default profiles, for each section, to be used in case of no data are found. + +We can control if we will set prebid targeting or send data to bidders in a global level or on each section (`contextual`, `wam` or `lite`). + +By default we try to send the data to all destinations, always. To restrict we can have two choices: + +* Set `setPrebidTargeting` or `sendToBidders` explicity to `true` or `false` on each section; +* Set `setPrebidTargeting` or `sendToBidders` globally to `false` and only enable on the right sections; + +```javascript +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: { + token: "<>", // mandatory + targetURL: "https://example.org", // default is document.URL + setPrebidTargeting: true, // override param.setPrebidTargeting. default is true + sendToBidders: true, // override param.sendToBidders. default is true + defaultProfile: { // optional, used if nothing is found + webo_ctx: [ ... ], // contextual segments + webo_ds: [ ...], // data science segments + }, + enabled: true, + }, + weboUserDataConf: { + setPrebidTargeting: true, // override param.setPrebidTargeting. default is true + sendToBidders: true, // override param.sendToBidders. default is true + defaultProfile: { // optional, used if nothing is found + webo_cs: [...], // wam custom segments + webo_audiences: [...], // wam audiences + }, + enabled: true, + }, + sfbxLiteDataConf: { + setPrebidTargeting: true, // override param.setPrebidTargeting. default is true + sendToBidders: true, // override param.sendToBidders. default is true + defaultProfile: { // optional, used if nothing is found + /* add specific lite segments here */ + }, + enabled: true, + }, + } + }] + } + }); +}); +``` + +Imagine we need to configure the following options using the previous example, we can write the configuration like the one below. + +||contextual|wam|lite| +| :------------ | :------------ | :------------ |:------------ | +|setPrebidTargeting|true|false|true| +|sendToBidders|false|true|true| + +```javascript +var pbjs = pbjs || {}; +pbjs.que = pbjs.que || []; + +pbjs.que.push(function () { + pbjs.setConfig({ + debug: true, + realTimeData: { + auctionDelay: 1000, + dataProviders: [{ + name: "weborama", + waitForIt: true, + params: { + setPrebidTargeting: false, // optional. set the default value of each section. + sendToBidders: false, // optional. set the default value of each section. + weboCtxConf: { + token: "<>", // mandatory + targetURL: "https://example.org", // default is document.URL + setPrebidTargeting: true, // override param.setPrebidTargeting. default is true + enabled: true, + }, + weboUserDataConf: { + sendToBidders: true, // override param.sendToBidders. default is true + enabled: true, + }, + sfbxLiteDataConf: { + setPrebidTargeting: true, // override param.setPrebidTargeting. default is true + sendToBidders: true, // override param.sendToBidders. default is true + enabled: true, + }, + } + }] + } + }); +}); +``` + +We can also define a list of adunits / bidders that will receive data instead of using boolean values. + +```javascript +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: { + token: "to-be-defined", // mandatory + setPrebidTargeting: ['adUnitCode1',...], // set target only on certain adunits + sendToBidders: ['appnexus',...], // overide, send to only some bidders + enabled: true, + }, + weboUserDataConf: { + accountId: 12345, // recommended + setPrebidTargeting: ['adUnitCode2',...], // set target only on certain adunits + sendToBidders: ['rubicon',...], // overide, send to only some bidders + enabled: true, + }, + sfbxLiteDataConf: { + setPrebidTargeting: ['adUnitCode3',...], // set target only on certain adunits + sendToBidders: ['smartadserver',...], // overide, send to only some bidders + enabled: true, + } + } + }] + } + }); +}); +``` + +Finally, we can combine several styles in the same configuration if needed. Including the callback style. + +```javascript +var pbjs = pbjs || {}; +pbjs.que = pbjs.que || []; + +pbjs.que.push(function () { + pbjs.setConfig({ + debug: true, + realTimeData: { + auctionDelay: 1000, + dataProviders: [{ + name: "weborama", + waitForIt: true, + params: { + setPrebidTargeting: true, // optional + sendToBidders: true, // optional + onData: function(data, meta){ // optional + var userCentricData = meta.user; // maybe undefined + var sourceOfData = meta.source; // contextual, wam or lite + + var isDefault = meta.isDefault; // true if uses default profile + + console.log('onData', data, meta); + }, + weboCtxConf: { + token: "to-be-defined", // mandatory + targetURL: "https://prebid.org", // default is document.URL + setPrebidTargeting: true, // override param.setPrebidTargeting or default true + sendToBidders: ['appnexus',...], // overide, send to only some bidders + defaultProfile: { // optional + webo_ctx: ['moon'], + webo_ds: ['bar'] + }, + enabled: true, + //, onData: function (data, ...) { ...} + }, + weboUserDataConf: { + accountId: 12345, // recommended + setPrebidTargeting: ['adUnitCode1',...], // set target only on certain adunits + sendToBidders: { // send to only some bidders and adunits + 'appnexus': true, // all adunits for appnexus + 'pubmatic': ['adUnitCode1',...] // some adunits for pubmatic + // other bidders will be ignored + }, + defaultProfile: { // optional + webo_cs: ['Red'], + webo_audiences: ['bam'] + }, + localStorageProfileKey: 'webo_wam2gam_entry', // default + enabled: true, + //, onData: function (data, ...) { ...} + }, + sfbxLiteDataConf: { + setPrebidTargeting: function(adUnitCode){ // specify set target via callback + return adUnitCode == 'adUnitCode1'; + }, + sendToBidders: function(bid, adUnitCode){ // specify sendToBidders via callback + return bid.bidder == 'appnexus' && adUnitCode == 'adUnitCode1'; + } + defaultProfile: { // optional + lite_occupation: ['gérant', 'bénévole'], + lite_hobbies: ['sport', 'cinéma'], + }, + localStorageProfileKey: '_lite', // default + enabled: true, + //, onData: function (data, ...) { ...} + } + } + }] + } + }); +}); +``` ### 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 +We also set the bidder (and global, if no specific bidders are set on `sendToBidders`) ortb2 `site.ext.data` and `user.ext.data` sections (as arbitrary data). 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 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) { - + + diff --git a/test/pages/bidderSettings.html b/test/pages/bidderSettings.html index 015ad3ca45f..205fc250be1 100644 --- a/test/pages/bidderSettings.html +++ b/test/pages/bidderSettings.html @@ -1,7 +1,7 @@ - + + + - + - +