diff --git a/.babelrc.js b/.babelrc.js index 43303f59a8b..add243a5b5d 100644 --- a/.babelrc.js +++ b/.babelrc.js @@ -1,25 +1 @@ - -let path = require('path'); - -function useLocal(module) { - return require.resolve(module, { - paths: [ - __dirname - ] - }) -} - -module.exports = { - "presets": [ - [ - useLocal('@babel/preset-env'), - { - "useBuiltIns": "entry" - } - ] - ], - "plugins": [ - path.resolve(__dirname, './plugins/pbjsGlobals.js'), - useLocal('babel-plugin-transform-object-assign') - ] -}; +module.exports = require('./babelConfig.js')(); diff --git a/.circleci/config.yml b/.circleci/config.yml index 404026d9446..c11f87b6f59 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -3,11 +3,11 @@ # Check https://circleci.com/docs/2.0/language-javascript/ for more details # -aliases: +aliases: - &environment docker: # specify the version you desire here - - image: circleci/node:12.16.1 + - image: circleci/node:12.16.1-browsers resource_class: xlarge # Specify service dependencies here if necessary # CircleCI maintains a library of pre-built images @@ -36,7 +36,7 @@ aliases: - &run_endtoend_test name: BrowserStack End to end testing - command: echo "127.0.0.1 test.localhost" | sudo tee -a /etc/hosts && gulp e2e-test --host=test.localhost + command: gulp e2e-test # Download and run BrowserStack local - &setup_browserstack @@ -72,7 +72,7 @@ jobs: build: <<: *environment steps: *unit_test_steps - + e2etest: <<: *environment steps: *endtoend_test_steps @@ -82,16 +82,6 @@ workflows: commit: jobs: - build - nightly: - triggers: - - schedule: - cron: "0 0 * * *" - filters: - branches: - only: - - master - jobs: - - e2etest experimental: pipelines: true diff --git a/.eslintrc.js b/.eslintrc.js index 78e4fb1bb33..d3379d70919 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -23,10 +23,13 @@ module.exports = { 'BROWSERSTACK_USERNAME': false, 'BROWSERSTACK_KEY': false }, + // use babel as parser for fancy syntax + parser: '@babel/eslint-parser', parserOptions: { sourceType: 'module', ecmaVersion: 2018, }, + rules: { 'comma-dangle': 'off', semi: 'off', @@ -49,5 +52,9 @@ module.exports = { rules: { 'prebid/validate-imports': ['error', allowedModules[key]] } - })) + })).concat([{ + // code in other packages (such as plugins/eslint) is not "seen" by babel and its parser will complain. + files: 'plugins/*/**/*.js', + parser: 'esprima' + }]) }; diff --git a/.github/workflows/issue_tracker.yml b/.github/workflows/issue_tracker.yml new file mode 100644 index 00000000000..4397337b4c7 --- /dev/null +++ b/.github/workflows/issue_tracker.yml @@ -0,0 +1,89 @@ +name: Issue tracking +on: + issues: + types: + - opened +jobs: + track_issue: + runs-on: ubuntu-latest + steps: + - name: Generate token + id: generate_token + uses: tibdex/github-app-token@36464acb844fc53b9b8b2401da68844f6b05ebb0 + with: + app_id: ${{ secrets.ISSUE_APP_ID }} + private_key: ${{ secrets.ISSUE_APP_PEM }} + + - name: Get project data + env: + GITHUB_TOKEN: ${{ steps.generate_token.outputs.token }} + ORGANIZATION: prebid + DATE_FIELD: Created on + PROJECT_NUMBER: 2 + run: | + gh api graphql -f query=' + query($org: String!, $number: Int!) { + organization(login: $org){ + projectNext(number: $number) { + id + fields(first:100) { + nodes { + id + name + settings + } + } + } + } + }' -f org=$ORGANIZATION -F number=$PROJECT_NUMBER > project_data.json + + echo 'PROJECT_ID='$(jq '.data.organization.projectNext.id' project_data.json) >> $GITHUB_ENV + echo 'DATE_FIELD_ID='$(jq '.data.organization.projectNext.fields.nodes[] | select(.name== "'"$DATE_FIELD"'") | .id' project_data.json) >> $GITHUB_ENV + + - name: Add issue to project + env: + GITHUB_TOKEN: ${{ steps.generate_token.outputs.token }} + ISSUE_ID: ${{ github.event.issue.node_id }} + run: | + gh api graphql -f query=' + mutation($project:ID!, $issue:ID!) { + addProjectNextItem(input: {projectId: $project, contentId: $issue}) { + projectNextItem { + id, + content { + ... on Issue { + createdAt + } + ... on PullRequest { + createdAt + } + } + } + } + }' -f project=$PROJECT_ID -f issue=$ISSUE_ID > issue_data.json + + echo 'ITEM_ID='$(jq '.data.addProjectNextItem.projectNextItem.id' issue_data.json) >> $GITHUB_ENV + echo 'ITEM_CREATION_DATE='$(jq '.data.addProjectNextItem.projectNextItem.content.createdAt' issue_data.json) >> $GITHUB_ENV + + - name: Set fields + env: + GITHUB_TOKEN: ${{ steps.generate_token.outputs.token }} + run: | + gh api graphql -f query=' + mutation ( + $project: ID! + $item: ID! + $date_field: ID! + $date_value: String! + ) { + set_creation_date: updateProjectNextItemField(input: { + projectId: $project + itemId: $item + fieldId: $date_field + value: $date_value + }) { + projectNextItem { + id + } + } + }' -f project=$PROJECT_ID -f item=$ITEM_ID -f date_field=$DATE_FIELD_ID -f date_value=$ITEM_CREATION_DATE --silent diff --git a/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/README.md b/README.md index bc0e64afa06..42d747b20b6 100644 --- a/README.md +++ b/README.md @@ -130,16 +130,22 @@ Once setup, run the following command to globally install the `gulp-cli` package ## Build for Development -To build the project on your local machine, run: +To build the project on your local machine we recommend, running: - $ gulp serve + $ gulp serve-and-test --file -This runs some code quality checks, starts a web server at `http://localhost:9999` serving from the project root and generates the following files: +This will run testing but not linting. A web server will start at `http://localhost:9999` serving from the project root and generates the following files: + `./build/dev/prebid.js` - Full source code for dev and debug + `./build/dev/prebid.js.map` - Source map for dev and debug -+ `./build/dist/prebid.js` - Minified production code -+ `./prebid.js_.zip` - Distributable zip archive ++ `./build/dev/prebid-core.js` ++ `./build/dev/prebid-core.js.map` + + +Development may be a bit slower but if you prefer linting and additional watch files you can also still run just: + + $ gulp serve + ### Build Optimization diff --git a/RELEASE_SCHEDULE.md b/RELEASE_SCHEDULE.md index bfbd0772c3e..b68495ed4ae 100644 --- a/RELEASE_SCHEDULE.md +++ b/RELEASE_SCHEDULE.md @@ -3,12 +3,7 @@ - [Release Process](#release-process) - [1. Make sure that all PRs have been named and labeled properly per the PR Process](#1-make-sure-that-all-prs-have-been-named-and-labeled-properly-per-the-pr-process) - [2. Make sure all browserstack tests are passing](#2-make-sure-all-browserstack-tests-are-passing) - - [3. Prepare Prebid Code](#3-prepare-prebid-code) - - [4. Verify the Release](#4-verify-the-release) - - [5. Create a GitHub release](#5-create-a-github-release) - - [6. Update coveralls _(skip for legacy)_](#6-update-coveralls-skip-for-legacy) - - [7. Distribute the code](#7-distribute-the-code) - - [8. Increment Version for Next Release](#8-increment-version-for-next-release) + - [3. Start the release](#3-start-the-release) - [Beta Releases](#beta-releases) - [FAQs](#faqs) @@ -21,12 +16,10 @@ it will be about a week before the Prebid Org [Download Page](http://prebid.org/ You can determine what is in a given build using the [releases page](https://github.com/prebid/Prebid.js/releases) -Announcements regarding releases will be made to the #headerbidding-dev channel in subredditadops.slack.com. +Announcements regarding releases will be made to the #prebid-js channel in prebid.slack.com. ## Release Process -_Note: If `github.com/prebid/Prebid.js` is not configured as the git origin for your repo, all of the following git commands will have to be modified to reference the proper remote (e.g. `upstream`)_ - ### 1. Make sure that all PRs have been named and labeled properly per the [PR Process](https://github.com/prebid/Prebid.js/blob/master/PR_REVIEW.md#general-pr-review-process) * Do this by checking the latest draft release from the [releases page](https://github.com/prebid/Prebid.js/releases) and make sure nothing appears in the first section called "In This Release". If they do, please open the PRs and add the appropriate labels. * Do a quick check that all the titles/descriptions look ok, and if not, adjust the PR title. @@ -57,61 +50,10 @@ _Note: If `github.com/prebid/Prebid.js` is not configured as the git origin for ``` -### 3. Prepare Prebid Code - - Update the package.json version to become the current release. Then commit your changes. - - ``` - git commit -m "Prebid 4.x.x Release" - git push - ``` - -### 4. Verify the Release - - Make sure your there are no more merges to master branch. Prebid code is clean and up to date. - -### 5. Create a GitHub release - - Edit the most recent [release notes](https://github.com/prebid/Prebid.js/releases) draft and make sure the correct version is set and the master branch is selected in the dropdown. Click `Publish release`. GitHub will create release tag. - - Pull these changes locally by running command - ``` - git pull - git fetch --tags - ``` - - and verify the tag. - -### 6. Update coveralls _(skip for legacy)_ - - We use https://coveralls.io/ to show parts of code covered by unit tests. - - Set the environment variables. You may want to add these to your `~/.bashrc` for convenience. - ``` - export COVERALLS_SERVICE_NAME="travis-ci" - export COVERALLS_REPO_TOKEN="talk to Matt Kendall" - ``` - - Run `gulp coveralls` to update code coverage history. - -### 7. Distribute the code - - _Note: do not go to step 8 until step 7 has been verified completed._ - - Reach out to any of the Appnexus folks to trigger the jenkins job. - - // TODO: - Jenkins job is moving files to appnexus cdn, pushing prebid.js to npm, purging cache and sending notification to slack. - Move all the files from Appnexus CDN to jsDelivr and create bash script to do above tasks. - -### 8. Increment Version for Next Release - - Update the version by manually editing Prebid's `package.json` to become "4.x.x-pre" (using the values for the next release). Then commit your changes. - ``` - git commit -m "Increment pre version" - git push - ``` +### 3. Start the release +Follow the instructions at https://github.com/prebid/prebidjs-releaser. Note that you will need to be a member of the [https://github.com/orgs/prebid/teams/prebidjs-release](prebidjs-release) GitHub team. + ## Beta Releases Prebid.js features may be released as Beta or as Generally Available (GA). diff --git a/allowedModules.js b/allowedModules.js index 81920cdc15f..be9a2dc2abf 100644 --- a/allowedModules.js +++ b/allowedModules.js @@ -1,12 +1,5 @@ const sharedWhiteList = [ - 'core-js-pure/features/array/find', // no ie11 - 'core-js-pure/features/array/includes', // no ie11 - 'core-js-pure/features/set', // ie11 supports Set but not Set#values - 'core-js-pure/features/string/includes', // no ie11 - 'core-js-pure/features/number/is-integer', // no ie11, - 'core-js-pure/features/array/from', // no ie11 - 'core-js-pure/web/url-search-params' // no ie11 ]; module.exports = { diff --git a/babelConfig.js b/babelConfig.js new file mode 100644 index 00000000000..c1ddc11b689 --- /dev/null +++ b/babelConfig.js @@ -0,0 +1,30 @@ + +let path = require('path'); + +function useLocal(module) { + return require.resolve(module, { + paths: [ + __dirname + ] + }) +} + +module.exports = function (test = false) { + return { + 'presets': [ + [ + useLocal('@babel/preset-env'), + { + 'useBuiltIns': 'entry', + 'corejs': '3.13.0', + // a lot of tests use sinon.stub & others that stopped working on ES6 modules with webpack 5 + 'modules': test ? 'commonjs' : 'auto', + } + ] + ], + 'plugins': [ + path.resolve(__dirname, './plugins/pbjsGlobals.js'), + useLocal('babel-plugin-transform-object-assign'), + ], + } +} diff --git a/gulpfile.js b/gulpfile.js index 6ecfee1b672..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/haloRtdProvider_example.html b/integrationExamples/gpt/hadronRtdProvider_example.html similarity index 87% rename from integrationExamples/gpt/haloRtdProvider_example.html rename to integrationExamples/gpt/hadronRtdProvider_example.html index 14debbd2698..065c8379956 100644 --- a/integrationExamples/gpt/haloRtdProvider_example.html +++ b/integrationExamples/gpt/hadronRtdProvider_example.html @@ -1,8 +1,8 @@ -Halo Id: -
+Hadron Id: +
-Halo Real-Time Data: +Hadron Real-Time Data:
diff --git a/integrationExamples/gpt/idImportLibrary_example.html b/integrationExamples/gpt/idImportLibrary_example.html index 07a4f0fe1c5..363e8015f53 100644 --- a/integrationExamples/gpt/idImportLibrary_example.html +++ b/integrationExamples/gpt/idImportLibrary_example.html @@ -69,10 +69,10 @@ name: "zeotapIdPlus" }, { - name: 'haloId', + name: 'hadronId', storage: { type: "html5", - name: "haloId", + name: "hadronId", expires: 28 } }, { diff --git a/integrationExamples/gpt/idward_segments_example.html b/integrationExamples/gpt/idward_segments_example.html new file mode 100644 index 00000000000..9bc06124c77 --- /dev/null +++ b/integrationExamples/gpt/idward_segments_example.html @@ -0,0 +1,112 @@ + + + + + + + + + + + + + +

Prebid.js Test

+
Div-1
+
+ +
+
First Party Data (ortb2) Sent to Bidding Adapter
+
+ + diff --git a/integrationExamples/gpt/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 653dd9c59f3..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": { @@ -215,10 +227,10 @@ "name": "zeotapIdPlus" }, { - "name": "haloId", + "name": "hadronId", "storage": { "type": "cookie", - "name": "haloId", + "name": "hadronId", "expires": 28 } }, @@ -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 66e4a57d2a6..b81ec52b2c4 100644 --- a/integrationExamples/gpt/weboramaRtdProvider_example.html +++ b/integrationExamples/gpt/weboramaRtdProvider_example.html @@ -1,135 +1,174 @@ - + + + - - + + - + - - -
-

-test webo ctx using prebid.js -

-
-

Basic Prebid.js Example

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

+ test webo ctx using prebid.js +

+
+

Basic Prebid.js Example

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

(.*)<\/h1>/gm, '\n\n===== $1 =====\n\n') // Header H1 + .replace(/(.*)<\/h[2-6]>/gm, '\n\n*** $1 ***\n\n') // Headers + .replace(/(<([^>]+)>)/igm, ''); // Remove any other tags + // logMessage('https://console.appnexus.com/docs/understanding-the-debug-auction'); + logMessage(debugText); + } + + return bids; + }, + + /** + * @typedef {Object} mappingFileInfo + * @property {string} url mapping file json url + * @property {number} refreshInDays prebid stores mapping data in localstorage so you can return in how many days you want to update value stored in localstorage. + * @property {string} localStorageKey unique key to store your mapping json in localstorage + */ + + /** + * Returns mapping file info. This info will be used by bidderFactory to preload mapping file and store data in local storage + * @returns {mappingFileInfo} + */ + getMappingFileInfo: function () { + return { + url: mappingFileUrl, + refreshInDays: 2 + } + }, + + getUserSyncs: function (syncOptions, responses, gdprConsent) { + if (syncOptions.iframeEnabled && hasPurpose1Consent({gdprConsent})) { + return [{ + type: 'iframe', + url: 'https://acdn.adnxs.com/dmp/async_usersync.html' + }]; + } + }, + + transformBidParams: function (params, isOpenRtb) { + params = convertTypes({ + 'member': 'string', + 'invCode': 'string', + 'placementId': 'number', + 'keywords': transformBidderParamKeywords, + 'publisherId': 'number' + }, params); + + if (isOpenRtb) { + params.use_pmt_rule = (typeof params.usePaymentRule === 'boolean') ? params.usePaymentRule : false; + if (params.usePaymentRule) { delete params.usePaymentRule; } + + if (isPopulatedArray(params.keywords)) { + params.keywords.forEach(deleteValues); + } + + Object.keys(params).forEach(paramKey => { + let convertedKey = convertCamelToUnderscore(paramKey); + if (convertedKey !== paramKey) { + params[convertedKey] = params[paramKey]; + delete params[paramKey]; + } + }); + } + + return params; + }, + + /** + * Add element selector to javascript tracker to improve native viewability + * @param {Bid} bid + */ + onBidWon: function (bid) { + if (bid.native) { + reloadViewabilityScriptWithCorrectParameters(bid); + } + } +} + +function isPopulatedArray(arr) { + return !!(isArray(arr) && arr.length > 0); +} + +function deleteValues(keyPairObj) { + if (isPopulatedArray(keyPairObj.value) && keyPairObj.value[0] === '') { + delete keyPairObj.value; + } +} + +function reloadViewabilityScriptWithCorrectParameters(bid) { + let viewJsPayload = getMediafuseViewabilityScriptFromJsTrackers(bid.native.javascriptTrackers); + + if (viewJsPayload) { + let prebidParams = 'pbjs_adid=' + bid.adId + ';pbjs_auc=' + bid.adUnitCode; + + let jsTrackerSrc = getViewabilityScriptUrlFromPayload(viewJsPayload) + + let newJsTrackerSrc = jsTrackerSrc.replace('dom_id=%native_dom_id%', prebidParams); + + // find iframe containing script tag + let frameArray = document.getElementsByTagName('iframe'); + + // boolean var to modify only one script. That way if there are muliple scripts, + // they won't all point to the same creative. + let modifiedAScript = false; + + // first, loop on all ifames + for (let i = 0; i < frameArray.length && !modifiedAScript; i++) { + let currentFrame = frameArray[i]; + try { + // IE-compatible, see https://stackoverflow.com/a/3999191/2112089 + let nestedDoc = currentFrame.contentDocument || currentFrame.contentWindow.document; + + if (nestedDoc) { + // if the doc is present, we look for our jstracker + let scriptArray = nestedDoc.getElementsByTagName('script'); + for (let j = 0; j < scriptArray.length && !modifiedAScript; j++) { + let currentScript = scriptArray[j]; + if (currentScript.getAttribute('data-src') == jsTrackerSrc) { + currentScript.setAttribute('src', newJsTrackerSrc); + currentScript.setAttribute('data-src', ''); + if (currentScript.removeAttribute) { + currentScript.removeAttribute('data-src'); + } + modifiedAScript = true; + } + } + } + } catch (exception) { + // trying to access a cross-domain iframe raises a SecurityError + // this is expected and ignored + if (!(exception instanceof DOMException && exception.name === 'SecurityError')) { + // all other cases are raised again to be treated by the calling function + throw exception; + } + } + } + } +} + +function strIsMediafuseViewabilityScript(str) { + let regexMatchUrlStart = str.match(VIEWABILITY_URL_START); + let viewUrlStartInStr = regexMatchUrlStart != null && regexMatchUrlStart.length >= 1; + + let regexMatchFileName = str.match(VIEWABILITY_FILE_NAME); + let fileNameInStr = regexMatchFileName != null && regexMatchFileName.length >= 1; + + return str.startsWith(SCRIPT_TAG_START) && fileNameInStr && viewUrlStartInStr; +} + +function getMediafuseViewabilityScriptFromJsTrackers(jsTrackerArray) { + let viewJsPayload; + if (isStr(jsTrackerArray) && strIsMediafuseViewabilityScript(jsTrackerArray)) { + viewJsPayload = jsTrackerArray; + } else if (isArray(jsTrackerArray)) { + for (let i = 0; i < jsTrackerArray.length; i++) { + let currentJsTracker = jsTrackerArray[i]; + if (strIsMediafuseViewabilityScript(currentJsTracker)) { + viewJsPayload = currentJsTracker; + } + } + } + return viewJsPayload; +} + +function getViewabilityScriptUrlFromPayload(viewJsPayload) { + // extracting the content of the src attribute + // -> substring between src=" and " + let indexOfFirstQuote = viewJsPayload.indexOf('src="') + 5; // offset of 5: the length of 'src=' + 1 + let indexOfSecondQuote = viewJsPayload.indexOf('"', indexOfFirstQuote); + let jsTrackerSrc = viewJsPayload.substring(indexOfFirstQuote, indexOfSecondQuote); + return jsTrackerSrc; +} + +function hasPurpose1Consent(bidderRequest) { + let result = true; + if (bidderRequest && bidderRequest.gdprConsent) { + if (bidderRequest.gdprConsent.gdprApplies && bidderRequest.gdprConsent.apiVersion === 2) { + result = !!(deepAccess(bidderRequest.gdprConsent, 'vendorData.purpose.consents.1') === true); + } + } + return result; +} + +function formatRequest(payload, bidderRequest) { + let request = []; + let options = { + withCredentials: true + }; + + let endpointUrl = URL; + + if (!hasPurpose1Consent(bidderRequest)) { + endpointUrl = URL_SIMPLE; + } + + if (getParameterByName('apn_test').toUpperCase() === 'TRUE' || config.getConfig('apn_test') === true) { + options.customHeaders = { + 'X-Is-Test': 1 + } + } + + if (payload.tags.length > MAX_IMPS_PER_REQUEST) { + const clonedPayload = deepClone(payload); + + chunk(payload.tags, MAX_IMPS_PER_REQUEST).forEach(tags => { + clonedPayload.tags = tags; + const payloadString = JSON.stringify(clonedPayload); + request.push({ + method: 'POST', + url: endpointUrl, + data: payloadString, + bidderRequest, + options + }); + }); + } else { + const payloadString = JSON.stringify(payload); + request = { + method: 'POST', + url: endpointUrl, + data: payloadString, + bidderRequest, + options + }; + } + + return request; +} + +function newRenderer(adUnitCode, rtbBid, rendererOptions = {}) { + const renderer = Renderer.install({ + id: rtbBid.renderer_id, + url: rtbBid.renderer_url, + config: rendererOptions, + loaded: false, + adUnitCode + }); + + try { + renderer.setRender(outstreamRender); + } catch (err) { + logWarn('Prebid Error calling setRender on renderer', err); + } + + renderer.setEventHandlers({ + impression: () => logMessage('MediaFuse outstream video impression event'), + loaded: () => logMessage('MediaFuse outstream video loaded event'), + ended: () => { + logMessage('MediaFuse outstream renderer video event'); + document.querySelector(`#${adUnitCode}`).style.display = 'none'; + } + }); + return renderer; +} + +/** + * Unpack the Server's Bid into a Prebid-compatible one. + * @param serverBid + * @param rtbBid + * @param bidderRequest + * @return Bid + */ +function newBid(serverBid, rtbBid, bidderRequest) { + const bidRequest = getBidRequest(serverBid.uuid, [bidderRequest]); + const bid = { + requestId: serverBid.uuid, + cpm: rtbBid.cpm, + creativeId: rtbBid.creative_id, + dealId: rtbBid.deal_id, + currency: 'USD', + netRevenue: true, + ttl: 300, + adUnitCode: bidRequest.adUnitCode, + mediafuse: { + buyerMemberId: rtbBid.buyer_member_id, + dealPriority: rtbBid.deal_priority, + dealCode: rtbBid.deal_code + } + }; + + // WE DON'T FULLY SUPPORT THIS ATM - future spot for adomain code; creating a stub for 5.0 compliance + if (rtbBid.adomain) { + bid.meta = Object.assign({}, bid.meta, { advertiserDomains: [] }); + } + + if (rtbBid.advertiser_id) { + bid.meta = Object.assign({}, bid.meta, { advertiserId: rtbBid.advertiser_id }); + } + + // temporary function; may remove at later date if/when adserver fully supports dchain + function setupDChain(rtbBid) { + let dchain = { + ver: '1.0', + complete: 0, + nodes: [{ + bsid: rtbBid.buyer_member_id.toString() + }], + }; + + return dchain; + } + if (rtbBid.buyer_member_id) { + bid.meta = Object.assign({}, bid.meta, {dchain: setupDChain(rtbBid)}); + } + + if (rtbBid.brand_id) { + bid.meta = Object.assign({}, bid.meta, { brandId: rtbBid.brand_id }); + } + + if (rtbBid.rtb.video) { + // shared video properties used for all 3 contexts + Object.assign(bid, { + width: rtbBid.rtb.video.player_width, + height: rtbBid.rtb.video.player_height, + vastImpUrl: rtbBid.notify_url, + ttl: 3600 + }); + + const videoContext = deepAccess(bidRequest, 'mediaTypes.video.context'); + switch (videoContext) { + case ADPOD: + const primaryCatId = getIabSubCategory(bidRequest.bidder, rtbBid.brand_category_id); + bid.meta = Object.assign({}, bid.meta, { primaryCatId }); + const dealTier = rtbBid.deal_priority; + bid.video = { + context: ADPOD, + durationSeconds: Math.floor(rtbBid.rtb.video.duration_ms / 1000), + dealTier + }; + bid.vastUrl = rtbBid.rtb.video.asset_url; + break; + case OUTSTREAM: + bid.adResponse = serverBid; + bid.adResponse.ad = bid.adResponse.ads[0]; + bid.adResponse.ad.video = bid.adResponse.ad.rtb.video; + bid.vastXml = rtbBid.rtb.video.content; + + if (rtbBid.renderer_url) { + const videoBid = find(bidderRequest.bids, bid => bid.bidId === serverBid.uuid); + const rendererOptions = deepAccess(videoBid, 'renderer.options'); + bid.renderer = newRenderer(bid.adUnitCode, rtbBid, rendererOptions); + } + break; + case INSTREAM: + bid.vastUrl = rtbBid.notify_url + '&redir=' + encodeURIComponent(rtbBid.rtb.video.asset_url); + break; + } + } else if (rtbBid.rtb[NATIVE]) { + const nativeAd = rtbBid.rtb[NATIVE]; + + // setting up the jsTracker: + // we put it as a data-src attribute so that the tracker isn't called + // until we have the adId (see onBidWon) + let jsTrackerDisarmed = rtbBid.viewability.config.replace('src=', 'data-src='); + + let jsTrackers = nativeAd.javascript_trackers; + + if (jsTrackers == undefined) { + jsTrackers = jsTrackerDisarmed; + } else if (isStr(jsTrackers)) { + jsTrackers = [jsTrackers, jsTrackerDisarmed]; + } else { + jsTrackers.push(jsTrackerDisarmed); + } + + bid[NATIVE] = { + title: nativeAd.title, + body: nativeAd.desc, + body2: nativeAd.desc2, + cta: nativeAd.ctatext, + rating: nativeAd.rating, + sponsoredBy: nativeAd.sponsored, + privacyLink: nativeAd.privacy_link, + address: nativeAd.address, + downloads: nativeAd.downloads, + likes: nativeAd.likes, + phone: nativeAd.phone, + price: nativeAd.price, + salePrice: nativeAd.saleprice, + clickUrl: nativeAd.link.url, + displayUrl: nativeAd.displayurl, + clickTrackers: nativeAd.link.click_trackers, + impressionTrackers: nativeAd.impression_trackers, + javascriptTrackers: jsTrackers + }; + if (nativeAd.main_img) { + bid['native'].image = { + url: nativeAd.main_img.url, + height: nativeAd.main_img.height, + width: nativeAd.main_img.width, + }; + } + if (nativeAd.icon) { + bid['native'].icon = { + url: nativeAd.icon.url, + height: nativeAd.icon.height, + width: nativeAd.icon.width, + }; + } + } else { + Object.assign(bid, { + width: rtbBid.rtb.banner.width, + height: rtbBid.rtb.banner.height, + ad: rtbBid.rtb.banner.content + }); + try { + if (rtbBid.rtb.trackers) { + for (let i = 0; i < rtbBid.rtb.trackers[0].impression_urls.length; i++) { + const url = rtbBid.rtb.trackers[0].impression_urls[i]; + const tracker = createTrackPixelHtml(url); + bid.ad += tracker; + } + } + } catch (error) { + logError('Error appending tracking pixel', error); + } + } + + return bid; +} + +function bidToTag(bid) { + const tag = {}; + tag.sizes = transformSizes(bid.sizes); + tag.primary_size = tag.sizes[0]; + tag.ad_types = []; + tag.uuid = bid.bidId; + if (bid.params.placementId) { + tag.id = parseInt(bid.params.placementId, 10); + } else { + tag.code = bid.params.invCode; + } + tag.allow_smaller_sizes = bid.params.allowSmallerSizes || false; + tag.use_pmt_rule = bid.params.usePaymentRule || false + tag.prebid = true; + tag.disable_psa = true; + let bidFloor = getBidFloor(bid); + if (bidFloor) { + tag.reserve = bidFloor; + } + if (bid.params.position) { + tag.position = { 'above': 1, 'below': 2 }[bid.params.position] || 0; + } + if (bid.params.trafficSourceCode) { + tag.traffic_source_code = bid.params.trafficSourceCode; + } + if (bid.params.privateSizes) { + tag.private_sizes = transformSizes(bid.params.privateSizes); + } + if (bid.params.supplyType) { + tag.supply_type = bid.params.supplyType; + } + if (bid.params.pubClick) { + tag.pubclick = bid.params.pubClick; + } + if (bid.params.extInvCode) { + tag.ext_inv_code = bid.params.extInvCode; + } + if (bid.params.publisherId) { + tag.publisher_id = parseInt(bid.params.publisherId, 10); + } + if (bid.params.externalImpId) { + tag.external_imp_id = bid.params.externalImpId; + } + if (!isEmpty(bid.params.keywords)) { + let keywords = transformBidderParamKeywords(bid.params.keywords); + + if (keywords.length > 0) { + keywords.forEach(deleteValues); + } + tag.keywords = keywords; + } + + let gpid = deepAccess(bid, 'ortb2Imp.ext.data.pbadslot'); + if (gpid) { + tag.gpid = gpid; + } + + if (bid.mediaType === NATIVE || deepAccess(bid, `mediaTypes.${NATIVE}`)) { + tag.ad_types.push(NATIVE); + if (tag.sizes.length === 0) { + tag.sizes = transformSizes([1, 1]); + } + + if (bid.nativeParams) { + const nativeRequest = buildNativeRequest(bid.nativeParams); + tag[NATIVE] = { layouts: [nativeRequest] }; + } + } + + const videoMediaType = deepAccess(bid, `mediaTypes.${VIDEO}`); + const context = deepAccess(bid, 'mediaTypes.video.context'); + + if (videoMediaType && context === 'adpod') { + tag.hb_source = 7; + } else { + tag.hb_source = 1; + } + if (bid.mediaType === VIDEO || videoMediaType) { + tag.ad_types.push(VIDEO); + } + + // instream gets vastUrl, outstream gets vastXml + if (bid.mediaType === VIDEO || (videoMediaType && context !== 'outstream')) { + tag.require_asset_url = true; + } + + if (bid.params.video) { + tag.video = {}; + // place any valid video params on the tag + Object.keys(bid.params.video) + .filter(param => includes(VIDEO_TARGETING, param)) + .forEach(param => { + switch (param) { + case 'context': + case 'playback_method': + let type = bid.params.video[param]; + type = (isArray(type)) ? type[0] : type; + tag.video[param] = VIDEO_MAPPING[param][type]; + break; + // Deprecating tags[].video.frameworks in favor of tags[].video_frameworks + case 'frameworks': + break; + default: + tag.video[param] = bid.params.video[param]; + } + }); + + if (bid.params.video.frameworks && isArray(bid.params.video.frameworks)) { + tag['video_frameworks'] = bid.params.video.frameworks; + } + } + + // use IAB ORTB values if the corresponding values weren't already set by bid.params.video + if (videoMediaType) { + tag.video = tag.video || {}; + Object.keys(videoMediaType) + .filter(param => includes(VIDEO_RTB_TARGETING, param)) + .forEach(param => { + switch (param) { + case 'minduration': + case 'maxduration': + if (typeof tag.video[param] !== 'number') tag.video[param] = videoMediaType[param]; + break; + case 'skip': + if (typeof tag.video['skippable'] !== 'boolean') tag.video['skippable'] = (videoMediaType[param] === 1); + break; + case 'skipafter': + if (typeof tag.video['skipoffset'] !== 'number') tag.video['skippoffset'] = videoMediaType[param]; + break; + case 'playbackmethod': + if (typeof tag.video['playback_method'] !== 'number') { + let type = videoMediaType[param]; + type = (isArray(type)) ? type[0] : type; + + // we only support iab's options 1-4 at this time. + if (type >= 1 && type <= 4) { + tag.video['playback_method'] = type; + } + } + break; + case 'api': + if (!tag['video_frameworks'] && isArray(videoMediaType[param])) { + // need to read thru array; remove 6 (we don't support it), swap 4 <> 5 if found (to match our adserver mapping for these specific values) + let apiTmp = videoMediaType[param].map(val => { + let v = (val === 4) ? 5 : (val === 5) ? 4 : val; + + if (v >= 1 && v <= 5) { + return v; + } + }).filter(v => v); + tag['video_frameworks'] = apiTmp; + } + break; + } + }); + } + + if (bid.renderer) { + tag.video = Object.assign({}, tag.video, { custom_renderer_present: true }); + } + + if (bid.params.frameworks && isArray(bid.params.frameworks)) { + tag['banner_frameworks'] = bid.params.frameworks; + } + + let adUnit = find(auctionManager.getAdUnits(), au => bid.transactionId === au.transactionId); + if (adUnit && adUnit.mediaTypes && adUnit.mediaTypes.banner) { + tag.ad_types.push(BANNER); + } + + if (tag.ad_types.length === 0) { + delete tag.ad_types; + } + + return tag; +} + +/* Turn bid request sizes into ut-compatible format */ +function transformSizes(requestSizes) { + let sizes = []; + let sizeObj = {}; + + if (isArray(requestSizes) && requestSizes.length === 2 && + !isArray(requestSizes[0])) { + sizeObj.width = parseInt(requestSizes[0], 10); + sizeObj.height = parseInt(requestSizes[1], 10); + sizes.push(sizeObj); + } else if (typeof requestSizes === 'object') { + for (let i = 0; i < requestSizes.length; i++) { + let size = requestSizes[i]; + sizeObj = {}; + sizeObj.width = parseInt(size[0], 10); + sizeObj.height = parseInt(size[1], 10); + sizes.push(sizeObj); + } + } + + return sizes; +} + +function hasUserInfo(bid) { + return !!bid.params.user; +} + +function hasMemberId(bid) { + return !!parseInt(bid.params.member, 10); +} + +function hasAppDeviceInfo(bid) { + if (bid.params) { + return !!bid.params.app + } +} + +function hasAppId(bid) { + if (bid.params && bid.params.app) { + return !!bid.params.app.id + } + return !!bid.params.app +} + +function hasDebug(bid) { + return !!bid.debug +} + +function hasAdPod(bid) { + return ( + bid.mediaTypes && + bid.mediaTypes.video && + bid.mediaTypes.video.context === ADPOD + ); +} + +function hasOmidSupport(bid) { + let hasOmid = false; + const bidderParams = bid.params; + const videoParams = bid.params.video; + if (bidderParams.frameworks && isArray(bidderParams.frameworks)) { + hasOmid = includes(bid.params.frameworks, 6); + } + if (!hasOmid && videoParams && videoParams.frameworks && isArray(videoParams.frameworks)) { + hasOmid = includes(bid.params.video.frameworks, 6); + } + return hasOmid; +} + +/** + * Expand an adpod placement into a set of request objects according to the + * total adpod duration and the range of duration seconds. Sets minduration/ + * maxduration video property according to requireExactDuration configuration + */ +function createAdPodRequest(tags, adPodBid) { + const { durationRangeSec, requireExactDuration } = adPodBid.mediaTypes.video; + + const numberOfPlacements = getAdPodPlacementNumber(adPodBid.mediaTypes.video); + const maxDuration = getMaxValueFromArray(durationRangeSec); + + const tagToDuplicate = tags.filter(tag => tag.uuid === adPodBid.bidId); + let request = fill(...tagToDuplicate, numberOfPlacements); + + if (requireExactDuration) { + const divider = Math.ceil(numberOfPlacements / durationRangeSec.length); + const chunked = chunk(request, divider); + + // each configured duration is set as min/maxduration for a subset of requests + durationRangeSec.forEach((duration, index) => { + chunked[index].map(tag => { + setVideoProperty(tag, 'minduration', duration); + setVideoProperty(tag, 'maxduration', duration); + }); + }); + } else { + // all maxdurations should be the same + request.map(tag => setVideoProperty(tag, 'maxduration', maxDuration)); + } + + return request; +} + +function getAdPodPlacementNumber(videoParams) { + const { adPodDurationSec, durationRangeSec, requireExactDuration } = videoParams; + const minAllowedDuration = getMinValueFromArray(durationRangeSec); + const numberOfPlacements = Math.floor(adPodDurationSec / minAllowedDuration); + + return requireExactDuration + ? Math.max(numberOfPlacements, durationRangeSec.length) + : numberOfPlacements; +} + +function setVideoProperty(tag, key, value) { + if (isEmpty(tag.video)) { tag.video = {}; } + tag.video[key] = value; +} + +function getRtbBid(tag) { + return tag && tag.ads && tag.ads.length && find(tag.ads, ad => ad.rtb); +} + +function buildNativeRequest(params) { + const request = {}; + + // map standard prebid native asset identifier to /ut parameters + // e.g., tag specifies `body` but /ut only knows `description`. + // mapping may be in form {tag: ''} or + // {tag: {serverName: '', requiredParams: {...}}} + Object.keys(params).forEach(key => { + // check if one of the forms is used, otherwise + // a mapping wasn't specified so pass the key straight through + const requestKey = + (NATIVE_MAPPING[key] && NATIVE_MAPPING[key].serverName) || + NATIVE_MAPPING[key] || + key; + + // required params are always passed on request + const requiredParams = NATIVE_MAPPING[key] && NATIVE_MAPPING[key].requiredParams; + request[requestKey] = Object.assign({}, requiredParams, params[key]); + + // convert the sizes of image/icon assets to proper format (if needed) + const isImageAsset = !!(requestKey === NATIVE_MAPPING.image.serverName || requestKey === NATIVE_MAPPING.icon.serverName); + if (isImageAsset && request[requestKey].sizes) { + let sizes = request[requestKey].sizes; + if (isArrayOfNums(sizes) || (isArray(sizes) && sizes.length > 0 && sizes.every(sz => isArrayOfNums(sz)))) { + request[requestKey].sizes = transformSizes(request[requestKey].sizes); + } + } + + if (requestKey === NATIVE_MAPPING.privacyLink) { + request.privacy_supported = true; + } + }); + + return request; +} + +/** + * This function hides google div container for outstream bids to remove unwanted space on page. Mediafuse renderer creates a new iframe outside of google iframe to render the outstream creative. + * @param {string} elementId element id + */ +function hidedfpContainer(elementId) { + var el = document.getElementById(elementId).querySelectorAll("div[id^='google_ads']"); + if (el[0]) { + el[0].style.setProperty('display', 'none'); + } +} + +function hideSASIframe(elementId) { + try { + // find script tag with id 'sas_script'. This ensures it only works if you're using Smart Ad Server. + const el = document.getElementById(elementId).querySelectorAll("script[id^='sas_script']"); + if (el[0].nextSibling && el[0].nextSibling.localName === 'iframe') { + el[0].nextSibling.style.setProperty('display', 'none'); + } + } catch (e) { + // element not found! + } +} + +function outstreamRender(bid) { + hidedfpContainer(bid.adUnitCode); + hideSASIframe(bid.adUnitCode); + // push to render queue because ANOutstreamVideo may not be loaded yet + bid.renderer.push(() => { + window.ANOutstreamVideo.renderAd({ + tagId: bid.adResponse.tag_id, + sizes: [bid.getSize().split('x')], + targetId: bid.adUnitCode, // target div id to render video + uuid: bid.adResponse.uuid, + adResponse: bid.adResponse, + rendererOptions: bid.renderer.getConfig() + }, handleOutstreamRendererEvents.bind(null, bid)); + }); +} + +function handleOutstreamRendererEvents(bid, id, eventName) { + bid.renderer.handleVideoEvent({ id, eventName }); +} + +function parseMediaType(rtbBid) { + const adType = rtbBid.ad_type; + if (adType === VIDEO) { + return VIDEO; + } else if (adType === NATIVE) { + return NATIVE; + } else { + return BANNER; + } +} + +function addUserId(eids, id, source, rti) { + if (id) { + if (rti) { + eids.push({ source, id, rti_partner: rti }); + } else { + eids.push({ source, id }); + } + } + return eids; +} + +function getBidFloor(bid) { + if (!isFn(bid.getFloor)) { + return (bid.params.reserve) ? bid.params.reserve : null; + } + + let floor = bid.getFloor({ + currency: 'USD', + mediaType: '*', + size: '*' + }); + if (isPlainObject(floor) && !isNaN(floor.floor) && floor.currency === 'USD') { + return floor.floor; + } + return null; +} + +registerBidder(spec); diff --git a/modules/mediafuseBidAdapter.md b/modules/mediafuseBidAdapter.md new file mode 100644 index 00000000000..f9ed9835b94 --- /dev/null +++ b/modules/mediafuseBidAdapter.md @@ -0,0 +1,151 @@ +# Overview + +``` +Module Name: Mediafuse Bid Adapter +Module Type: Bidder Adapter +Maintainer: prebid-js@xandr.com +``` + +# Description + +Connects to Mediafuse exchange for bids. + +Mediafuse bid adapter supports Banner, Video (instream and outstream) and Native. + +# Test Parameters +``` +var adUnits = [ + // Banner adUnit + { + code: 'banner-div', + mediaTypes: { + banner: { + sizes: [[300, 250], [300,600]] + } + }, + bids: [{ + bidder: 'mediafuse', + params: { + placementId: 13144370 + } + }] + }, + // Native adUnit + { + code: 'native-div', + sizes: [[1, 1]], + mediaTypes: { + native: { + title: { + required: true + }, + body: { + required: true + }, + image: { + required: true + }, + sponsoredBy: { + required: true + }, + icon: { + required: false + } + } + }, + bids: [{ + bidder: 'mediafuse', + params: { + placementId: 13232354, + allowSmallerSizes: true + } + }] + }, + // Video instream adUnit + { + code: 'video-instream', + sizes: [[640, 480]], + mediaTypes: { + video: { + playerSize: [[640, 480]], + context: 'instream' + }, + }, + bids: [{ + bidder: 'mediafuse', + params: { + placementId: 13232361, + video: { + skippable: true, + playback_methods: ['auto_play_sound_off'] + } + } + }] + }, + // Video outstream adUnit + { + code: 'video-outstream', + sizes: [[300, 250]], + mediaTypes: { + video: { + playerSize: [[300, 250]], + context: 'outstream', + // Certain ORTB 2.5 video values can be read from the mediatypes object; below are examples of supported params. + // To note - mediafuse supports additional values for our system that are not part of the ORTB spec. If you want + // to use these values, they will have to be declared in the bids[].params.video object instead using the mediafuse syntax. + // Between the corresponding values of the mediaTypes.video and params.video objects, the properties in params.video will + // take precedence if declared; eg in the example below, the `skippable: true` setting will be used instead of the `skip: 0`. + minduration: 1, + maxduration: 60, + skip: 0, // 1 - true, 0 - false + skipafter: 5, + playbackmethod: [2], // note - we only support options 1-4 at this time + api: [1,2,3] // note - option 6 is not supported at this time + } + }, + bids: [ + { + bidder: 'mediafuse', + params: { + placementId: 13232385, + video: { + skippable: true, + playback_method: 'auto_play_sound_off' + } + } + } + ] + }, + // Banner adUnit in a App Webview + // Only use this for situations where prebid.js is in a webview of an App + // See Prebid Mobile for displaying ads via an SDK + { + code: 'banner-div', + mediaTypes: { + banner: { + sizes: [[300, 250], [300,600]] + } + } + bids: [{ + bidder: 'mediafuse', + params: { + placementId: 13144370, + app: { + id: "B1O2W3M4AN.com.prebid.webview", + geo: { + lat: 40.0964439, + lng: -75.3009142 + }, + device_id: { + idfa: "4D12078D-3246-4DA4-AD5E-7610481E7AE", // Apple advertising identifier + aaid: "38400000-8cf0-11bd-b23e-10b96e40000d", // Android advertising identifier + md5udid: "5756ae9022b2ea1e47d84fead75220c8", // MD5 hash of the ANDROID_ID + sha1udid: "4DFAA92388699AC6539885AEF1719293879985BF", // SHA1 hash of the ANDROID_ID + windowsadid: "750c6be243f1c4b5c9912b95a5742fc5" // Windows advertising identifier + } + } + } + }] + } +]; +``` diff --git a/modules/mediakeysBidAdapter.js b/modules/mediakeysBidAdapter.js index 72dca2f1add..5eb32a3f6e4 100644 --- a/modules/mediakeysBidAdapter.js +++ b/modules/mediakeysBidAdapter.js @@ -1,10 +1,28 @@ -import find from 'core-js-pure/features/array/find.js'; -import arrayFrom from 'core-js-pure/features/array/from'; -import { getWindowTop, isFn, logWarn, getDNT, deepAccess, isArray, inIframe, mergeDeep, isStr, isEmpty, deepSetValue, deepClone, parseUrl, cleanObj, logError, triggerPixel, isInteger, isNumber } from '../src/utils.js'; -import { registerBidder } from '../src/adapters/bidderFactory.js'; -import { config } from '../src/config.js'; -import { BANNER, NATIVE, VIDEO } from '../src/mediaTypes.js'; -import { createEidsArray } from './userId/eids.js'; +import {arrayFrom, find} from '../src/polyfill.js'; +import { + cleanObj, + deepAccess, + deepClone, + deepSetValue, + getDNT, + getWindowTop, + inIframe, + isArray, + isEmpty, + isFn, + isInteger, + isNumber, + isStr, + logError, + logWarn, + mergeDeep, + parseUrl, + triggerPixel +} from '../src/utils.js'; +import {registerBidder} from '../src/adapters/bidderFactory.js'; +import {config} from '../src/config.js'; +import {BANNER, NATIVE, VIDEO} from '../src/mediaTypes.js'; +import {createEidsArray} from './userId/eids.js'; const AUCTION_TYPE = 1; const BIDDER_CODE = 'mediakeys'; @@ -653,11 +671,6 @@ export const spec = { if (fpd.user) { mergeDeep(payload, { user: fpd.user }); } - // Here we can handle device.geo prop - const deviceGeo = deepAccess(fpd, 'device.geo'); - if (deviceGeo) { - mergeDeep(payload.device, { geo: deviceGeo }); - } const request = { method: 'POST', diff --git a/modules/medianetAnalyticsAdapter.js b/modules/medianetAnalyticsAdapter.js index e281dde8ad0..09ebbc9bc31 100644 --- a/modules/medianetAnalyticsAdapter.js +++ b/modules/medianetAnalyticsAdapter.js @@ -1,11 +1,23 @@ -import { triggerPixel, deepAccess, getWindowTop, uniques, groupBy, isEmpty, _map, isPlainObject, logInfo, logError } from '../src/utils.js'; +import { + _map, + deepAccess, + getWindowTop, + groupBy, + isEmpty, + isPlainObject, + logError, + logInfo, + triggerPixel, + uniques, + getHighestCpm +} from '../src/utils.js'; import adapter from '../src/AnalyticsAdapter.js'; import adapterManager from '../src/adapterManager.js'; import CONSTANTS from '../src/constants.json'; -import { ajax } from '../src/ajax.js'; -import { getRefererInfo } from '../src/refererDetection.js'; -import { AUCTION_COMPLETED, AUCTION_IN_PROGRESS, getPriceGranularity } from '../src/auction.js'; -import includes from 'core-js-pure/features/array/includes.js'; +import {ajax} from '../src/ajax.js'; +import {getRefererInfo} from '../src/refererDetection.js'; +import {AUCTION_COMPLETED, AUCTION_IN_PROGRESS, getPriceGranularity} from '../src/auction.js'; +import {includes} from '../src/polyfill.js'; const analyticsType = 'endpoint'; const ENDPOINT = 'https://pb-logs.media.net/log?logid=kfk&evtid=prebid_analytics_events_client'; @@ -490,6 +502,27 @@ function _getSizes(mediaTypes, sizes) { } } +/* + - The code is used to determine if the current bid is higher than the previous bid. + - If it is, then the code will return true and if not, it will return false. + */ +function canSelectCurrentBid(previousBid, currentBid) { + if (!(previousBid instanceof Bid)) return false; + + // For first bid response the previous bid will be containing bid request obj + // in which the cpm would be undefined so the current bid can directly be selected. + const isFirstBidResponse = previousBid.cpm === undefined && currentBid.cpm !== undefined; + if (isFirstBidResponse) return true; + + // if there are 2 bids, get the highest bid + const selectedBid = getHighestCpm(previousBid, currentBid); + + // Return true if selectedBid is currentBid, + // The timeToRespond field is used as an identifier for distinguishing + // between the current iterating bid and the previous bid. + return selectedBid.timeToRespond === currentBid.timeToRespond; +} + function bidResponseHandler(bid) { const { width, height, mediaType, cpm, requestId, timeToRespond, auctionId, dealId } = bid; const {originalCpm, bidderCode, creativeId, adId, currency} = bid; @@ -498,7 +531,7 @@ function bidResponseHandler(bid) { return; } let bidObj = auctions[auctionId].findBid('bidId', requestId); - if (!(bidObj instanceof Bid)) { + if (!canSelectCurrentBid(bidObj, bid)) { return; } Object.assign( @@ -511,7 +544,7 @@ function bidResponseHandler(bid) { bidObj.originalCpm = originalCpm || cpm; let dfpbd = deepAccess(bid, 'adserverTargeting.hb_pb'); if (!dfpbd) { - let priceGranularity = getPriceGranularity(mediaType, bid); + let priceGranularity = getPriceGranularity(bid); let priceGranularityKey = PRICE_GRANULARITY[priceGranularity]; dfpbd = bid[priceGranularityKey] || cpm; } diff --git a/modules/medianetRtdProvider.js b/modules/medianetRtdProvider.js index cd86bf891f3..07b1d66fbc5 100644 --- a/modules/medianetRtdProvider.js +++ b/modules/medianetRtdProvider.js @@ -1,7 +1,7 @@ -import { isStr, isEmptyStr, logError, mergeDeep, isFn, insertElement } from '../src/utils.js'; -import { submodule } from '../src/hook.js'; -import { getGlobal } from '../src/prebidGlobal.js'; -import includes from 'core-js-pure/features/array/includes.js'; +import {insertElement, isEmptyStr, isFn, isStr, logError, mergeDeep} from '../src/utils.js'; +import {submodule} from '../src/hook.js'; +import {getGlobal} from '../src/prebidGlobal.js'; +import {includes} from '../src/polyfill.js'; const MODULE_NAME = 'medianet'; const SOURCE = MODULE_NAME + 'rtd'; diff --git a/modules/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 1043c7cc3e6..427a16f1341 100644 --- a/modules/mediasquareBidAdapter.js +++ b/modules/mediasquareBidAdapter.js @@ -11,6 +11,7 @@ const BIDDER_ENDPOINT_WINNING = 'winning'; export const spec = { code: BIDDER_CODE, + gvlid: 791, aliases: ['msq'], // short code supportedMediaTypes: [BANNER, NATIVE, VIDEO], /** @@ -31,10 +32,16 @@ export const spec = { buildRequests: function(validBidRequests, bidderRequest) { let codes = []; let endpoint = document.location.search.match(/msq_test=true/) ? BIDDER_URL_TEST : BIDDER_URL_PROD; + let floor = {}; const test = config.getConfig('debug') ? 1 : 0; let adunitValue = null; Object.keys(validBidRequests).forEach(key => { adunitValue = validBidRequests[key]; + if (typeof adunitValue.getFloor === 'function') { + floor = adunitValue.getFloor({currency: 'EUR', mediaType: '*', size: '*'}); + } else { + floor = {}; + } codes.push({ owner: adunitValue.params.owner, code: adunitValue.params.code, @@ -42,12 +49,14 @@ export const spec = { bidId: adunitValue.bidId, auctionId: adunitValue.auctionId, transactionId: adunitValue.transactionId, - mediatypes: adunitValue.mediaTypes + mediatypes: adunitValue.mediaTypes, + floor: floor }); }); const payload = { codes: codes, - referer: encodeURIComponent(bidderRequest.refererInfo.referer) + referer: encodeURIComponent(bidderRequest.refererInfo.referer), + pbjs: '$prebid.version$' }; if (bidderRequest) { // modules informations (gdpr, ccpa, schain, userId) if (bidderRequest.gdprConsent) { @@ -108,6 +117,9 @@ export const spec = { if ('match' in value) { bidResponse['mediasquare']['match'] = value['match']; } + if ('hasConsent' in value) { + bidResponse['mediasquare']['hasConsent'] = value['hasConsent']; + } if ('native' in value) { bidResponse['native'] = value['native']; bidResponse['mediaType'] = 'native'; @@ -145,19 +157,22 @@ export const spec = { */ onBidWon: function(bid) { // fires a pixel to confirm a winning bid - let params = []; + let params = {'pbjs': '$prebid.version$'}; let endpoint = document.location.search.match(/msq_test=true/) ? BIDDER_URL_TEST : BIDDER_URL_PROD; let paramsToSearchFor = ['cpm', 'size', 'mediaType', 'currency', 'creativeId', 'adUnitCode', 'timeToRespond', 'requestId', 'auctionId'] if (bid.hasOwnProperty('mediasquare')) { - if (bid['mediasquare'].hasOwnProperty('bidder')) { params.push('bidder=' + bid['mediasquare']['bidder']); } - if (bid['mediasquare'].hasOwnProperty('code')) { params.push('code=' + bid['mediasquare']['code']); } - if (bid['mediasquare'].hasOwnProperty('match')) { params.push('match=' + bid['mediasquare']['match']); } + if (bid['mediasquare'].hasOwnProperty('bidder')) { params['bidder'] = bid['mediasquare']['bidder']; } + if (bid['mediasquare'].hasOwnProperty('code')) { params['code'] = bid['mediasquare']['code']; } + if (bid['mediasquare'].hasOwnProperty('match')) { params['match'] = bid['mediasquare']['match']; } + if (bid['mediasquare'].hasOwnProperty('hasConsent')) { params['hasConsent'] = bid['mediasquare']['hasConsent']; } }; for (let i = 0; i < paramsToSearchFor.length; i++) { - if (bid.hasOwnProperty(paramsToSearchFor[i])) { params.push(paramsToSearchFor[i] + '=' + bid[paramsToSearchFor[i]]); } + if (bid.hasOwnProperty(paramsToSearchFor[i])) { + params[paramsToSearchFor[i]] = bid[paramsToSearchFor[i]]; + if (typeof params[paramsToSearchFor[i]] == 'number') { params[paramsToSearchFor[i]] = params[paramsToSearchFor[i]].toString() } + } } - if (params.length > 0) { params = '?' + params.join('&'); } - ajax(endpoint + BIDDER_ENDPOINT_WINNING + params, null, undefined, {method: 'GET', withCredentials: true}); + ajax(endpoint + BIDDER_ENDPOINT_WINNING, null, JSON.stringify(params), {method: 'POST', withCredentials: true}); return true; } diff --git a/modules/mgidBidAdapter.js b/modules/mgidBidAdapter.js index c811a0b2981..51b713c8958 100644 --- a/modules/mgidBidAdapter.js +++ b/modules/mgidBidAdapter.js @@ -7,7 +7,7 @@ import { getStorageManager } from '../src/storageManager.js'; const GVLID = 358; const DEFAULT_CUR = 'USD'; const BIDDER_CODE = 'mgid'; -export const storage = getStorageManager(GVLID, BIDDER_CODE); +export const storage = getStorageManager({gvlid: GVLID, bidderCode: BIDDER_CODE}); const ENDPOINT_URL = 'https://prebid.mgid.com/prebid/'; const LOG_WARN_PREFIX = '[MGID warn]: '; const LOG_INFO_PREFIX = '[MGID info]: '; diff --git a/modules/minutemediaBidAdapter.js b/modules/minutemediaBidAdapter.js new file mode 100644 index 00000000000..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/nativoBidAdapter.js b/modules/nativoBidAdapter.js index c9e6a1f659f..e07a124665f 100644 --- a/modules/nativoBidAdapter.js +++ b/modules/nativoBidAdapter.js @@ -12,8 +12,71 @@ const TIME_TO_LIVE = 360 const SUPPORTED_AD_TYPES = [BANNER] +/** + * Keep track of bid data by keys + * @returns {Object} - Map of bid data that can be referenced by multiple keys + */ +const BidDataMap = () => { + const referenceMap = {} + const bids = [] + + /** + * Add a refence to the index by key value + * @param {String} key - The key to store the index reference + * @param {Integer} index - The index value of the bidData + */ + function adKeyReference(key, index) { + if (!referenceMap.hasOwnProperty(key)) { + referenceMap[key] = index + } + } + + /** + * Adds a bid to the map + * @param {Object} bid - Bid data + * @param {Array/String} keys - Keys to reference the index value + */ + function addBidData(bid, keys) { + const index = bids.length + bids.push(bid) + + if (Array.isArray(keys)) { + keys.forEach((key) => { + adKeyReference(String(key), index) + }) + return + } + + adKeyReference(String(keys), index) + } + + /** + * Get's the bid data refrerenced by the key + * @param {String} key - The key value to find the bid data by + * @returns {Object} - The bid data + */ + function getBidData(key) { + const stringKey = String(key) + if (referenceMap.hasOwnProperty(stringKey)) { + return bids[referenceMap[stringKey]] + } + } + + // Return API + return { + addBidData, + getBidData, + } +} + const bidRequestMap = {} const adUnitsRequested = {} +const extData = {} + +// Filtering +const adsToFilter = new Set() +const advertisersToFilter = new Set() +const campaignsToFilter = new Set() // Prebid adapter referrence doc: https://docs.prebid.org/dev-docs/bidder-adaptor.html @@ -45,7 +108,7 @@ export const spec = { if (!bid.params) return bid.bidder === BIDDER_CODE // Check if any supplied parameters are invalid - const hasInvalidParameters = Object.keys(bid.params).some(key => { + const hasInvalidParameters = Object.keys(bid.params).some((key) => { const value = bid.params[key] const validityCheck = validParameter[key] @@ -69,8 +132,8 @@ export const spec = { */ buildRequests: function (validBidRequests, bidderRequest) { const placementIds = new Set() - const placmentBidIdMap = {} let placementId, pageUrl + const bidDataMap = BidDataMap() validBidRequests.forEach((request) => { pageUrl = deepAccess( request, @@ -83,13 +146,13 @@ export const spec = { placementIds.add(placementId) } - var key = placementId || request.adUnitCode - placmentBidIdMap[key] = { + const bidData = { bidId: request.bidId, size: getLargestSize(request.sizes), } + bidDataMap.addBidData(bidData, [placementId, request.adUnitCode]) }) - bidRequestMap[bidderRequest.bidderRequestId] = placmentBidIdMap + bidRequestMap[bidderRequest.bidderRequestId] = bidDataMap // Build adUnit data const adUnitData = { @@ -123,6 +186,20 @@ export const spec = { }, ] + // Add filtering + if (adsToFilter.size > 0) { + params.unshift({ key: 'ntv_atf', value: Array.from(adsToFilter).join(',') }) + } + + if (advertisersToFilter.size > 0) { + params.unshift({ key: 'ntv_avtf', value: Array.from(advertisersToFilter).join(',') }) + } + + if (campaignsToFilter.size > 0) { + params.unshift({ key: 'ntv_ctf', value: Array.from(campaignsToFilter).join(',') }) + } + + // Add placement IDs if (placementIds.size > 0) { // Convert Set to Array (IE 11 Safe) const placements = [] @@ -131,6 +208,7 @@ export const spec = { params.unshift({ key: 'ntv_ptd', value: placements.join(',') }) } + // Add GDPR params if (bidderRequest.gdprConsent) { // Put on the beginning of the qs param array params.unshift({ @@ -139,6 +217,7 @@ export const spec = { }) } + // Add USP params if (bidderRequest.uspConsent) { // Put on the beginning of the qs param array params.unshift({ key: 'us_privacy', value: bidderRequest.uspConsent }) @@ -195,6 +274,8 @@ export const spec = { }, } + if (bid.ext) extData[bid.id] = bid.ext + bidResponses.push(bidResponse) }) }) @@ -300,7 +381,15 @@ export const spec = { * Will be called when a bid from the adapter won the auction. * @param {Object} bid - The bid that won the auction */ - onBidWon: function (bid) {}, + onBidWon: function (bid) { + const ext = extData[bid.dealId] + + if (!ext) return + + appendFilterData(adsToFilter, ext.adsToFilter) + appendFilterData(advertisersToFilter, ext.advertisersToFilter) + appendFilterData(campaignsToFilter, ext.campaignsToFilter) + }, /** * Will be called when the adserver targeting has been set for a bid from the adapter. @@ -315,12 +404,14 @@ export const spec = { * @returns {String} - The bidId value associated with the corresponding placementId */ getAdUnitData: function (bidderRequestId, bid) { - var data = deepAccess(bidRequestMap, `${bidderRequestId}.${bid.impid}`) + const bidDataMap = bidRequestMap[bidderRequestId] - if (data) return data + const placementId = bid.impid + const adUnitCode = deepAccess(bid, 'ext.ad_unit_id') - var unitCode = deepAccess(bid, 'ext.ad_unit_id') - return deepAccess(bidRequestMap, `${bidderRequestId}.${unitCode}`) + return ( + bidDataMap.getBidData(adUnitCode) || bidDataMap.getBidData(placementId) + ) }, } registerBidder(spec) @@ -375,3 +466,14 @@ function getLargestSize(sizes, method = area) { * @returns The calculated area */ const area = (size) => size[0] * size[1] + +/** + * Save any filter data from winning bid requests for subsequent requests + * @param {Array} filter - The filter data bucket currently stored + * @param {Array} filterData - The filter data to add + */ +function appendFilterData(filter, filterData) { + if (filterData && Array.isArray(filterData) && filterData.length) { + filterData.forEach((ad) => filter.add(ad)) + } +} diff --git a/modules/nextMillenniumBidAdapter.js b/modules/nextMillenniumBidAdapter.js index 4791740bf2f..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 }); }); @@ -109,6 +117,7 @@ export const spec = { let bidders = [] if (responses) { _each(responses, (response) => { + if (!(response && response.body && response.body.ext && response.body.ext.responsetimemillis)) return _each(Object.keys(response.body.ext.responsetimemillis), b => bidders.push(b)) }) } @@ -124,4 +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 new file mode 100644 index 00000000000..814a2f55299 --- /dev/null +++ b/modules/nexx360BidAdapter.js @@ -0,0 +1,175 @@ +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, 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 GVLID = 965; + +export const spec = { + code: BIDDER_CODE, + gvlid: GVLID, + aliases: ['revenuemaker'], // short code + supportedMediaTypes: [BANNER, VIDEO], + /** + * Determines whether or not the given bid request is valid. + * + * @param {BidRequest} bid The bid params to validate. + * @return boolean True if this is a valid bid, and false otherwise. + */ + 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); + }, + /** + * Make a server request from the list of BidRequests. + * + * @param {validBidRequests[]} - an array of bids + * @return ServerRequest Info describing the request to the server. + */ + buildRequests: function(validBidRequests, bidderRequest) { + const adUnits = []; + const test = config.getConfig('debug') ? 1 : 0; + let adunitValue = null; + let userEids = null; + Object.keys(validBidRequests).forEach(key => { + adunitValue = validBidRequests[key]; + 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, + 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 = { + adUnits, + href: encodeURIComponent(bidderRequest.refererInfo.referer) + }; + if (bidderRequest) { // modules informations (gdpr, ccpa, schain, userId) + if (bidderRequest.gdprConsent) { + payload.gdpr = bidderRequest.gdprConsent.gdprApplies ? 1 : 0; + payload.gdprConsent = bidderRequest.gdprConsent.consentString; + } else { + payload.gdpr = 0; + payload.gdprConsent = ''; + } + if (bidderRequest.uspConsent) { payload.uspConsent = bidderRequest.uspConsent; } + if (bidderRequest.schain) { payload.schain = bidderRequest.schain; } + if (userEids !== null) payload.userEids = userEids; + }; + if (test) payload.test = 1; + const payloadString = JSON.stringify(payload); + return { + method: 'POST', + url: BIDDER_URL, + data: payloadString, + }; + }, + /** + * Unpack the response from the server into a list of bids. + * + * @param {ServerResponse} serverResponse A successful response from the server. + * @return {Bid[]} An array of bids which were nested inside the server. + */ + interpretResponse: function(serverResponse, bidRequest) { + const serverBody = serverResponse.body; + const 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'], + ttl: value['ttl'], + creativeId: value['creativeId'], + netRevenue: true, + nexx360: { + 'ssp': value['bidder'], + 'consent': value['consent'], + 'tagId': value['tagId'] + }, + /* + meta: { + 'advertiserDomains': value['adomain'] + } + */ + }; + 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); + }); + } + return bidResponses; + }, + + /** + * Register the user sync pixels which should be dropped after the auction. + * + * @param {SyncOptions} syncOptions Which user syncs are allowed? + * @param {ServerResponse[]} serverResponses List of server's responses. + * @return {UserSync[]} The user syncs which should be dropped. + */ + getUserSyncs: function(syncOptions, serverResponses, gdprConsent, uspConsent) { + if (typeof serverResponses === 'object' && serverResponses != null && serverResponses.length > 0 && serverResponses[0].hasOwnProperty('body') && + serverResponses[0].body.hasOwnProperty('cookies') && typeof serverResponses[0].body.cookies === 'object') { + return serverResponses[0].body.cookies.slice(0, 5); + } else { + return []; + } + }, + + /** + * Register bidder specific code, which will execute if a bid from this bidder won the auction + * @param {Bid} The bid that won the auction + */ + onBidWon: function(bid) { + // fires a pixel to confirm a winning bid + const params = { type: 'prebid', 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; + if (bid.nexx360.hasOwnProperty('consent')) params.consent = bid.nexx360.consent; + }; + params.price = bid.cpm; + const url = `${METRICS_TRACKER_URL}?${new URLSearchParams(params).toString()}`; + ajax(url, null, undefined, {method: 'GET', withCredentials: true}); + return true; + } + +} +registerBidder(spec); diff --git a/modules/nexx360BidAdapter.md b/modules/nexx360BidAdapter.md new file mode 100644 index 00000000000..532d48418b6 --- /dev/null +++ b/modules/nexx360BidAdapter.md @@ -0,0 +1,59 @@ +# Overview + +``` +Module Name: Nexx360 Bid Adapter +Module Type: Bidder Adapter +Maintainer: gabriel@nexx360.io +``` + +# Description + +Connects to Nexx360 network for bids. + +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 + { + code: 'banner-div', + mediaTypes: { + banner: { + sizes: [[300, 250], [300,600]] + } + }, + bids: [{ + bidder: 'nexx360', + params: { + account: '1067', + tagId: 'luvxjvgn' + } + }] + }, +]; +``` + +### 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 4c3324d3fc0..ae9cc4c818f 100644 --- a/modules/novatiqIdSystem.js +++ b/modules/novatiqIdSystem.js @@ -5,24 +5,25 @@ * @requires module:modules/userId */ -import { logInfo } from '../src/utils.js'; +import { logInfo, getWindowLocation } from '../src/utils.js'; import { ajax } from '../src/ajax.js'; import { submodule } from '../src/hook.js'; +import { getStorageManager } from '../src/storageManager.js'; /** @type {Submodule} */ export const novatiqIdSubmodule = { -/** -* used to link submodule with config -* @type {string} -*/ + /** + * used to link submodule with config + * @type {string} + */ name: 'novatiq', /** -* decode the stored id value for passing to bid requests -* @function -* @returns {novatiq: {snowflake: string}} -*/ + * decode the stored id value for passing to bid requests + * @function + * @returns {novatiq: {snowflake: string}} + */ decode(novatiqId, config) { let responseObj = { novatiq: { @@ -33,42 +34,202 @@ export const novatiqIdSubmodule = { }, /** -* performs action to obtain id and return a value in the callback's response argument -* @function -* @param {SubmoduleConfig} config -* @returns {id: string} -*/ + * performs action to obtain id and return a value in the callback's response argument + * @function + * @param {SubmoduleConfig} config + * @returns {id: string} + */ getId(config) { - function snowflakeId(placeholder) { - return placeholder - ? (placeholder ^ Math.random() * 16 >> placeholder / 4).toString(16) - : ([1e7] + -1e3 + -4e3 + -8e3 + -1e11 + 1e3).replace(/[018]/g, snowflakeId); - } - const configParams = config.params || {}; - const srcId = this.getSrcId(configParams); + const urlParams = this.getUrlParams(configParams); + const srcId = this.getSrcId(configParams, urlParams); + const sharedId = this.getSharedId(configParams); + const useCallbacks = this.useCallbacks(configParams); + + logInfo('NOVATIQ config params: ' + JSON.stringify(configParams)); logInfo('NOVATIQ Sync request used sourceid param: ' + srcId); + logInfo('NOVATIQ Sync request Shared ID: ' + sharedId); + + return this.sendSyncRequest(useCallbacks, sharedId, srcId, urlParams); + }, + + sendSyncRequest(useCallbacks, sharedId, sspid, urlParams) { + const syncUrl = this.getSyncUrl(sharedId, sspid, urlParams); + const url = syncUrl.url; + const novatiqId = syncUrl.novatiqId; + + // for testing + const sharedStatus = (sharedId != undefined && sharedId != false) ? 'Found' : 'Not Found'; + + if (useCallbacks) { + let res = this.sendAsyncSyncRequest(novatiqId, url); ; + res.sharedStatus = sharedStatus; + + return res; + } else { + this.sendSimpleSyncRequest(novatiqId, url); + + return { 'id': novatiqId, + 'sharedStatus': sharedStatus } + } + }, - let partnerhost; - partnerhost = window.location.hostname; - logInfo('NOVATIQ partner hostname: ' + partnerhost); + sendAsyncSyncRequest(novatiqId, url) { + logInfo('NOVATIQ Setting up ASYNC sync request'); + + const resp = function (callback) { + logInfo('NOVATIQ *** Calling ASYNC sync request'); + + function onSuccess(response, responseObj) { + let syncrc; + var novatiqIdJson = { syncResponse: 0 }; + syncrc = responseObj.status; + logInfo('NOVATIQ Sync Response Code:' + syncrc); + logInfo('NOVATIQ *** ASYNC request returned ' + syncrc); + if (syncrc === 200) { + novatiqIdJson = { 'id': novatiqId, syncResponse: 1 }; + } else { + if (syncrc === 204) { + novatiqIdJson = { 'id': novatiqId, syncResponse: 2 }; + } + } + callback(novatiqIdJson); + } + + ajax(url, + { success: onSuccess }, + undefined, { method: 'GET', withCredentials: false }); + } + + return {callback: resp}; + }, + + sendSimpleSyncRequest(novatiqId, url) { + logInfo('NOVATIQ Sending SIMPLE sync request'); - const novatiqId = snowflakeId(); - const url = 'https://spadsync.com/sync?sptoken=' + novatiqId + '&sspid=' + srcId + '&ssphost=' + partnerhost; ajax(url, undefined, undefined, { method: 'GET', withCredentials: false }); logInfo('NOVATIQ snowflake: ' + novatiqId); - return { 'id': novatiqId } }, - getSrcId(configParams) { - logInfo('NOVATIQ Configured sourceid param: ' + configParams.sourceid); + getNovatiqId(urlParams) { + // standard uuid format + let uuidFormat = [1e7] + -1e3 + -4e3 + -8e3 + -1e11; + if (urlParams.useStandardUuid == false) { + // novatiq standard uuid(like) format + uuidFormat = uuidFormat + 1e3; + } + + return (uuidFormat).replace(/[018]/g, c => + (c ^ crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> c / 4).toString(16) + ); + }, + + getSyncUrl(sharedId, sspid, urlParams) { + let novatiqId = this.getNovatiqId(urlParams); + + let url = 'https://spadsync.com/sync?' + urlParams.novatiqId + '=' + novatiqId; + + if (urlParams.useSspId) { + url = url + '&sspid=' + sspid; + } + + if (urlParams.useSspHost) { + let ssphost = getWindowLocation().hostname; + logInfo('NOVATIQ partner hostname: ' + ssphost); + + url = url + '&ssphost=' + ssphost; + } + + // append on the shared ID if we have one + if (sharedId != null) { + url = url + '&sharedId=' + sharedId; + } + + return { + url: url, + novatiqId: novatiqId + } + }, + + getUrlParams(configParams) { + let urlParams = { + novatiqId: 'snowflake', + useStandardUuid: false, + useSspId: true, + useSspHost: true + } + + if (typeof configParams.urlParams != 'undefined') { + if (configParams.urlParams.novatiqId != undefined) { + urlParams.novatiqId = configParams.urlParams.novatiqId; + } + if (configParams.urlParams.useStandardUuid != undefined) { + urlParams.useStandardUuid = configParams.urlParams.useStandardUuid; + } + if (configParams.urlParams.useSspId != undefined) { + urlParams.useSspId = configParams.urlParams.useSspId; + } + if (configParams.urlParams.useSspHost != undefined) { + urlParams.useSspHost = configParams.urlParams.useSspHost; + } + } + + return urlParams; + }, + + useCallbacks(configParams) { + return typeof configParams.useCallbacks != 'undefined' && configParams.useCallbacks === true; + }, - function isHex(str) { - var a = parseInt(str, 16); - return (a.toString(16) === str) + useSharedId(configParams) { + return typeof configParams.useSharedId != 'undefined' && configParams.useSharedId === true; + }, + + getCookieOrStorageID(configParams) { + let cookieOrStorageID = '_pubcid'; + + if (typeof configParams.sharedIdName != 'undefined' && configParams.sharedIdName != null && configParams.sharedIdName != '') { + cookieOrStorageID = configParams.sharedIdName; + logInfo('NOVATIQ sharedID name redefined: ' + cookieOrStorageID); + } + + return cookieOrStorageID; + }, + + // return null if we aren't supposed to use one or we are but there isn't one present + getSharedId(configParams) { + let sharedId = null; + if (this.useSharedId(configParams)) { + let cookieOrStorageID = this.getCookieOrStorageID(configParams); + const storage = getStorageManager({moduleName: 'pubCommonId'}); + + // first check local storage + if (storage.hasLocalStorage()) { + sharedId = storage.getDataFromLocalStorage(cookieOrStorageID); + logInfo('NOVATIQ sharedID retrieved from local storage:' + sharedId); + } + + // if nothing check the local cookies + if (sharedId == null) { + sharedId = storage.getCookie(cookieOrStorageID); + logInfo('NOVATIQ sharedID retrieved from cookies:' + sharedId); + } } + logInfo('NOVATIQ sharedID returning: ' + sharedId); + + return sharedId; + }, + + getSrcId(configParams, urlParams) { + if (urlParams.useSspId == false) { + logInfo('NOVATIQ Configured to NOT use sspid'); + return ''; + } + + logInfo('NOVATIQ Configured sourceid param: ' + configParams.sourceid); + let srcId; if (typeof configParams.sourceid === 'undefined' || configParams.sourceid === null || configParams.sourceid === '') { srcId = '000'; @@ -76,9 +237,6 @@ export const novatiqIdSubmodule = { } else if (configParams.sourceid.length < 3 || configParams.sourceid.length > 3) { srcId = '001'; logInfo('NOVATIQ sourceid param set to value 001 due to wrong size in config section 3 chars max e.g. 1ab'); - } else if (isHex(configParams.sourceid) == false) { - srcId = '002'; - logInfo('NOVATIQ sourceid param set to value 002 due to wrong format in config section expecting hex value only'); } else { srcId = configParams.sourceid; } diff --git a/modules/novatiqIdSystem.md b/modules/novatiqIdSystem.md index ce561a696e3..f33fc700311 100644 --- a/modules/novatiqIdSystem.md +++ b/modules/novatiqIdSystem.md @@ -1,8 +1,8 @@ -# Novatiq Snowflake ID +# Novatiq Hyper ID -Novatiq proprietary dynamic snowflake ID is a unique, non sequential and single use identifier for marketing activation. Our in network solution matches verification requests to telco network IDs, safely and securely inside telecom infrastructure. Novatiq snowflake ID can be used for identity validation and as a secured 1st party data delivery mechanism. +The Novatiq proprietary dynamic Hyper ID is a unique, non sequential and single use identifier for marketing activation. Our in network solution matches verification requests to telco network IDs safely and securely inside telecom infrastructure. The Novatiq Hyper ID can be used for identity validation and as a secured 1st party data delivery mechanism. -## Novatiq Snowflake ID Configuration +## Novatiq Hyper ID Configuration Enable by adding the Novatiq submodule to your Prebid.js package with: @@ -18,19 +18,80 @@ pbjs.setConfig({ userIds: [{ name: 'novatiq', params: { - sourceid '1a3', // change to the Partner Number you received from Novatiq + // change to the Partner Number you received from Novatiq + sourceid '1a3' } } }], - auctionDelay: 50 // 50ms maximum auction delay, applies to all userId modules + // 50ms maximum auction delay, applies to all userId modules + auctionDelay: 50 } }); ``` -| Param under userSync.userIds[] | Scope | Type | Description | Example | +### Parameters for the Novatiq Module +| Param | Scope | Type | Description | Example | | --- | --- | --- | --- | --- | | name | Required | String | Module identification: `"novatiq"` | `"novatiq"` | | params | Required | Object | Configuration specifications for the Novatiq module. | | -| params.sourceid | Required | String | This is the Novatiq Partner Number obtained via Novatiq registration. | `1a3` | +| params.sourceid | Required | String | The Novatiq Partner Number obtained via Novatiq | `1a3` | +| params.useSharedId | Optional | Boolean | Use the sharedID module if it's activated. | `true` | +| params.sharedIdName | Optional | String | Same as the SharedID "name" parameter
Defaults to "_pubcid" | `"demo_pubcid"` | +| params.useCallbacks | Optional | Boolean | Use callbacks for custom integrations | `false` | +| params.urlParams | Optional | Object | Sync URl configuration for custom integrations | | +| params.urlParams.novatiqId | Optional | String | The name of the parameter used to indicate the novatiq ID uuid | `snowflake` | +| params.urlParams.useStandardUuid | Optional | Boolean | Use a standard UUID format, or the Novatiq UUID format | `false` | +| params.urlParams.useSspId | Optional | Boolean | Send the sspid (sourceid) along with the sync request | `false` | +| params.urlParams.useSspHost | Optional | Boolean | Send the ssphost along with the sync request | `false` | + +# Novatiq Hyper ID with Prebid SharedID support +You can make use of the Prebid.js SharedId module as follows. + +## Novatiq Hyper ID Configuration + +Enable by adding the Novatiq and SharedId submodule to your Prebid.js package with: + +``` +gulp build --modules=novatiqIdSystem,userId,pubCommonId +``` + +Module activation and configuration: + +```javascript +pbjs.setConfig({ + userSync: { + userIds: [ + { + name: "pubCommonId", + storage: { + type: "cookie", + // optional: will default to _pubcid if left blank + name: "demo_pubcid", + + // expires in 1 years + expires: 365 + }, + bidders: [ 'adtarget' ] + }, + { + name: 'novatiq', + params: { + // change to the Partner Number you received from Novatiq + sourceid '1a3', + + // Use the sharedID module + useSharedId: true, + + // optional: will default to _pubcid if left blank. + // If not left blank must match "name" in the the module above + sharedIdName: 'demo_pubcid' + } + } + }], + // 50ms maximum auction delay, applies to all userId modules + auctionDelay: 50 + } +}); +``` If you have any questions, please reach out to us at prebid@novatiq.com. diff --git a/modules/oguryBidAdapter.js b/modules/oguryBidAdapter.js index c21eaffa48c..7d2989b2066 100644 --- a/modules/oguryBidAdapter.js +++ b/modules/oguryBidAdapter.js @@ -10,7 +10,7 @@ const DEFAULT_TIMEOUT = 1000; const BID_HOST = 'https://mweb-hb.presage.io/api/header-bidding-request'; const TIMEOUT_MONITORING_HOST = 'https://ms-ads-monitoring-events.presage.io'; const MS_COOKIE_SYNC_DOMAIN = 'https://ms-cookie-sync.presage.io'; -const ADAPTER_VERSION = '1.2.7'; +const ADAPTER_VERSION = '1.2.10'; function isBidRequestValid(bid) { const adUnitSizes = getAdUnitSizes(bid); @@ -41,10 +41,10 @@ function buildRequests(validBidRequests, bidderRequest) { const openRtbBidRequestBanner = { id: bidderRequest.auctionId, tmax: DEFAULT_TIMEOUT, - at: 2, + at: 1, regs: { ext: { - gdpr: 1 + gdpr: bidderRequest.gdprConsent && bidderRequest.gdprConsent.gdprApplies ? 1 : 0 }, }, site: { @@ -63,14 +63,7 @@ function buildRequests(validBidRequests, bidderRequest) { } }; - if (bidderRequest.hasOwnProperty('gdprConsent') && - bidderRequest.gdprConsent.hasOwnProperty('gdprApplies')) { - openRtbBidRequestBanner.regs.ext.gdpr = bidderRequest.gdprConsent.gdprApplies ? 1 : 0 - } - - if (bidderRequest.hasOwnProperty('gdprConsent') && - bidderRequest.gdprConsent.hasOwnProperty('consentString') && - bidderRequest.gdprConsent.consentString.length > 0) { + if (bidderRequest.gdprConsent && bidderRequest.gdprConsent.consentString) { openRtbBidRequestBanner.user.ext.consent = bidderRequest.gdprConsent.consentString } @@ -78,7 +71,7 @@ function buildRequests(validBidRequests, bidderRequest) { const sizes = getAdUnitSizes(bidRequest) .map(size => ({ w: size[0], h: size[1] })); - if (bidRequest.hasOwnProperty('mediaTypes') && + if (bidRequest.mediaTypes && bidRequest.mediaTypes.hasOwnProperty('banner')) { openRtbBidRequestBanner.site.id = bidRequest.params.assetKey; @@ -166,11 +159,11 @@ function onBidWon(bid) { w.OG_PREBID_BID_OBJECT = { ...(bid && { ...bid }), } - if (bid && bid.hasOwnProperty('nurl') && bid.nurl.length > 0) ajax(bid['nurl'], null); + if (bid && bid.nurl) ajax(bid.nurl, null); } function onTimeout(timeoutData) { - ajax(`${TIMEOUT_MONITORING_HOST}/bid_timeout`, null, JSON.stringify(timeoutData[0]), { + ajax(`${TIMEOUT_MONITORING_HOST}/bid_timeout`, null, JSON.stringify({...timeoutData[0], location: window.location.href}), { method: 'POST', contentType: 'application/json' }); diff --git a/modules/oneVideoBidAdapter.js b/modules/oneVideoBidAdapter.js index e0db143dc0f..aeb19e7c32c 100644 --- a/modules/oneVideoBidAdapter.js +++ b/modules/oneVideoBidAdapter.js @@ -10,6 +10,7 @@ export const spec = { SYNC_ENDPOINT1: 'https://pixel.advertising.com/ups/57304/sync?gdpr=&gdpr_consent=&_origin=0&redir=true', SYNC_ENDPOINT2: 'https://match.adsrvr.org/track/cmf/generic?ttd_pid=adaptv&ttd_tpi=1', supportedMediaTypes: ['video', 'banner'], + gvlid: 25, /** * Determines whether or not the given bid request is valid. * diff --git a/modules/onetagBidAdapter.js b/modules/onetagBidAdapter.js index 5642dce9018..89c614dba23 100644 --- a/modules/onetagBidAdapter.js +++ b/modules/onetagBidAdapter.js @@ -1,20 +1,20 @@ 'use strict'; -import { BANNER, VIDEO } from '../src/mediaTypes.js'; -import { INSTREAM, OUTSTREAM } from '../src/video.js'; -import { Renderer } from '../src/Renderer.js'; -import find from 'core-js-pure/features/array/find.js'; -import { getStorageManager } from '../src/storageManager.js'; -import { registerBidder } from '../src/adapters/bidderFactory.js'; -import { createEidsArray } from './userId/eids.js'; -import { deepClone } from '../src/utils.js'; +import {BANNER, VIDEO} from '../src/mediaTypes.js'; +import {INSTREAM, OUTSTREAM} from '../src/video.js'; +import {Renderer} from '../src/Renderer.js'; +import {find} from '../src/polyfill.js'; +import {getStorageManager} from '../src/storageManager.js'; +import {registerBidder} from '../src/adapters/bidderFactory.js'; +import {createEidsArray} from './userId/eids.js'; +import {deepClone} from '../src/utils.js'; const ENDPOINT = 'https://onetag-sys.com/prebid-request'; const USER_SYNC_ENDPOINT = 'https://onetag-sys.com/usync/'; const BIDDER_CODE = 'onetag'; const GVLID = 241; -const storage = getStorageManager(GVLID); +const storage = getStorageManager({gvlid: GVLID, bidderCode: BIDDER_CODE}); /** * Determines whether or not the given bid request is valid. diff --git a/modules/open8BidAdapter.js b/modules/open8BidAdapter.js new file mode 100644 index 00000000000..7fa97235525 --- /dev/null +++ b/modules/open8BidAdapter.js @@ -0,0 +1,188 @@ +import { Renderer } from '../src/Renderer.js'; +import {ajax} from '../src/ajax.js'; +import { createTrackPixelHtml, getBidIdParameter, logError, logWarn, tryAppendQueryString } from '../src/utils.js'; +import { registerBidder } from '../src/adapters/bidderFactory.js'; +import { VIDEO, BANNER } from '../src/mediaTypes.js'; + +const BIDDER_CODE = 'open8'; +const URL = 'https://as.vt.open8.com/v1/control/prebid'; +const AD_TYPE = { + VIDEO: 1, + BANNER: 2 +}; + +export const spec = { + code: BIDDER_CODE, + supportedMediaTypes: [VIDEO, BANNER], + + isBidRequestValid: function(bid) { + return !!(bid.params.slotKey); + }, + + buildRequests: function(validBidRequests, bidderRequest) { + var requests = []; + for (var i = 0; i < validBidRequests.length; i++) { + var bid = validBidRequests[i]; + var queryString = ''; + var slotKey = getBidIdParameter('slotKey', bid.params); + queryString = tryAppendQueryString(queryString, 'slot_key', slotKey); + queryString = tryAppendQueryString(queryString, 'imp_id', generateImpId()); + queryString += ('bid_id=' + bid.bidId); + + requests.push({ + method: 'GET', + url: URL, + data: queryString + }); + } + return requests; + }, + + interpretResponse: function(serverResponse, request) { + var bidderResponse = serverResponse.body; + + if (!bidderResponse.isAdReturn) { + return []; + } + + var ad = bidderResponse.ad; + + const bid = { + slotKey: bidderResponse.slotKey, + userId: bidderResponse.userId, + impId: bidderResponse.impId, + media: bidderResponse.media, + ds: ad.ds, + spd: ad.spd, + fa: ad.fa, + pr: ad.pr, + mr: ad.mr, + nurl: ad.nurl, + requestId: ad.bidId, + cpm: ad.price, + creativeId: ad.creativeId, + dealId: ad.dealId, + currency: ad.currency || 'JPY', + netRevenue: true, + ttl: 360, // 6 minutes + meta: { + advertiserDomains: ad.adomain || [] + } + } + + if (ad.adType === AD_TYPE.VIDEO) { + const videoAd = bidderResponse.ad.video; + Object.assign(bid, { + vastXml: videoAd.vastXml, + width: videoAd.w, + height: videoAd.h, + renderer: newRenderer(bidderResponse), + adResponse: bidderResponse, + mediaType: VIDEO + }); + } else if (ad.adType === AD_TYPE.BANNER) { + const bannerAd = bidderResponse.ad.banner; + Object.assign(bid, { + width: bannerAd.w, + height: bannerAd.h, + ad: bannerAd.adm, + mediaType: BANNER + }); + if (bannerAd.imps) { + try { + bannerAd.imps.forEach(impTrackUrl => { + const tracker = createTrackPixelHtml(impTrackUrl); + bid.ad += tracker; + }); + } catch (error) { + logError('Error appending imp tracking pixel', error); + } + } + } + return [bid]; + }, + + getUserSyncs: function(syncOptions, serverResponses) { + const syncs = []; + if (syncOptions.iframeEnabled && serverResponses.length) { + const syncIFs = serverResponses[0].body.syncIFs; + if (syncIFs) { + syncIFs.forEach(sync => { + syncs.push({ + type: 'iframe', + url: sync + }); + }); + } + } + if (syncOptions.pixelEnabled && serverResponses.length) { + const syncPixs = serverResponses[0].body.syncPixels; + if (syncPixs) { + syncPixs.forEach(sync => { + syncs.push({ + type: 'image', + url: sync + }); + }); + } + } + return syncs; + }, + onBidWon: function(bid) { + if (!bid.nurl) { return; } + const winUrl = bid.nurl.replace( + /\$\{AUCTION_PRICE\}/, + bid.cpm + ); + ajax(winUrl, null); + } +} + +function generateImpId() { + var l = 16; + var c = 'abcdefghijklmnopqrstuvwsyz0123456789'; + var cl = c.length; + var r = ''; + for (var i = 0; i < l; i++) { + r += c[Math.floor(Math.random() * cl)]; + } + return r; +} + +function newRenderer(bidderResponse) { + const renderer = Renderer.install({ + id: bidderResponse.ad.bidId, + url: bidderResponse.ad.video.purl, + loaded: false, + }); + + try { + renderer.setRender(outstreamRender); + } catch (err) { + logWarn('Prebid Error calling setRender on newRenderer', err); + } + + return renderer; +} + +function outstreamRender(bid) { + bid.renderer.push(() => { + window.op8.renderPrebid({ + vastXml: bid.vastXml, + adUnitCode: bid.adUnitCode, + slotKey: bid.slotKey, + impId: bid.impId, + userId: bid.userId, + media: bid.media, + ds: bid.ds, + spd: bid.spd, + fa: bid.fa, + pr: bid.pr, + mr: bid.mr, + adResponse: bid.adResponse, + mediaType: bid.mediaType + }); + }); +} + +registerBidder(spec); diff --git a/modules/openwebBidAdapter.js b/modules/openwebBidAdapter.js index 9476d2d2914..f515eb14011 100644 --- a/modules/openwebBidAdapter.js +++ b/modules/openwebBidAdapter.js @@ -1,8 +1,8 @@ -import { isNumber, deepAccess, isArray, flatten, convertTypes, parseSizesInput } from '../src/utils.js'; -import { registerBidder } from '../src/adapters/bidderFactory.js'; -import { ADPOD, BANNER, VIDEO } from '../src/mediaTypes.js'; -import { config } from '../src/config.js'; -import find from 'core-js-pure/features/array/find.js'; +import {convertTypes, deepAccess, flatten, isArray, isNumber, parseSizesInput} from '../src/utils.js'; +import {registerBidder} from '../src/adapters/bidderFactory.js'; +import {ADPOD, BANNER, VIDEO} from '../src/mediaTypes.js'; +import {config} from '../src/config.js'; +import {find} from '../src/polyfill.js'; const ENDPOINT = 'https://ghb.spotim.market/v2/auction'; const BIDDER_CODE = 'openweb'; diff --git a/modules/openxAnalyticsAdapter.js b/modules/openxAnalyticsAdapter.js index f67f8bd0c75..89140c0aacd 100644 --- a/modules/openxAnalyticsAdapter.js +++ b/modules/openxAnalyticsAdapter.js @@ -1,10 +1,23 @@ -import { logInfo, logError, getWindowLocation, parseQS, logMessage, _each, deepAccess, logWarn, _map, flatten, uniques, isEmpty, parseSizesInput } from '../src/utils.js'; +import { + _each, + _map, + deepAccess, + flatten, + getWindowLocation, + isEmpty, + logError, + logInfo, + logMessage, + logWarn, + parseQS, + parseSizesInput, + uniques +} from '../src/utils.js'; import adapter from '../src/AnalyticsAdapter.js'; import CONSTANTS from '../src/constants.json'; import adapterManager from '../src/adapterManager.js'; -import { ajax } from '../src/ajax.js'; -import find from 'core-js-pure/features/array/find.js'; -import includes from 'core-js-pure/features/array/includes.js'; +import {ajax} from '../src/ajax.js'; +import {find, includes} from '../src/polyfill.js'; export const AUCTION_STATES = { INIT: 'initialized', // auction has initialized diff --git a/modules/openxBidAdapter.js b/modules/openxBidAdapter.js index 60b441e2c10..85dcfbb3b47 100644 --- a/modules/openxBidAdapter.js +++ b/modules/openxBidAdapter.js @@ -1,8 +1,18 @@ -import { deepAccess, convertTypes, isArray, inIframe, _map, deepSetValue, _each, parseSizesInput, parseUrl } from '../src/utils.js'; +import { + _each, + _map, + convertTypes, + deepAccess, + deepSetValue, + inIframe, + isArray, + parseSizesInput, + parseUrl +} from '../src/utils.js'; import {config} from '../src/config.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; import {BANNER, VIDEO} from '../src/mediaTypes.js'; -import includes from 'core-js-pure/features/array/includes.js' +import {includes} from '../src/polyfill.js'; const SUPPORTED_AD_TYPES = [BANNER, VIDEO]; const VIDEO_TARGETING = ['startdelay', 'mimes', 'minduration', 'maxduration', diff --git a/modules/optimeraRtdProvider.js b/modules/optimeraRtdProvider.js index 024a558baca..dfe8f1bfcf2 100644 --- a/modules/optimeraRtdProvider.js +++ b/modules/optimeraRtdProvider.js @@ -101,7 +101,7 @@ export function scoreFileRequest() { export function returnTargetingData(adUnits, config) { const targeting = {}; try { - adUnits.forEach(function(adUnit) { + adUnits.forEach((adUnit) => { if (optimeraTargeting[adUnit]) { targeting[adUnit] = {}; targeting[adUnit][optimeraKeyName] = [optimeraTargeting[adUnit]]; @@ -141,12 +141,11 @@ export function init(moduleConfig) { setScoresURL(); scoreFileRequest(); return true; - } else { - if (!_moduleParams.clientID) { - logError('Optimera clientID is missing in the Optimera RTD configuration.'); - } - return false; } + if (!_moduleParams.clientID) { + logError('Optimera clientID is missing in the Optimera RTD configuration.'); + } + return false; } /** @@ -163,7 +162,7 @@ export function init(moduleConfig) { export function setScoresURL() { const optimeraHost = window.location.host; const optimeraPathName = window.location.pathname; - let newScoresURL = `${scoresBaseURL}${clientID}/${optimeraHost}${optimeraPathName}.js`; + const newScoresURL = `${scoresBaseURL}${clientID}/${optimeraHost}${optimeraPathName}.js`; if (scoresURL !== newScoresURL) { scoresURL = newScoresURL; fetchScoreFile = true; @@ -174,7 +173,7 @@ export function setScoresURL() { /** * Set the scores for the device if given. - * Add any any insights to the winddow.optimeraInsights object. + * Add data and insights to the winddow.optimera object. * * @param {*} result * @returns {string} JSON string of Optimera Scores. @@ -187,9 +186,16 @@ export function setScores(result) { scores = scores.device[device]; } logInfo(scores); + window.optimera = window.optimera || {}; + window.optimera.data = window.optimera.data || {}; + window.optimera.insights = window.optimera.insights || {}; + Object.keys(scores).map((key) => { + if (key !== 'insights') { + window.optimera.data[key] = scores[key]; + } + }); if (scores.insights) { - window.optimeraInsights = window.optimeraInsights || {}; - window.optimeraInsights.data = scores.insights; + window.optimera.insights = scores.insights; } } catch (e) { logError('Optimera score file could not be parsed.'); diff --git a/modules/orbidderBidAdapter.js b/modules/orbidderBidAdapter.js index 111c1876e14..38af3a8d1d6 100644 --- a/modules/orbidderBidAdapter.js +++ b/modules/orbidderBidAdapter.js @@ -3,7 +3,7 @@ import {registerBidder} from '../src/adapters/bidderFactory.js'; import { getStorageManager } from '../src/storageManager.js'; import { BANNER, NATIVE } from '../src/mediaTypes.js'; -const storageManager = getStorageManager(); +const storageManager = getStorageManager({bidderCode: 'orbidder'}); /** * Determines whether or not the given bid response is valid. diff --git a/modules/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/pilotxBidAdapter.js b/modules/pilotxBidAdapter.js new file mode 100644 index 00000000000..335c461e3d9 --- /dev/null +++ b/modules/pilotxBidAdapter.js @@ -0,0 +1,147 @@ +import { registerBidder } from '../src/adapters/bidderFactory.js'; +const BIDDER_CODE = 'pilotx'; +const ENDPOINT_URL = '//adn.pilotx.tv/hb' +export const spec = { + code: BIDDER_CODE, + supportedMediaTypes: ['banner', 'video'], + aliases: ['pilotx'], // short code + /** + * Determines whether or not the given bid request is valid. + * + * @param {BidRequest} bid The bid params to validate. + * @return boolean True if this is a valid bid, and false otherwise. + */ + isBidRequestValid: function (bid) { + let sizesCheck = !!bid.sizes + let paramSizesCheck = !!bid.params.sizes + var sizeConfirmed = false + if (sizesCheck) { + if (bid.sizes.length < 1) { + return false + } else { + sizeConfirmed = true + } + } + if (paramSizesCheck) { + if (bid.params.sizes.length < 1 && !sizeConfirmed) { + return false + } else { + sizeConfirmed = true + } + } + if (!sizeConfirmed) { + return false + } + return !!(bid.params.placementId); + }, + /** + * Make a server request from the list of BidRequests. + * + * @param {validBidRequests[]} - an array of bids + * @return ServerRequest Info describing the request to the server. + */ + buildRequests: function (validBidRequests, bidderRequest) { + let payloadItems = {}; + validBidRequests.forEach(bidRequest => { + let sizes = []; + let placementId = this.setPlacementID(bidRequest.params.placementId) + payloadItems[placementId] = {} + if (bidRequest.sizes.length > 0) { + if (Array.isArray(bidRequest.sizes[0])) { + for (let i = 0; i < bidRequest.sizes.length; i++) { + sizes[i] = [(bidRequest.sizes[i])[0], (bidRequest.sizes[i])[1]] + } + } else { + sizes[0] = [bidRequest.sizes[0], bidRequest.sizes[1]] + } + payloadItems[placementId]['sizes'] = sizes + } + if (bidRequest.mediaTypes != null) { + for (let i in bidRequest.mediaTypes) { + payloadItems[placementId][i] = { + ...bidRequest.mediaTypes[i] + } + } + } + let consentTemp = '' + let consentRequiredTemp = false + if (bidderRequest && bidderRequest.gdprConsent) { + consentTemp = bidderRequest.gdprConsent.consentString + // will check if the gdprApplies field was populated with a boolean value (ie from page config). If it's undefined, then default to true + consentRequiredTemp = (typeof bidderRequest.gdprConsent.gdprApplies === 'boolean') ? bidderRequest.gdprConsent.gdprApplies : true + } + + payloadItems[placementId]['gdprConsentString'] = consentTemp + payloadItems[placementId]['gdprConsentRequired'] = consentRequiredTemp + payloadItems[placementId]['bidId'] = bidRequest.bidId + }); + const payload = payloadItems; + const payloadString = JSON.stringify(payload); + return { + method: 'POST', + url: ENDPOINT_URL, + data: payloadString, + }; + }, + /** + * Unpack the response from the server into a list of bids. + * + * @param {ServerResponse} serverResponse A successful response from the server. + * @return {Bid[]} An array of bids which were nested inside the server. + */ + interpretResponse: function (serverResponse, bidRequest) { + const serverBody = serverResponse.body; + const bidResponses = []; + if (serverBody.mediaType == 'banner') { + const bidResponse = { + requestId: serverBody.requestId, + cpm: serverBody.cpm, + width: serverBody.width, + height: serverBody.height, + creativeId: serverBody.creativeId, + currency: serverBody.currency, + netRevenue: false, + ttl: serverBody.ttl, + ad: serverBody.ad, + mediaType: 'banner', + meta: { + mediaType: 'banner', + advertiserDomains: serverBody.advertiserDomains + } + } + bidResponses.push(bidResponse) + } else if (serverBody.mediaType == 'video') { + const bidResponse = { + requestId: serverBody.requestId, + cpm: serverBody.cpm, + width: serverBody.width, + height: serverBody.height, + creativeId: serverBody.creativeId, + currency: serverBody.currency, + netRevenue: false, + ttl: serverBody.ttl, + vastUrl: serverBody.vastUrl, + mediaType: 'video', + meta: { + mediaType: 'video', + advertiserDomains: serverBody.advertiserDomains + } + } + bidResponses.push(bidResponse) + } + + return bidResponses; + }, + + /** + * Formats placement ids for adserver ingestion purposes + * @param {string[]} The placement ID/s in an array + */ + setPlacementID: function (placementId) { + if (Array.isArray(placementId)) { + return placementId.join('#') + } + return placementId + }, +} +registerBidder(spec); diff --git a/modules/pilotxBidAdapter.md b/modules/pilotxBidAdapter.md new file mode 100644 index 00000000000..37489bda4a0 --- /dev/null +++ b/modules/pilotxBidAdapter.md @@ -0,0 +1,50 @@ +# Overview + +``` +Module Name: Pilotx Prebid Adapter +Module Type: Bidder Adapter +Maintainer: tony@pilotx.tv +``` + +# Description + +Connects to Pilotx + +Pilotx's bid adapter supports banner and video. + +# Test Parameters +``` +// Banner adUnit +var adUnits = [{ + code: 'div-gpt-ad-1460505748561-0', + mediaTypes: { + banner: { + sizes: [[300, 250], [300,600]], + } + }, + bids: [{ + bidder: 'pilotx', + params: { + placementId: ["1423"] + } + }] + +}]; + +// Video adUnit +var videoAdUnit = { + code: 'video1', + mediaTypes: { + video: { + context: 'instream', + playerSize: [640, 480], + } + }, + bids: [{ + bidder: 'pilotx', + params: { + placementId: '1422', + } + }] +}; +``` \ No newline at end of file diff --git a/modules/pixfutureBidAdapter.js b/modules/pixfutureBidAdapter.js index e9db875fc2f..29552ec796d 100644 --- a/modules/pixfutureBidAdapter.js +++ b/modules/pixfutureBidAdapter.js @@ -1,14 +1,22 @@ -import { registerBidder } from '../src/adapters/bidderFactory.js'; -import { getStorageManager } from '../src/storageManager.js'; -import { BANNER } from '../src/mediaTypes.js'; -import { config } from '../src/config.js'; -import includes from 'core-js-pure/features/array/includes.js'; -import { convertCamelToUnderscore, isArray, isNumber, isPlainObject, deepAccess, isEmpty, transformBidderParamKeywords, isFn } from '../src/utils.js'; -import { auctionManager } from '../src/auctionManager.js'; -import find from 'core-js-pure/features/array/find.js'; +import {registerBidder} from '../src/adapters/bidderFactory.js'; +import {getStorageManager} from '../src/storageManager.js'; +import {BANNER} from '../src/mediaTypes.js'; +import {config} from '../src/config.js'; +import {find, includes} from '../src/polyfill.js'; +import { + convertCamelToUnderscore, + deepAccess, + isArray, + isEmpty, + isFn, + isNumber, + isPlainObject, + transformBidderParamKeywords +} from '../src/utils.js'; +import {auctionManager} from '../src/auctionManager.js'; const SOURCE = 'pbjs'; -const storageManager = getStorageManager(); +const storageManager = getStorageManager({bidderCode: 'pixfuture'}); const USER_PARAMS = ['age', 'externalUid', 'segments', 'gender', 'dnt', 'language']; export const spec = { code: 'pixfuture', diff --git a/modules/prebidServerBidAdapter/index.js b/modules/prebidServerBidAdapter/index.js index efe2576efd5..b1b54d9e313 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 + bind, + cleanObj, + createTrackPixelHtml, + deepAccess, + deepClone, + deepSetValue, + flatten, + generateUUID, + getBidRequest, + getDefinedParams, + getPrebidInternal, + insertUserSyncIframe, + isArray, + isEmpty, + isNumber, + isPlainObject, + isStr, + logError, + logInfo, + logMessage, + logWarn, + mergeDeep, + parseSizesInput, + pick, timestamp, + triggerPixel, + uniques } from '../../src/utils.js'; import CONSTANTS from '../../src/constants.json'; import adapterManager from '../../src/adapterManager.js'; import { config } from '../../src/config.js'; import { VIDEO, NATIVE } from '../../src/mediaTypes.js'; -import { processNativeAdUnitParams } from '../../src/native.js'; import { isValid } from '../../src/adapters/bidderFactory.js'; -import events from '../../src/events.js'; -import includes from 'core-js-pure/features/array/includes.js'; +import * as events from '../../src/events.js'; +import {find, includes} from '../../src/polyfill.js'; import { S2S_VENDORS } from './config.js'; import { ajax } from '../../src/ajax.js'; -import find from 'core-js-pure/features/array/find.js'; +import {hook} from '../../src/hook.js'; +import {getGlobal} from '../../src/prebidGlobal.js'; const getConfig = config.getConfig; @@ -54,6 +76,7 @@ let eidPermissions; * @typedef {Object} S2SDefaultConfig * @summary Base config properties for server to server header bidding * @property {string} [adapter='prebidServer'] adapter code to use for S2S + * @property {boolean} [allowUnknownBidderCodes=false] allow bids from bidders that were not explicitly requested * @property {boolean} [enabled=false] enables S2S bidding * @property {number} [timeout=1000] timeout for S2S bidders - should be lower than `pbjs.requestBids({timeout})` * @property {number} [syncTimeout=1000] timeout for cookie sync iframe / image rendering @@ -77,10 +100,12 @@ let eidPermissions; * @type {S2SDefaultConfig} */ const s2sDefaultConfig = { + bidders: Object.freeze([]), timeout: 1000, syncTimeout: 1000, maxBids: 1, adapter: 'prebidServer', + allowUnknownBidderCodes: false, adapterOptions: {}, syncUrlModifier: {} }; @@ -120,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; @@ -433,7 +458,6 @@ let nativeEventTrackerMethodMap = { * Protocol spec for OpenRTB endpoint * e.g., https:///v1/openrtb2/auction */ -let bidIdMap = {}; let nativeAssetCache = {}; // store processed native params to preserve /** @@ -491,8 +515,24 @@ export function resetWurlMap() { wurlMap = {}; } -const OPEN_RTB_PROTOCOL = { - buildRequest(s2sBidRequest, bidRequests, adUnits, s2sConfig, requestedBidders) { +function ORTB2(s2sBidRequest, bidderRequests, adUnits, requestedBidders) { + this.s2sBidRequest = s2sBidRequest; + this.bidderRequests = bidderRequests; + this.adUnits = adUnits; + this.s2sConfig = s2sBidRequest.s2sConfig; + this.requestedBidders = requestedBidders; + + this.bidIdMap = {}; + this.adUnitsByImp = {}; + this.impRequested = {}; + this.auctionId = bidderRequests.map(br => br.auctionId).reduce((l, r) => (l == null || l === r) && r); + this.requestTimestamp = timestamp(); +} + +Object.assign(ORTB2.prototype, { + buildRequest() { + const {s2sBidRequest, bidderRequests: bidRequests, adUnits, s2sConfig, requestedBidders} = this; + let imps = []; let aliases = {}; const firstBidRequest = bidRequests[0]; @@ -500,6 +540,14 @@ const OPEN_RTB_PROTOCOL = { // transform ad unit into array of OpenRTB impression objects let impIds = new Set(); adUnits.forEach(adUnit => { + // TODO: support labels / conditional bids + // for now, just warn about them + adUnit.bids.forEach((bid) => { + if (bid.mediaTypes != null) { + logWarn(`Prebid Server adapter does not (yet) support bidder-specific mediaTypes for the same adUnit. Size mapping configuration will be ignored for adUnit: ${adUnit.code}, bidder: ${bid.bidder}`); + } + }) + // in case there is a duplicate imp.id, add '-2' suffix to the second imp.id. // e.g. if there are 2 adUnits (case of twin adUnit codes) with code 'test', // first imp will have id 'test' and second imp will have id 'test-2' @@ -510,17 +558,21 @@ const OPEN_RTB_PROTOCOL = { impressionId = `${adUnit.code}-${i}`; } impIds.add(impressionId); + this.adUnitsByImp[impressionId] = adUnit; - const nativeParams = processNativeAdUnitParams(deepAccess(adUnit, 'mediaTypes.native')); + const nativeParams = adUnit.nativeParams; let nativeAssets; if (nativeParams) { + 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) : {}); } @@ -584,9 +636,7 @@ const OPEN_RTB_PROTOCOL = { const bannerParams = deepAccess(adUnit, 'mediaTypes.banner'); adUnit.bids.forEach(bid => { - // OpenRTB response contains imp.id and bidder name. These are - // combined to create a unique key for each bid since an id isn't returned - bidIdMap[`${impressionId}${bid.bidder}`] = bid.bid_id; + this.setBidRequestId(impressionId, bid.bidder, bid.bid_id); // check for and store valid aliases to add to the request if (adapterManager.aliasRegistry[bid.bidder]) { const bidder = adapterManager.bidderRegistry[bid.bidder]; @@ -661,6 +711,7 @@ const OPEN_RTB_PROTOCOL = { // get bidder params in form { : {...params} } // initialize reduce function with the user defined `ext` properties on the ad unit const ext = adUnit.bids.reduce((acc, bid) => { + if (bid.bidder == null) return acc; const adapter = adapterManager.bidderRegistry[bid.bidder]; if (adapter && adapter.getSpec().transformBidParams) { bid.params = adapter.getSpec().transformBidParams(bid.params, true, adUnit, bidRequests); @@ -669,7 +720,7 @@ const OPEN_RTB_PROTOCOL = { return acc; }, {...deepAccess(adUnit, 'ortb2Imp.ext')}); - const imp = { id: impressionId, ext, secure: s2sConfig.secure }; + const imp = { ...adUnit.ortb2Imp, id: impressionId, ext, secure: s2sConfig.secure }; const ortb2 = {...deepAccess(adUnit, 'ortb2Imp.ext.data')}; Object.keys(ortb2).forEach(prop => { @@ -700,7 +751,7 @@ const OPEN_RTB_PROTOCOL = { } }); - Object.assign(imp, mediaTypes); + mergeDeep(imp, mediaTypes); // if storedAuctionResponse has been set, pass SRID const storedAuctionResponseBid = find(firstBidRequest.bids, bid => (bid.adUnitCode === adUnit.code && bid.storedAuctionResponse)); @@ -708,21 +759,64 @@ const OPEN_RTB_PROTOCOL = { 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) { @@ -854,10 +948,12 @@ const OPEN_RTB_PROTOCOL = { addBidderFirstPartyDataToRequest(request); + request.imp.forEach((imp) => this.impRequested[imp.id] = imp); return request; }, - interpretResponse(response, bidderRequests, s2sConfig) { + interpretResponse(response) { + const {bidderRequests, s2sConfig} = this; const bids = []; [['errors', 'serverErrors'], ['responsetimemillis', 'serverResponseTimeMs']] @@ -867,22 +963,27 @@ const OPEN_RTB_PROTOCOL = { // a seatbid object contains a `bid` array and a `seat` string response.seatbid.forEach(seatbid => { (seatbid.bid || []).forEach(bid => { - let bidRequest; - let key = `${bid.impid}${seatbid.seat}`; - if (bidIdMap[key]) { - bidRequest = getBidRequest( - bidIdMap[key], - bidderRequests - ); + let bidRequest = this.getBidRequest(bid.impid, seatbid.seat); + if (bidRequest == null) { + if (!s2sConfig.allowUnknownBidderCodes) { + logWarn(`PBS adapter received bid from unknown bidder (${seatbid.seat}), but 's2sConfig.allowUnknownBidderCodes' is not set. Ignoring bid.`); + return; + } + // for stored impression, a request was made with bidder code `null`. Pick it up here so that NO_BID, BID_WON, etc events + // can work as expected (otherwise, the original request will always result in NO_BID). + bidRequest = this.getBidRequest(bid.impid, null); } const cpm = bid.price; const status = cpm !== 0 ? CONSTANTS.STATUS.GOOD : CONSTANTS.STATUS.NO_BID; - let bidObject = createBid(status, bidRequest || { + let bidObject = createBid(status, { bidder: seatbid.seat, - src: TYPE + src: TYPE, + bidId: bidRequest ? (bidRequest.bidId || bidRequest.bid_Id) : null, + transactionId: this.adUnitsByImp[bid.impid].transactionId, + auctionId: this.auctionId, }); - + bidObject.requestTimestamp = this.requestTimestamp; bidObject.cpm = cpm; // temporarily leaving attaching it to each bidResponse so no breaking change @@ -900,7 +1001,7 @@ const OPEN_RTB_PROTOCOL = { // store wurl by auctionId and adId so it can be accessed from the BID_WON event handler if (isStr(deepAccess(bid, 'ext.prebid.events.win'))) { - addWurl(bidRequest.auctionId, bidObject.adId, deepAccess(bid, 'ext.prebid.events.win')); + addWurl(this.auctionId, bidObject.adId, deepAccess(bid, 'ext.prebid.events.win')); } let extPrebidTargeting = deepAccess(bid, 'ext.prebid.targeting'); @@ -921,9 +1022,8 @@ const OPEN_RTB_PROTOCOL = { if (deepAccess(bid, 'ext.prebid.type') === VIDEO) { bidObject.mediaType = VIDEO; - let sizes = bidRequest.sizes && bidRequest.sizes[0]; - bidObject.playerWidth = sizes[0]; - bidObject.playerHeight = sizes[1]; + const impReq = this.impRequested[bid.impid]; + [bidObject.playerWidth, bidObject.playerHeight] = [impReq.video.w, impReq.video.h]; // try to get cache values from 'response.ext.prebid.cache.js' // else try 'bid.ext.prebid.targeting' as fallback @@ -1006,7 +1106,6 @@ const OPEN_RTB_PROTOCOL = { bidObject.width = bid.w; bidObject.height = bid.h; if (bid.dealid) { bidObject.dealId = bid.dealid; } - bidObject.requestId = bidRequest.bidId || bidRequest.bid_Id; bidObject.creative_id = bid.crid; bidObject.creativeId = bid.crid; if (bid.burl) { bidObject.burl = bid.burl; } @@ -1021,14 +1120,24 @@ const OPEN_RTB_PROTOCOL = { bidObject.ttl = (bid.exp) ? bid.exp : configTtl; bidObject.netRevenue = (bid.netRevenue) ? bid.netRevenue : DEFAULT_S2S_NETREVENUE; - bids.push({ adUnit: bidRequest.adUnitCode, bid: bidObject }); + bids.push({ adUnit: this.adUnitsByImp[bid.impid].code, bid: bidObject }); }); }); } return bids; + }, + setBidRequestId(impId, bidderCode, bidId) { + this.bidIdMap[this.impBidderKey(impId, bidderCode)] = bidId; + }, + getBidRequest(impId, bidderCode) { + const key = this.impBidderKey(impId, bidderCode); + return this.bidIdMap[key] && getBidRequest(this.bidIdMap[key], this.bidderRequests); + }, + impBidderKey(impId, bidderCode) { + return `${impId}${bidderCode}`; } -}; +}); /** * BID_WON event to request the wurl @@ -1076,20 +1185,8 @@ export function PrebidServer() { /* Prebid executes this function when the page asks to send out bid requests */ baseAdapter.callBids = function(s2sBidRequest, bidRequests, addBidResponse, done, ajax) { - const adUnits = deepClone(s2sBidRequest.ad_units); let { gdprConsent, uspConsent } = getConsentData(bidRequests); - // at this point ad units should have a size array either directly or mapped so filter for that - const validAdUnits = adUnits.filter(unit => - unit.mediaTypes && (unit.mediaTypes.native || (unit.mediaTypes.banner && unit.mediaTypes.banner.sizes) || (unit.mediaTypes.video && unit.mediaTypes.video.playerSize)) - ); - - // in case config.bidders contains invalid bidders, we only process those we sent requests for - const requestedBidders = validAdUnits - .map(adUnit => adUnit.bids.map(bid => bid.bidder).filter(uniques)) - .reduce(flatten) - .filter(uniques); - if (Array.isArray(_s2sConfigs)) { if (s2sBidRequest.s2sConfig && s2sBidRequest.s2sConfig.syncEndpoint && getMatchingConsentUrl(s2sBidRequest.s2sConfig.syncEndpoint, gdprConsent)) { let syncBidders = s2sBidRequest.s2sConfig.bidders @@ -1099,59 +1196,23 @@ export function PrebidServer() { queueSync(syncBidders, gdprConsent, uspConsent, s2sBidRequest.s2sConfig); } - const request = OPEN_RTB_PROTOCOL.buildRequest(s2sBidRequest, bidRequests, validAdUnits, s2sBidRequest.s2sConfig, requestedBidders); - const requestJson = request && JSON.stringify(request); - logInfo('BidRequest: ' + requestJson); - const endpointUrl = getMatchingConsentUrl(s2sBidRequest.s2sConfig.endpoint, gdprConsent); - if (request && requestJson && endpointUrl) { - ajax( - endpointUrl, - { - success: response => handleResponse(response, requestedBidders, bidRequests, addBidResponse, done, s2sBidRequest.s2sConfig), - error: done - }, - requestJson, - { contentType: 'text/plain', withCredentials: true } - ); - } else { - logError('PBS request not made. Check endpoints.'); - } - } - }; - - /* Notify Prebid of bid responses so bids can get in the auction */ - function handleResponse(response, requestedBidders, bidderRequests, addBidResponse, done, s2sConfig) { - let result; - let bids = []; - let { gdprConsent, uspConsent } = getConsentData(bidderRequests); - - try { - result = JSON.parse(response); - - bids = OPEN_RTB_PROTOCOL.interpretResponse( - result, - bidderRequests, - s2sConfig - ); - - bids.forEach(({adUnit, bid}) => { - if (isValid(adUnit, bid, bidderRequests)) { - addBidResponse(adUnit, bid); + processPBSRequest(s2sBidRequest, bidRequests, ajax, { + onResponse: function (isValid, requestedBidders) { + if (isValid) { + bidRequests.forEach(bidderRequest => events.emit(CONSTANTS.EVENTS.BIDDER_DONE, bidderRequest)); + } + done(); + doClientSideSyncs(requestedBidders, gdprConsent, uspConsent); + }, + onError: done, + onBid: function ({adUnit, bid}) { + if (isValid(adUnit, bid)) { + addBidResponse(adUnit, bid); + } } - }); - - bidderRequests.forEach(bidderRequest => events.emit(CONSTANTS.EVENTS.BIDDER_DONE, bidderRequest)); - } catch (error) { - logError(error); - } - - if (!result || (result.status && includes(result.status, 'Error'))) { - logError('error parsing response: ', result.status); + }) } - - done(); - doClientSideSyncs(requestedBidders, gdprConsent, uspConsent); - } + }; // Listen for bid won to call wurl events.on(CONSTANTS.EVENTS.BID_WON, bidWonHandler); @@ -1163,6 +1224,66 @@ export function PrebidServer() { }); } +/** + * Build and send the appropriate HTTP request over the network, then interpret the response. + * @param s2sBidRequest + * @param bidRequests + * @param ajax + * @param onResponse {function(boolean, Array[String])} invoked on a successful HTTP response - with a flag indicating whether it was successful, + * and a list of the unique bidder codes that were sent in the request + * @param onError {function(String, {})} invoked on HTTP failure - with status message and XHR error + * @param onBid {function({})} invoked once for each bid in the response - with the bid as returned by interpretResponse + */ +export const processPBSRequest = hook('sync', function (s2sBidRequest, bidRequests, ajax, {onResponse, onError, onBid}) { + let { gdprConsent } = getConsentData(bidRequests); + const adUnits = deepClone(s2sBidRequest.ad_units); + + // at this point ad units should have a size array either directly or mapped so filter for that + const validAdUnits = adUnits.filter(unit => + unit.mediaTypes && (unit.mediaTypes.native || (unit.mediaTypes.banner && unit.mediaTypes.banner.sizes) || (unit.mediaTypes.video && unit.mediaTypes.video.playerSize)) + ); + + // in case config.bidders contains invalid bidders, we only process those we sent requests for + const requestedBidders = validAdUnits + .map(adUnit => adUnit.bids.map(bid => bid.bidder).filter(uniques)) + .reduce(flatten) + .filter(uniques); + + const ortb2 = new ORTB2(s2sBidRequest, bidRequests, validAdUnits, requestedBidders); + const request = ortb2.buildRequest(); + const requestJson = request && JSON.stringify(request); + logInfo('BidRequest: ' + requestJson); + const endpointUrl = getMatchingConsentUrl(s2sBidRequest.s2sConfig.endpoint, gdprConsent); + if (request && requestJson && endpointUrl) { + ajax( + endpointUrl, + { + success: function (response) { + let result; + try { + result = JSON.parse(response); + const bids = ortb2.interpretResponse(result); + bids.forEach(onBid); + } catch (error) { + logError(error); + } + if (!result || (result.status && includes(result.status, 'Error'))) { + logError('error parsing response: ', result ? result.status : 'not valid JSON'); + onResponse(false, requestedBidders); + } else { + onResponse(true, requestedBidders); + } + }, + error: onError + }, + requestJson, + {contentType: 'text/plain', withCredentials: true} + ); + } else { + logError('PBS request not made. Check endpoints.'); + } +}, 'processPBSRequest'); + /** * Global setter that sets eids permissions for bidders * This setter is to be used by userId module when included diff --git a/modules/prebidmanagerAnalyticsAdapter.js b/modules/prebidmanagerAnalyticsAdapter.js index a1a0a636e3c..1ac7ba84916 100644 --- a/modules/prebidmanagerAnalyticsAdapter.js +++ b/modules/prebidmanagerAnalyticsAdapter.js @@ -7,7 +7,7 @@ import { getStorageManager } from '../src/storageManager.js'; /** * prebidmanagerAnalyticsAdapter.js - analytics adapter for prebidmanager */ -export const storage = getStorageManager(undefined, 'prebidmanager'); +export const storage = getStorageManager({gvlid: undefined, moduleName: 'prebidmanager'}); const DEFAULT_EVENT_URL = 'https://endpoint.prebidmanager.com/endpoint' const analyticsType = 'endpoint'; const analyticsName = 'Prebid Manager Analytics: '; diff --git a/modules/priceFloors.js b/modules/priceFloors.js index b55638c1a5c..ff4213f1330 100644 --- a/modules/priceFloors.js +++ b/modules/priceFloors.js @@ -1,13 +1,30 @@ -import { parseUrl, deepAccess, parseGPTSingleSizeArray, getGptSlotInfoForAdUnitCode, deepSetValue, logWarn, deepClone, getParameterByName, generateUUID, logError, logInfo, isNumber, pick, debugTurnedOn } from '../src/utils.js'; -import { getGlobal } from '../src/prebidGlobal.js'; -import { config } from '../src/config.js'; -import { ajaxBuilder } from '../src/ajax.js'; -import events from '../src/events.js'; +import { + debugTurnedOn, + deepAccess, + deepClone, + deepSetValue, + generateUUID, + getGptSlotInfoForAdUnitCode, + getParameterByName, + isNumber, + logError, + logInfo, + logWarn, + parseGPTSingleSizeArray, + parseUrl, + pick +} from '../src/utils.js'; +import {getGlobal} from '../src/prebidGlobal.js'; +import {config} from '../src/config.js'; +import {ajaxBuilder} from '../src/ajax.js'; +import * as events from '../src/events.js'; import CONSTANTS from '../src/constants.json'; -import { getHook } from '../src/hook.js'; -import { createBid } from '../src/bidfactory.js'; -import find from 'core-js-pure/features/array/find.js'; -import { getRefererInfo } from '../src/refererDetection.js'; +import {getHook} from '../src/hook.js'; +import {createBid} from '../src/bidfactory.js'; +import {find} from '../src/polyfill.js'; +import {getRefererInfo} from '../src/refererDetection.js'; +import {bidderSettings} from '../src/bidderSettings.js'; +import {auctionManager} from '../src/auctionManager.js'; /** * @summary This Module is intended to provide users with the ability to dynamically set and enforce price floors on a per auction basis. @@ -65,9 +82,14 @@ function getHostNameFromReferer(referer) { } // First look into bidRequest! -function getGptSlotFromBidRequest(bidRequest) { - const isGam = deepAccess(bidRequest, 'ortb2Imp.ext.data.adserver.name') === 'gam'; - return isGam && bidRequest.ortb2Imp.ext.data.adserver.adslot; +function getGptSlotFromAdUnit(transactionId, {index = auctionManager.index} = {}) { + const adUnit = index.getAdUnit({transactionId}); + const isGam = deepAccess(adUnit, 'ortb2Imp.ext.data.adserver.name') === 'gam'; + return isGam && adUnit.ortb2Imp.ext.data.adserver.adslot; +} + +function getAdUnitCode(request, response, {index = auctionManager.index} = {}) { + return request?.adUnitCode || index.getAdUnit(response).code; } /** @@ -76,9 +98,9 @@ function getGptSlotFromBidRequest(bidRequest) { export let fieldMatchingFunctions = { 'size': (bidRequest, bidResponse) => parseGPTSingleSizeArray(bidResponse.size) || '*', 'mediaType': (bidRequest, bidResponse) => bidResponse.mediaType || 'banner', - 'gptSlot': (bidRequest, bidResponse) => getGptSlotFromBidRequest(bidRequest) || getGptSlotInfoForAdUnitCode(bidRequest.adUnitCode).gptSlot, + 'gptSlot': (bidRequest, bidResponse) => getGptSlotFromAdUnit((bidRequest || bidResponse).transactionId) || getGptSlotInfoForAdUnitCode(getAdUnitCode(bidRequest, bidResponse)).gptSlot, 'domain': (bidRequest, bidResponse) => referrerHostname || getHostNameFromReferer(getRefererInfo().referer), - 'adUnitCode': (bidRequest, bidResponse) => bidRequest.adUnitCode + 'adUnitCode': (bidRequest, bidResponse) => getAdUnitCode(bidRequest, bidResponse) } /** @@ -116,7 +138,7 @@ export function getFirstMatchingFloor(floorData, bidObject, responseObject = {}) let matchingData = { floorMin: floorData.floorMin || 0, - floorRuleValue: floorData.values[matchingRule] || floorData.default, + floorRuleValue: isNaN(floorData.values[matchingRule]) ? floorData.default : floorData.values[matchingRule], matchingData: allPossibleMatches[0], // the first possible match is an "exact" so contains all data relevant for anlaytics adapters matchingRule }; @@ -147,7 +169,7 @@ function generatePossibleEnumerations(arrayOfFields, delimiter) { * @summary If a the input bidder has a registered cpmadjustment it returns the input CPM after being adjusted */ export function getBiddersCpmAdjustment(bidderName, inputCpm, bid = {}) { - const adjustmentFunction = deepAccess(getGlobal(), `bidderSettings.${bidderName}.bidCpmAdjustment`) || deepAccess(getGlobal(), 'bidderSettings.standard.bidCpmAdjustment'); + const adjustmentFunction = bidderSettings.get(bidderName, 'bidCpmAdjustment'); if (adjustmentFunction) { return parseFloat(adjustmentFunction(inputCpm, {...bid, cpm: inputCpm})); } @@ -664,15 +686,16 @@ function shouldFloorBid(floorData, floorInfo, bid) { * And if the rule we find determines a bid should be floored we will do so. */ export function addBidResponseHook(fn, adUnitCode, bid) { - let floorData = _floorDataForAuction[this.bidderRequest.auctionId]; - // if no floor data or associated bidRequest then bail - const matchingBidRequest = find(this.bidderRequest.bids, bidRequest => bidRequest.bidId && bidRequest.bidId === bid.requestId); - if (!floorData || !bid || floorData.skipped || !matchingBidRequest) { + let floorData = _floorDataForAuction[bid.auctionId]; + // if no floor data then bail + if (!floorData || !bid || floorData.skipped) { return fn.call(this, adUnitCode, bid); } + const matchingBidRequest = auctionManager.index.getBidRequest(bid) + // get the matching rule - let floorInfo = getFirstMatchingFloor(floorData.data, {...matchingBidRequest}, {...bid, size: [bid.width, bid.height]}); + let floorInfo = getFirstMatchingFloor(floorData.data, matchingBidRequest, {...bid, size: [bid.width, bid.height]}); if (!floorInfo.matchingFloor) { logWarn(`${MODULE_NAME}: unable to determine a matching price floor for bidResponse`, bid); @@ -706,7 +729,7 @@ export function addBidResponseHook(fn, adUnitCode, bid) { if (shouldFloorBid(floorData, floorInfo, bid)) { // bid fails floor -> throw it out // create basic bid no-bid with necessary data fro analytics adapters - let flooredBid = createBid(CONSTANTS.STATUS.NO_BID, matchingBidRequest); + let flooredBid = createBid(CONSTANTS.STATUS.NO_BID, bid.getIdentifiers()); Object.assign(flooredBid, pick(bid, [ 'floorData', 'width', diff --git a/modules/pubCommonId.js b/modules/pubCommonId.js index 6ecf0723aae..faca59cce1c 100644 --- a/modules/pubCommonId.js +++ b/modules/pubCommonId.js @@ -5,7 +5,7 @@ */ import { logMessage, parseUrl, buildUrl, triggerPixel, generateUUID, isArray } from '../src/utils.js'; import { config } from '../src/config.js'; -import events from '../src/events.js'; +import * as events from '../src/events.js'; import CONSTANTS from '../src/constants.json'; import { getStorageManager } from '../src/storageManager.js'; diff --git a/modules/publinkIdSystem.js b/modules/publinkIdSystem.js index 990227e7cfe..9d5645a38cb 100644 --- a/modules/publinkIdSystem.js +++ b/modules/publinkIdSystem.js @@ -16,7 +16,7 @@ const GVLID = 24; const PUBLINK_COOKIE = '_publink'; const PUBLINK_S2S_COOKIE = '_publink_srv'; -export const storage = getStorageManager(GVLID); +export const storage = getStorageManager({gvlid: GVLID}); function isHex(s) { return /^[A-F0-9]+$/i.test(s); diff --git a/modules/pubmaticBidAdapter.js b/modules/pubmaticBidAdapter.js index 366a0326054..d64c6092afa 100644 --- a/modules/pubmaticBidAdapter.js +++ b/modules/pubmaticBidAdapter.js @@ -12,6 +12,7 @@ const USER_SYNC_URL_IMAGE = 'https://image8.pubmatic.com/AdServer/ImgSync?p='; const DEFAULT_CURRENCY = 'USD'; const AUCTION_TYPE = 1; const GROUPM_ALIAS = {code: 'groupm', gvlid: 98}; +const MARKETPLACE_PARTNERS = ['groupm'] const UNDEFINED = undefined; const DEFAULT_WIDTH = 0; const DEFAULT_HEIGHT = 0; @@ -976,6 +977,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) { @@ -1065,6 +1085,12 @@ export const spec = { * @return ServerRequest Info describing the request to the server. */ buildRequests: (validBidRequests, bidderRequest) => { + if (bidderRequest && MARKETPLACE_PARTNERS.includes(bidderRequest.bidderCode)) { + // We have got the buildRequests function call for Marketplace Partners + logInfo('For all publishers using ' + bidderRequest.bidderCode + ' bidder, the PubMatic bidder will also be enabled so PubMatic server will respond back with the bids that needs to be submitted for PubMatic and ' + bidderRequest.bidderCode + ' in the network call sent by PubMatic bidder. Hence we do not want to create a network call for ' + bidderRequest.bidderCode + '. This way we are trying to save a network call from browser.'); + return; + } + var refererInfo; if (bidderRequest && bidderRequest.refererInfo) { refererInfo = bidderRequest.refererInfo; @@ -1075,12 +1101,13 @@ export const spec = { var dctrArr = []; var bid; var blockedIabCategories = []; + var allowedIabCategories = []; validBidRequests.forEach(originalBid => { bid = deepClone(originalBid); bid.params.adSlot = bid.params.adSlot || ''; _parseAdSlot(bid); - if (bid.params.hasOwnProperty('video')) { + if ((bid.mediaTypes && bid.mediaTypes.hasOwnProperty('video')) || bid.params.hasOwnProperty('video')) { // Nothing to do } else { // If we have a native mediaType configured alongside banner, its ok if the banner size is not set in width and height @@ -1106,6 +1133,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); @@ -1175,7 +1205,7 @@ export const spec = { } _handleEids(payload, validBidRequests); - _blockedIabCategoriesValidation(payload, blockedIabCategories); + _handleFlocId(payload, validBidRequests); // First Party Data const commonFpd = config.getConfig('ortb2') || {}; @@ -1185,7 +1215,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 +1326,13 @@ export const spec = { }; } + // if from the server-response the bid.ext.marketplace is set then + // submit the bid to Prebid as marketplace name + if (bid.ext && !!bid.ext.marketplace && MARKETPLACE_PARTNERS.includes(bid.ext.marketplace)) { + newBid.bidderCode = bid.ext.marketplace; + newBid.bidder = bid.ext.marketplace; + } + bidResponses.push(newBid); }); }); diff --git a/modules/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/richaudienceBidAdapter.js b/modules/richaudienceBidAdapter.js index 0f62e988249..b49d7c5584c 100755 --- a/modules/richaudienceBidAdapter.js +++ b/modules/richaudienceBidAdapter.js @@ -59,10 +59,15 @@ export const spec = { REFERER = (typeof bidderRequest.refererInfo.referer != 'undefined' ? encodeURIComponent(bidderRequest.refererInfo.referer) : null) payload.gdpr_consent = ''; - payload.gdpr = bidderRequest.gdprConsent.gdprApplies; + payload.gdpr = false; if (bidderRequest && bidderRequest.gdprConsent) { - payload.gdpr_consent = bidderRequest.gdprConsent.consentString; + if (typeof bidderRequest.gdprConsent.gdprApplies != 'undefined') { + payload.gdpr = bidderRequest.gdprConsent.gdprApplies; + } + if (typeof bidderRequest.gdprConsent.consentString != 'undefined') { + payload.gdpr_consent = bidderRequest.gdprConsent.consentString; + } } var payloadString = JSON.stringify(payload); @@ -192,6 +197,13 @@ function raiGetSizes(bid) { function raiGetDemandType(bid) { let raiFormat = 'display'; + if (typeof bid.sizes != 'undefined') { + bid.sizes.forEach(function (sz) { + if ((sz[0] == '1800' && sz[1] == '1000') || (sz[0] == '1' && sz[1] == '1')) { + raiFormat = 'skin' + } + }) + } if (bid.mediaTypes != undefined) { if (bid.mediaTypes.video != undefined) { raiFormat = 'video'; @@ -295,7 +307,7 @@ function raiGetFloor(bid, config) { raiFloor = bid.params.bidfloor; } else if (typeof bid.getFloor == 'function') { let floorSpec = bid.getFloor({ - currency: config.getConfig('currency.adServerCurrency'), + currency: config.getConfig('floors.data.currency') != null ? config.getConfig('floors.data.currency') : 'USD', mediaType: typeof bid.mediaTypes['banner'] == 'object' ? 'banner' : 'video', size: '*' }) diff --git a/modules/riseBidAdapter.js b/modules/riseBidAdapter.js index 70777fe03f2..a8ea023d46a 100644 --- a/modules/riseBidAdapter.js +++ b/modules/riseBidAdapter.js @@ -1,17 +1,17 @@ -import { logWarn, isArray, isFn, deepAccess, isEmpty, contains, timestamp, getBidIdParameter } from '../src/utils.js'; +import { logWarn, logInfo, isArray, isFn, deepAccess, isEmpty, contains, timestamp, getBidIdParameter, triggerPixel, isInteger } from '../src/utils.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; -import {VIDEO} from '../src/mediaTypes.js'; +import {BANNER, VIDEO} from '../src/mediaTypes.js'; import {config} from '../src/config.js'; -const SUPPORTED_AD_TYPES = [VIDEO]; +const SUPPORTED_AD_TYPES = [BANNER, VIDEO]; const BIDDER_CODE = 'rise'; -const ADAPTER_VERSION = '5.0.0'; +const ADAPTER_VERSION = '6.0.0'; const TTL = 360; const CURRENCY = 'USD'; const SELLER_ENDPOINT = 'https://hb.yellowblue.io/'; const MODES = { - PRODUCTION: 'hb', - TEST: 'hb-test' + PRODUCTION: 'hb-multi', + TEST: 'hb-multi-test' } const SUPPORTED_SYNC_METHODS = { IFRAME: 'iframe', @@ -23,7 +23,7 @@ export const spec = { gvlid: 1043, version: ADAPTER_VERSION, supportedMediaTypes: SUPPORTED_AD_TYPES, - isBidRequestValid: function(bidRequest) { + isBidRequestValid: function (bidRequest) { if (!bidRequest.params) { logWarn('no params have been set to Rise adapter'); return false; @@ -36,54 +36,70 @@ export const spec = { return true; }, - buildRequests: function (bidRequests, bidderRequest) { - if (bidRequests.length === 0) { - return []; - } + buildRequests: function (validBidRequests, bidderRequest) { + const combinedRequestsObject = {}; - const requests = []; + // use data from the first bid, to create the general params for all bids + const generalObject = validBidRequests[0]; + const testMode = generalObject.params.testMode; - bidRequests.forEach(bid => { - requests.push(buildVideoRequest(bid, bidderRequest)); - }); + combinedRequestsObject.params = generateGeneralParams(generalObject, bidderRequest); + combinedRequestsObject.bids = generateBidsParams(validBidRequests, bidderRequest); - return requests; + return { + method: 'POST', + url: getEndpoint(testMode), + data: combinedRequestsObject + } }, - interpretResponse: function({body}) { + interpretResponse: function ({body}) { const bidResponses = []; - const bidResponse = { - requestId: body.requestId, - cpm: body.cpm, - width: body.width, - height: body.height, - creativeId: body.requestId, - currency: body.currency, - netRevenue: body.netRevenue, - ttl: body.ttl || TTL, - vastXml: body.vastXml, - mediaType: VIDEO - }; - - if (body.adomain && body.adomain.length) { - bidResponse.meta = {}; - bidResponse.meta.advertiserDomains = body.adomain + if (body.bids) { + body.bids.forEach(adUnit => { + const bidResponse = { + requestId: adUnit.requestId, + cpm: adUnit.cpm, + currency: adUnit.currency || CURRENCY, + width: adUnit.width, + height: adUnit.height, + ttl: adUnit.ttl || TTL, + creativeId: adUnit.requestId, + netRevenue: adUnit.netRevenue || true, + nurl: adUnit.nurl, + mediaType: adUnit.mediaType, + meta: { + mediaType: adUnit.mediaType + } + }; + + if (adUnit.mediaType === VIDEO) { + bidResponse.vastXml = adUnit.vastXml; + } else if (adUnit.mediaType === BANNER) { + bidResponse.ad = adUnit.ad; + } + + if (adUnit.adomain && adUnit.adomain.length) { + bidResponse.meta.advertiserDomains = adUnit.adomain; + } + + bidResponses.push(bidResponse); + }); } - bidResponses.push(bidResponse); return bidResponses; }, - getUserSyncs: function(syncOptions, serverResponses) { + getUserSyncs: function (syncOptions, serverResponses) { const syncs = []; for (const response of serverResponses) { - if (syncOptions.iframeEnabled && response.body.userSyncURL) { + if (syncOptions.iframeEnabled && response.body.params.userSyncURL) { syncs.push({ type: 'iframe', - url: response.body.userSyncURL + url: response.body.params.userSyncURL }); } - if (syncOptions.pixelEnabled && isArray(response.body.userSyncPixels)) { - const pixels = response.body.userSyncPixels.map(pixel => { + if (syncOptions.pixelEnabled && isArray(response.body.params.userSyncPixels)) { + const pixels = response.body.params.userSyncPixels.map(pixel => { return { type: 'image', url: pixel @@ -93,6 +109,16 @@ export const spec = { } } return syncs; + }, + onBidWon: function (bid) { + if (bid == null) { + return; + } + + logInfo('onBidWon:', bid); + if (bid.hasOwnProperty('nurl') && bid.nurl.length > 0) { + triggerPixel(bid.nurl); + } } }; @@ -103,46 +129,33 @@ registerBidder(spec); * @param bid {bid} * @returns {Number} */ -function getFloor(bid) { +function getFloor(bid, mediaType) { if (!isFn(bid.getFloor)) { return 0; } let floorResult = bid.getFloor({ currency: CURRENCY, - mediaType: VIDEO, + mediaType: mediaType, size: '*' }); return floorResult.currency === CURRENCY && floorResult.floor ? floorResult.floor : 0; } /** - * Build the video request - * @param bid {bid} - * @param bidderRequest {bidderRequest} - * @returns {Object} - */ -function buildVideoRequest(bid, bidderRequest) { - const sellerParams = generateParameters(bid, bidderRequest); - const {params} = bid; - return { - method: 'GET', - url: getEndpoint(params.testMode), - data: sellerParams - }; -} - -/** - * Get the the ad size from the bid + * Get the the ad sizes array from the bid * @param bid {bid} * @returns {Array} */ -function getSizes(bid) { - if (deepAccess(bid, 'mediaTypes.video.sizes')) { - return bid.mediaTypes.video.sizes[0]; +function getSizesArray(bid, mediaType) { + let sizesArray = [] + + if (deepAccess(bid, `mediaTypes.${mediaType}.sizes`)) { + sizesArray = bid.mediaTypes[mediaType].sizes; } else if (Array.isArray(bid.sizes) && bid.sizes.length > 0) { - return bid.sizes[0]; + sizesArray = bid.sizes; } - return []; + + return sizesArray; } /** @@ -239,122 +252,180 @@ function getDeviceType(ua) { return '1'; } +function generateBidsParams(validBidRequests, bidderRequest) { + const bidsArray = []; + + if (validBidRequests.length) { + validBidRequests.forEach(bid => { + bidsArray.push(generateBidParameters(bid, bidderRequest)); + }); + } + + return bidsArray; +} + /** - * Generate query parameters for the request - * @param bid {bid} - * @param bidderRequest {bidderRequest} - * @returns {Object} + * Generate bid specific parameters + * @param {bid} bid + * @param {bidderRequest} bidderRequest + * @returns {Object} bid specific params object */ -function generateParameters(bid, bidderRequest) { +function generateBidParameters(bid, bidderRequest) { const {params} = bid; - const timeout = config.getConfig('bidderTimeout'); - const {syncEnabled, filterSettings} = config.getConfig('userSync') || {}; - const [width, height] = getSizes(bid); - const {bidderCode} = bidderRequest; - const domain = window.location.hostname; + const mediaType = isBanner(bid) ? BANNER : VIDEO; + const sizesArray = getSizesArray(bid, mediaType); // fix floor price in case of NAN if (isNaN(params.floorPrice)) { params.floorPrice = 0; } - const requestParams = { + const bidObject = { + mediaType, + adUnitCode: getBidIdParameter('adUnitCode', bid), + sizes: sizesArray, + floorPrice: Math.max(getFloor(bid, mediaType), params.floorPrice), + bidId: getBidIdParameter('bidId', bid), + bidderRequestId: getBidIdParameter('bidderRequestId', bid), + transactionId: getBidIdParameter('transactionId', bid), + }; + + const pos = deepAccess(bid, `mediaTypes.${mediaType}.pos`); + if (pos) { + bidObject.pos = pos; + } + + const gpid = deepAccess(bid, `ortb2Imp.ext.gpid`); + if (gpid) { + bidObject.gpid = gpid; + } + + const placementId = params.placementId || deepAccess(bid, `mediaTypes.${mediaType}.name`); + if (placementId) { + bidObject.placementId = placementId; + } + + if (mediaType === VIDEO) { + const playbackMethod = deepAccess(bid, `mediaTypes.video.playbackmethod`); + let playbackMethodValue; + + // verify playbackMethod is of type integer array, or integer only. + if (Array.isArray(playbackMethod) && isInteger(playbackMethod[0])) { + // only the first playbackMethod in the array will be used, according to OpenRTB 2.5 recommendation + playbackMethodValue = playbackMethod[0]; + } else if (isInteger(playbackMethod)) { + playbackMethodValue = playbackMethod; + } + + if (playbackMethodValue) { + bidObject.playbackMethod = playbackMethodValue; + } + + const placement = deepAccess(bid, `mediaTypes.video.placement`); + if (placement) { + bidObject.placement = placement; + } + + const minDuration = deepAccess(bid, `mediaTypes.video.minduration`); + if (minDuration) { + bidObject.minDuration = minDuration; + } + + const maxDuration = deepAccess(bid, `mediaTypes.video.maxduration`); + if (maxDuration) { + bidObject.maxDuration = maxDuration; + } + + const skip = deepAccess(bid, `mediaTypes.video.skip`); + if (skip) { + bidObject.skip = skip; + } + + const linearity = deepAccess(bid, `mediaTypes.video.linearity`); + if (linearity) { + bidObject.linearity = linearity; + } + } + + return bidObject; +} + +function isBanner(bid) { + return bid.mediaTypes && bid.mediaTypes.banner; +} + +/** + * Generate params that are common between all bids + * @param {single bid object} generalObject + * @param {bidderRequest} bidderRequest + * @returns {object} the common params object + */ +function generateGeneralParams(generalObject, bidderRequest) { + const domain = window.location.hostname; + const {syncEnabled, filterSettings} = config.getConfig('userSync') || {}; + const {bidderCode} = bidderRequest; + const generalBidParams = generalObject.params; + const timeout = config.getConfig('bidderTimeout'); + + // these params are snake_case instead of camelCase to allow backwards compatability on the server. + // in the future, these will be converted to camelCase to match our convention. + const generalParams = { wrapper_type: 'prebidjs', wrapper_vendor: '$$PREBID_GLOBAL$$', wrapper_version: '$prebid.version$', adapter_version: ADAPTER_VERSION, auction_start: timestamp(), - ad_unit_code: getBidIdParameter('adUnitCode', bid), - tmax: timeout, - width: width, - height: height, - publisher_id: params.org, - floor_price: Math.max(getFloor(bid), params.floorPrice), - ua: navigator.userAgent, - bid_id: getBidIdParameter('bidId', bid), - bidder_request_id: getBidIdParameter('bidderRequestId', bid), - transaction_id: getBidIdParameter('transactionId', bid), - session_id: getBidIdParameter('auctionId', bid), + publisher_id: generalBidParams.org, publisher_name: domain, site_domain: domain, dnt: (navigator.doNotTrack == 'yes' || navigator.doNotTrack == '1' || navigator.msDoNotTrack == '1') ? 1 : 0, - device_type: getDeviceType(navigator.userAgent) - }; + device_type: getDeviceType(navigator.userAgent), + ua: navigator.userAgent, + session_id: getBidIdParameter('auctionId', generalObject), + tmax: timeout + } - const userIdsParam = getBidIdParameter('userId', bid); + const userIdsParam = getBidIdParameter('userId', generalObject); if (userIdsParam) { - requestParams.userIds = JSON.stringify(userIdsParam); + generalParams.userIds = JSON.stringify(userIdsParam); } const ortb2Metadata = config.getConfig('ortb2') || {}; if (ortb2Metadata.site) { - requestParams.site_metadata = JSON.stringify(ortb2Metadata.site); + generalParams.site_metadata = JSON.stringify(ortb2Metadata.site); } if (ortb2Metadata.user) { - requestParams.user_metadata = JSON.stringify(ortb2Metadata.user); - } - - const playbackMethod = deepAccess(bid, 'mediaTypes.video.playbackmethod'); - if (playbackMethod) { - requestParams.playback_method = playbackMethod; - } - const placement = deepAccess(bid, 'mediaTypes.video.placement'); - if (placement) { - requestParams.placement = placement; - } - const pos = deepAccess(bid, 'mediaTypes.video.pos'); - if (pos) { - requestParams.pos = pos; - } - const minduration = deepAccess(bid, 'mediaTypes.video.minduration'); - if (minduration) { - requestParams.min_duration = minduration; - } - const maxduration = deepAccess(bid, 'mediaTypes.video.maxduration'); - if (maxduration) { - requestParams.max_duration = maxduration; - } - const skip = deepAccess(bid, 'mediaTypes.video.skip'); - if (skip) { - requestParams.skip = skip; - } - const linearity = deepAccess(bid, 'mediaTypes.video.linearity'); - if (linearity) { - requestParams.linearity = linearity; - } - - if (params.placementId) { - requestParams.placement_id = params.placementId; + generalParams.user_metadata = JSON.stringify(ortb2Metadata.user); } if (syncEnabled) { const allowedSyncMethod = getAllowedSyncMethod(filterSettings, bidderCode); if (allowedSyncMethod) { - requestParams.cs_method = allowedSyncMethod; + generalParams.cs_method = allowedSyncMethod; } } if (bidderRequest.uspConsent) { - requestParams.us_privacy = bidderRequest.uspConsent; + generalParams.us_privacy = bidderRequest.uspConsent; } if (bidderRequest && bidderRequest.gdprConsent && bidderRequest.gdprConsent.gdprApplies) { - requestParams.gdpr = bidderRequest.gdprConsent.gdprApplies; - requestParams.gdpr_consent = bidderRequest.gdprConsent.consentString; + generalParams.gdpr = bidderRequest.gdprConsent.gdprApplies; + generalParams.gdpr_consent = bidderRequest.gdprConsent.consentString; } - if (params.ifa) { - requestParams.ifa = params.ifa; + if (generalBidParams.ifa) { + generalParams.ifa = generalBidParams.ifa; } - if (bid.schain) { - requestParams.schain = getSupplyChain(bid.schain); + if (generalObject.schain) { + generalParams.schain = getSupplyChain(generalObject.schain); } if (bidderRequest && bidderRequest.refererInfo) { - requestParams.referrer = deepAccess(bidderRequest, 'refererInfo.referer'); - requestParams.page_url = config.getConfig('pageUrl') || deepAccess(window, 'location.href'); + generalParams.referrer = deepAccess(bidderRequest, 'refererInfo.referer'); + generalParams.page_url = config.getConfig('pageUrl') || deepAccess(window, 'location.href'); } - return requestParams; + return generalParams } diff --git a/modules/roxotAnalyticsAdapter.js b/modules/roxotAnalyticsAdapter.js index c9245d4ae08..b11898b9ea8 100644 --- a/modules/roxotAnalyticsAdapter.js +++ b/modules/roxotAnalyticsAdapter.js @@ -1,10 +1,10 @@ -import { deepClone, getParameterByName, logInfo, logError } from '../src/utils.js'; +import {deepClone, getParameterByName, logError, logInfo} from '../src/utils.js'; import adapter from '../src/AnalyticsAdapter.js'; import CONSTANTS from '../src/constants.json'; import adapterManager from '../src/adapterManager.js'; -import includes from 'core-js-pure/features/array/includes.js'; +import {includes} from '../src/polyfill.js'; import {ajaxBuilder} from '../src/ajax.js'; -import { getStorageManager } from '../src/storageManager.js'; +import {getStorageManager} from '../src/storageManager.js'; const storage = getStorageManager(); diff --git a/modules/rtbhouseBidAdapter.js b/modules/rtbhouseBidAdapter.js index 40389b1e1e0..b8436179a30 100644 --- a/modules/rtbhouseBidAdapter.js +++ b/modules/rtbhouseBidAdapter.js @@ -1,7 +1,7 @@ -import { isArray, deepAccess, getOrigin, logError } from '../src/utils.js'; -import { BANNER, NATIVE } from '../src/mediaTypes.js'; -import { registerBidder } from '../src/adapters/bidderFactory.js'; -import includes from 'core-js-pure/features/array/includes.js'; +import {deepAccess, getOrigin, isArray, logError} from '../src/utils.js'; +import {BANNER, NATIVE} from '../src/mediaTypes.js'; +import {registerBidder} from '../src/adapters/bidderFactory.js'; +import {includes} from '../src/polyfill.js'; const BIDDER_CODE = 'rtbhouse'; const REGIONS = ['prebid-eu', 'prebid-us', 'prebid-asia']; @@ -9,6 +9,7 @@ const ENDPOINT_URL = 'creativecdn.com/bidder/prebid/bids'; const DEFAULT_CURRENCY_ARR = ['USD']; // NOTE - USD is the only supported currency right now; Hardcoded for bids const SUPPORTED_MEDIA_TYPES = [BANNER, NATIVE]; const TTL = 55; +const GVLID = 16; // Codes defined by OpenRTB Native Ads 1.1 specification export const OPENRTB = { @@ -36,6 +37,7 @@ export const OPENRTB = { export const spec = { code: BIDDER_CODE, supportedMediaTypes: SUPPORTED_MEDIA_TYPES, + gvlid: GVLID, isBidRequestValid: function (bid) { return !!(includes(REGIONS, bid.params.region) && bid.params.publisherId); diff --git a/modules/rtdModule/index.js b/modules/rtdModule/index.js index 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 5b17048cbd3..69335ff33a8 100644 --- a/modules/rubiconAnalyticsAdapter.js +++ b/modules/rubiconAnalyticsAdapter.js @@ -1,4 +1,4 @@ -import { generateUUID, mergeDeep, deepAccess, parseUrl, logError, pick, isEmpty, logWarn, debugTurnedOn, parseQS, getWindowLocation, isAdUnitCodeMatchingSlot, isNumber, isGptPubadsDefined, _each, deepSetValue } from '../src/utils.js'; +import { generateUUID, mergeDeep, deepAccess, parseUrl, logError, pick, isEmpty, logWarn, debugTurnedOn, parseQS, getWindowLocation, isAdUnitCodeMatchingSlot, isNumber, isGptPubadsDefined, _each, deepSetValue, deepClone, logInfo } from '../src/utils.js'; import adapter from '../src/AnalyticsAdapter.js'; import adapterManager from '../src/adapterManager.js'; import CONSTANTS from '../src/constants.json'; @@ -8,10 +8,11 @@ import { getGlobal } from '../src/prebidGlobal.js'; import { getStorageManager } from '../src/storageManager.js'; const RUBICON_GVL_ID = 52; -export const storage = getStorageManager(RUBICON_GVL_ID, 'rubicon'); +export const storage = getStorageManager({gvlid: RUBICON_GVL_ID, moduleName: 'rubicon'}); const COOKIE_NAME = 'rpaSession'; const LAST_SEEN_EXPIRE_TIME = 1800000; // 30 mins const END_EXPIRE_TIME = 21600000; // 6 hours +const MODULE_NAME = 'Rubicon Analytics'; const pbsErrorMap = { 1: 'timeout-error', @@ -31,7 +32,8 @@ const { BIDDER_DONE, BID_TIMEOUT, BID_WON, - SET_TARGETING + SET_TARGETING, + BILLABLE_EVENT }, STATUS: { GOOD, @@ -55,13 +57,19 @@ const cache = { targeting: {}, timeouts: {}, gpt: {}, + billing: {} }; const BID_REJECTED_IPF = 'rejected-ipf'; export let rubiConf = { pvid: generateUUID().slice(0, 8), - analyticsEventDelay: 0 + analyticsEventDelay: 0, + dmBilling: { + enabled: false, + vendors: [], + waitForAuction: true + } }; // we are saving these as global to this module so that if a pub accidentally overwrites the entire // rubicon object, then we do not lose other data @@ -76,7 +84,7 @@ export function getHostNameFromReferer(referer) { try { rubiconAdapter.referrerHostname = parseUrl(referer, { noDecodeWholeURL: true }).hostname; } catch (e) { - logError('Rubicon Analytics: Unable to parse hostname from supplied url: ', referer, e); + logError(`${MODULE_NAME}: Unable to parse hostname from supplied url: `, referer, e); rubiconAdapter.referrerHostname = ''; } return rubiconAdapter.referrerHostname @@ -115,6 +123,55 @@ function formatSource(src) { return src.toLowerCase(); } +function getBillingPayload(event) { + // for now we are mapping all events to type "general", later we will expand support for specific types + let billingEvent = deepClone(event); + billingEvent.type = 'general'; + billingEvent.accountId = accountId; + // mark as sent + deepSetValue(cache.billing, `${event.vendor}.${event.billingId}`, true); + return billingEvent; +} + +function sendBillingEvent(event) { + let message = getBasicEventDetails(undefined, 'soloBilling'); + message.billableEvents = [getBillingPayload(event)]; + ajax( + rubiconAdapter.getUrl(), + null, + JSON.stringify(message), + { + contentType: 'application/json' + } + ); +} + +function getBasicEventDetails(auctionId, trigger) { + let auctionCache = cache.auctions[auctionId]; + let referrer = config.getConfig('pageUrl') || pageReferer || (auctionCache && auctionCache.referrer); + let message = { + timestamps: { + prebidLoaded: rubiconAdapter.MODULE_INITIALIZED_TIME, + auctionEnded: auctionCache ? auctionCache.endTs : undefined, + eventTime: Date.now() + }, + trigger, + integration: rubiConf.int_type || DEFAULT_INTEGRATION, + version: '$prebid.version$', + referrerUri: referrer, + referrerHostname: rubiconAdapter.referrerHostname || getHostNameFromReferer(referrer), + channel: 'web', + }; + if (rubiConf.wrapperName) { + message.wrapper = { + name: rubiConf.wrapperName, + family: rubiConf.wrapperFamily, + rule: rubiConf.rule_name + } + } + return message; +} + function sendMessage(auctionId, bidWonId, trigger) { function formatBid(bid) { return pick(bid, [ @@ -160,28 +217,8 @@ function sendMessage(auctionId, bidWonId, trigger) { samplingFactor }); } + let message = getBasicEventDetails(auctionId, trigger); let auctionCache = cache.auctions[auctionId]; - let referrer = config.getConfig('pageUrl') || (auctionCache && auctionCache.referrer); - let message = { - timestamps: { - prebidLoaded: rubiconAdapter.MODULE_INITIALIZED_TIME, - auctionEnded: auctionCache.endTs, - eventTime: Date.now() - }, - trigger, - integration: rubiConf.int_type || DEFAULT_INTEGRATION, - version: '$prebid.version$', - referrerUri: referrer, - referrerHostname: rubiconAdapter.referrerHostname || getHostNameFromReferer(referrer), - channel: 'web', - }; - if (rubiConf.wrapperName) { - message.wrapper = { - name: rubiConf.wrapperName, - family: rubiConf.wrapperFamily, - rule: rubiConf.rule_name - } - } if (auctionCache && !auctionCache.sent) { let adUnitMap = Object.keys(auctionCache.bids).reduce((adUnits, bidId) => { let bid = auctionCache.bids[bidId]; @@ -195,6 +232,7 @@ function sendMessage(auctionId, bidWonId, trigger) { 'adserverTargeting', () => !isEmpty(cache.targeting[bid.adUnit.adUnitCode]) ? stringProperties(cache.targeting[bid.adUnit.adUnitCode]) : undefined, 'gam', gam => !isEmpty(gam) ? gam : undefined, 'pbAdSlot', + 'gpid', 'pattern' ]); adUnit.bids = []; @@ -234,6 +272,9 @@ function sendMessage(auctionId, bidWonId, trigger) { let auction = { clientTimeoutMillis: auctionCache.timeout, + auctionStart: auctionCache.timestamp, + auctionEnd: auctionCache.endTs, + bidderOrder: auctionCache.bidderOrder, samplingFactor, accountId, adUnits: Object.keys(adUnitMap).map(i => adUnitMap[i]), @@ -318,6 +359,12 @@ function sendMessage(auctionId, bidWonId, trigger) { ]; } + // if we have not sent any billingEvents send them + const pendingBillingEvents = getPendingBillingEvents(auctionCache); + if (pendingBillingEvents && pendingBillingEvents.length) { + message.billableEvents = pendingBillingEvents; + } + ajax( this.getUrl(), null, @@ -328,6 +375,17 @@ function sendMessage(auctionId, bidWonId, trigger) { ); } +function getPendingBillingEvents(auctionCache) { + if (auctionCache && auctionCache.billing && auctionCache.billing.length) { + return auctionCache.billing.reduce((accum, billingEvent) => { + if (deepAccess(cache.billing, `${billingEvent.vendor}.${billingEvent.billingId}`) === false) { + accum.push(getBillingPayload(billingEvent)); + } + return accum; + }, []); + } +} + function adUnitIsOnlyInstream(adUnit) { return adUnit.mediaTypes && Object.keys(adUnit.mediaTypes).length === 1 && deepAccess(adUnit, 'mediaTypes.video.context') === 'instream'; } @@ -356,7 +414,7 @@ function getBidPrice(bid) { try { return Number(prebidGlobal.convertCurrency(cpm, currency, 'USD')); } catch (err) { - logWarn('Rubicon Analytics Adapter: Could not determine the bidPriceUSD of the bid ', bid); + logWarn(`${MODULE_NAME}: Could not determine the bidPriceUSD of the bid `, bid); } } @@ -384,8 +442,9 @@ export function parseBidResponse(bid, previousBidResponse, auctionFloorData) { 'floorRuleValue', () => deepAccess(bid, 'floorData.floorRuleValue'), 'floorRule', () => debugTurnedOn() ? deepAccess(bid, 'floorData.floorRule') : undefined, 'adomains', () => { - let adomains = deepAccess(bid, 'meta.advertiserDomains'); - return Array.isArray(adomains) && adomains.length > 0 ? adomains.slice(0, 10) : undefined + const adomains = deepAccess(bid, 'meta.advertiserDomains'); + const validAdomains = Array.isArray(adomains) && adomains.filter(domain => typeof domain === 'string'); + return validAdomains && validAdomains.length > 0 ? validAdomains.slice(0, 10) : undefined } ]); } @@ -445,7 +504,7 @@ function getRpaCookie() { try { return JSON.parse(window.atob(encodedCookie)); } catch (e) { - logError(`Rubicon Analytics: Unable to decode ${COOKIE_NAME} value: `, e); + logError(`${MODULE_NAME}: Unable to decode ${COOKIE_NAME} value: `, e); } } return {}; @@ -455,7 +514,7 @@ function setRpaCookie(decodedCookie) { try { storage.setDataInLocalStorage(COOKIE_NAME, window.btoa(JSON.stringify(decodedCookie))); } catch (e) { - logError(`Rubicon Analytics: Unable to encode ${COOKIE_NAME} value: `, e); + logError(`${MODULE_NAME}: Unable to encode ${COOKIE_NAME} value: `, e); } } @@ -487,14 +546,20 @@ function subscribeToGamSlots() { window.googletag.pubads().addEventListener('slotRenderEnded', event => { const isMatchingAdSlot = isAdUnitCodeMatchingSlot(event.slot); // loop through auctions and adUnits and mark the info - Object.keys(cache.auctions).forEach(auctionId => { + // only mark first auction which finds a match + let hasMatch = false; + Object.keys(cache.auctions).find(auctionId => { (Object.keys(cache.auctions[auctionId].bids) || []).forEach(bidId => { let bid = cache.auctions[auctionId].bids[bidId]; // if this slot matches this bids adUnit, add the adUnit info - if (isMatchingAdSlot(bid.adUnit.adUnitCode)) { + // only mark it if it already has not been marked + if (!bid.adUnit.gamRendered && isMatchingAdSlot(bid.adUnit.adUnitCode)) { // mark this adUnit as having been rendered by gam cache.auctions[auctionId].gamHasRendered[bid.adUnit.adUnitCode] = true; + // this current auction has an adunit that matched the slot, so mark it as matched so next auciton is skipped + hasMatch = true; + bid.adUnit.gam = pick(event, [ // these come in as `null` from Gpt, which when stringified does not get removed // so set explicitly to undefined when not a number @@ -504,6 +569,9 @@ function subscribeToGamSlots() { 'adSlot', () => event.slot.getAdUnitPath(), 'isSlotEmpty', () => event.isEmpty || undefined ]); + + // this lets us know next iteration not to check this bids adunit + bid.adUnit.gamRendered = true; } }); // Now if all adUnits have gam rendered, send the payload @@ -516,10 +584,35 @@ function subscribeToGamSlots() { sendMessage.call(rubiconAdapter, auctionId, undefined, 'gam') } } + return hasMatch; }); }); } +let pageReferer; + +const isBillingEventValid = event => { + // vendor is whitelisted + const isWhitelistedVendor = rubiConf.dmBilling.vendors.includes(event.vendor); + // event is not duplicated + const isNotDuplicate = typeof deepAccess(cache.billing, `${event.vendor}.${event.billingId}`) !== 'boolean'; + // billingId is defined and a string + return typeof event.billingId === 'string' && isWhitelistedVendor && isNotDuplicate; +} + +const sendOrAddEventToQueue = event => { + // if any auction is not sent yet, then add it to the auction queue + const pendingAuction = Object.keys(cache.auctions).find(auctionId => !cache.auctions[auctionId].sent); + + if (rubiConf.dmBilling.waitForAuction && pendingAuction) { + cache.auctions[pendingAuction].billing = cache.auctions[pendingAuction].billing || []; + cache.auctions[pendingAuction].billing.push(event); + } else { + // send it + sendBillingEvent(event); + } +} + let baseAdapter = adapter({ analyticsType: 'endpoint' }); let rubiconAdapter = Object.assign({}, baseAdapter, { MODULE_INITIALIZED_TIME: Date.now(), @@ -535,7 +628,7 @@ let rubiconAdapter = Object.assign({}, baseAdapter, { if (config.options.endpoint) { this.getUrl = () => config.options.endpoint; } else { - logError('required endpoint missing from rubicon analytics'); + logError(`${MODULE_NAME}: required endpoint missing`); error = true; } if (typeof config.options.sampling !== 'undefined') { @@ -543,7 +636,7 @@ let rubiconAdapter = Object.assign({}, baseAdapter, { } if (typeof config.options.samplingFactor !== 'undefined') { if (typeof config.options.sampling !== 'undefined') { - logWarn('Both options.samplingFactor and options.sampling enabled in rubicon analytics, defaulting to samplingFactor'); + logWarn(`${MODULE_NAME}: Both options.samplingFactor and options.sampling enabled defaulting to samplingFactor`); } samplingFactor = parseFloat(config.options.samplingFactor); config.options.sampling = 1 / samplingFactor; @@ -553,10 +646,10 @@ let rubiconAdapter = Object.assign({}, baseAdapter, { let validSamplingFactors = [1, 10, 20, 40, 100]; if (validSamplingFactors.indexOf(samplingFactor) === -1) { error = true; - logError('invalid samplingFactor for rubicon analytics: ' + samplingFactor + ', must be one of ' + validSamplingFactors.join(', ')); + logError(`${MODULE_NAME}: invalid samplingFactor ${samplingFactor} - must be one of ${validSamplingFactors.join(', ')}`); } else if (!accountId) { error = true; - logError('required accountId missing for rubicon analytics'); + logError(`${MODULE_NAME}: required accountId missing for rubicon analytics`); } if (!error) { @@ -568,6 +661,7 @@ let rubiconAdapter = Object.assign({}, baseAdapter, { accountId = undefined; rubiConf = {}; cache.gpt.registered = false; + cache.billing = {}; baseAdapter.disableAnalytics.apply(this, arguments); }, track({ eventType, args }) { @@ -582,7 +676,8 @@ let rubiconAdapter = Object.assign({}, baseAdapter, { cacheEntry.bids = {}; cacheEntry.bidsWon = {}; cacheEntry.gamHasRendered = {}; - cacheEntry.referrer = deepAccess(args, 'bidderRequests.0.refererInfo.referer'); + cacheEntry.referrer = pageReferer = deepAccess(args, 'bidderRequests.0.refererInfo.referer'); + cacheEntry.bidderOrder = []; const floorData = deepAccess(args, 'bidderRequests.0.bids.0.floorData'); if (floorData) { cacheEntry.floorData = { ...floorData }; @@ -607,6 +702,7 @@ let rubiconAdapter = Object.assign({}, baseAdapter, { } break; case BID_REQUESTED: + cache.auctions[args.auctionId].bidderOrder.push(args.bidderCode); Object.assign(cache.auctions[args.auctionId].bids, args.bids.reduce((memo, bid) => { // mark adUnits we expect bidWon events for cache.auctions[args.auctionId].bidsWon[bid.adUnitCode] = false; @@ -685,7 +781,8 @@ let rubiconAdapter = Object.assign({}, baseAdapter, { } }, 'pbAdSlot', () => deepAccess(bid, 'ortb2Imp.ext.data.pbadslot'), - 'pattern', () => deepAccess(bid, 'ortb2Imp.ext.data.aupname') + 'pattern', () => deepAccess(bid, 'ortb2Imp.ext.data.aupname'), + 'gpid', () => deepAccess(bid, 'ortb2Imp.ext.gpid') ]) ]); return memo; @@ -710,7 +807,7 @@ let rubiconAdapter = Object.assign({}, baseAdapter, { auctionEntry.floorData.enforcements = { ...args.floorData.enforcements }; } if (!bid) { - logError('Rubicon Anlytics Adapter Error: Could not find associated bid request for bid response with requestId: ', args.requestId); + logError(`${MODULE_NAME}: Could not find associated bid request for bid response with requestId: `, args.requestId); break; } bid.source = formatSource(bid.source || args.source); @@ -781,7 +878,12 @@ let rubiconAdapter = Object.assign({}, baseAdapter, { break; case AUCTION_END: // see how long it takes for the payload to come fire - cache.auctions[args.auctionId].endTs = Date.now(); + let auctionData = cache.auctions[args.auctionId]; + // if for some reason the auction did not do its normal thing, this could be undefied so bail + if (!auctionData) { + break; + } + auctionData.endTs = Date.now(); const isOnlyInstreamAuction = args.adUnits && args.adUnits.every(adUnit => adUnitIsOnlyInstream(adUnit)); // If only instream, do not wait around, just send payload @@ -808,6 +910,14 @@ let rubiconAdapter = Object.assign({}, baseAdapter, { } }); break; + case BILLABLE_EVENT: + if (rubiConf.dmBilling.enabled && isBillingEventValid(args)) { + // add to the map indicating it has not been sent yet + deepSetValue(cache.billing, `${args.vendor}.${args.billingId}`, false); + sendOrAddEventToQueue(args); + } else { + logInfo(`${MODULE_NAME}: Billing event ignored`, args); + } } } }); diff --git a/modules/rubiconBidAdapter.js b/modules/rubiconBidAdapter.js index 28e491900f8..afb95d56d69 100644 --- a/modules/rubiconBidAdapter.js +++ b/modules/rubiconBidAdapter.js @@ -1,14 +1,28 @@ -import { mergeDeep, _each, logError, deepAccess, deepSetValue, isStr, isNumber, logWarn, convertTypes, isArray, parseSizesInput, logMessage, formatQS } from '../src/utils.js'; +import { + _each, + convertTypes, + deepAccess, + deepSetValue, + formatQS, + isArray, + isNumber, + isStr, + logError, + logMessage, + logWarn, + mergeDeep, + parseSizesInput +} from '../src/utils.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; import {config} from '../src/config.js'; import {BANNER, VIDEO} from '../src/mediaTypes.js'; -import find from 'core-js-pure/features/array/find.js'; -import { Renderer } from '../src/Renderer.js'; -import { getGlobal } from '../src/prebidGlobal.js'; +import {find} from '../src/polyfill.js'; +import {Renderer} from '../src/Renderer.js'; +import {getGlobal} from '../src/prebidGlobal.js'; const DEFAULT_INTEGRATION = 'pbjs_lite'; const DEFAULT_PBS_INTEGRATION = 'pbjs'; -const DEFAULT_RENDERER_URL = 'https://video-outstream.rubiconproject.com/apex-2.0.0.js'; +const DEFAULT_RENDERER_URL = 'https://video-outstream.rubiconproject.com/apex-2.2.1.js'; // renderer code at https://github.com/rubicon-project/apex2 let rubiConf = {}; @@ -393,6 +407,7 @@ export const spec = { .concat([ 'tk_flint', 'x_source.tid', + 'l_pb_bid_id', 'x_source.pchain', 'p_screen_res', 'rp_floor', @@ -466,6 +481,7 @@ export const spec = { 'rp_secure': '1', 'tk_flint': `${rubiConf.int_type || DEFAULT_INTEGRATION}_v$prebid.version$`, 'x_source.tid': bidRequest.transactionId, + 'l_pb_bid_id': bidRequest.bidId, 'x_source.pchain': params.pchain, 'p_screen_res': _getScreenResolution(), 'tk_user_key': params.userId, @@ -864,7 +880,7 @@ function renderBid(bid) { height: bid.height, vastUrl: bid.vastUrl, placement: { - attachTo: `#${bid.adUnitCode}`, + attachTo: adUnitElement, align: config.align || 'center', position: config.position || 'append' }, diff --git a/modules/s2sTesting.js b/modules/s2sTesting.js index 1f2bb473174..8e9628c8810 100644 --- a/modules/s2sTesting.js +++ b/modules/s2sTesting.js @@ -1,12 +1,12 @@ -import { setS2STestingModule } from '../src/adapterManager.js'; +import {PARTITIONS, partitionBidders, filterBidsForAdUnit, getS2SBidderSet} from '../src/adapterManager.js'; +import {find} from '../src/polyfill.js'; +import {getBidderCodes, logWarn} from '../src/utils.js'; -let s2sTesting = {}; - -const SERVER = 'server'; -const CLIENT = 'client'; - -s2sTesting.SERVER = SERVER; -s2sTesting.CLIENT = CLIENT; +const {CLIENT, SERVER} = PARTITIONS; +export const s2sTesting = { + ...PARTITIONS, + clientTestBidders: new Set() +}; s2sTesting.bidSource = {}; // store bidder sources determined from s2sConfig bidderControl s2sTesting.globalRand = Math.random(); // if 10% of bidderA and 10% of bidderB should be server-side, make it the same 10% @@ -40,7 +40,7 @@ s2sTesting.getSourceBidderMap = function(adUnits = [], allS2SBidders = []) { [SERVER]: Object.keys(sourceBidders[SERVER]), [CLIENT]: Object.keys(sourceBidders[CLIENT]) }; -}; +} /** * @function calculateBidSources determines the source for each s2s bidder based on bidderControl weightings. these can be overridden at the adUnit level @@ -53,7 +53,7 @@ s2sTesting.calculateBidSources = function(s2sConfig = {}) { (s2sConfig.bidders || []).forEach((bidder) => { s2sTesting.bidSource[bidder] = s2sTesting.getSource(bidderControl[bidder] && bidderControl[bidder].bidSource) || SERVER; // default to server }); -}; +} /** * @function getSource() gets a random source based on the given sourceWeights (export just for testing) @@ -76,10 +76,59 @@ s2sTesting.getSource = function(sourceWeights = {}, bidSources = [SERVER, CLIENT // choose the first source with an incremental weight > random weight if (rndWeight < srcIncWeight[source]) return source; } -}; +} + +function doingS2STesting(s2sConfig) { + return s2sConfig && s2sConfig.enabled && s2sConfig.testing; +} -// inject the s2sTesting module into the adapterManager rather than importing it -// importing it causes the packager to include it even when it's not explicitly included in the build -setS2STestingModule(s2sTesting); +function isTestingServerOnly(s2sConfig) { + return Boolean(doingS2STesting(s2sConfig) && s2sConfig.testServerOnly); +} + +const adUnitsContainServerRequests = (adUnits, s2sConfig) => Boolean( + find(adUnits, adUnit => find(adUnit.bids, bid => ( + bid.bidSource || + (s2sConfig.bidderControl && s2sConfig.bidderControl[bid.bidder]) + ) && bid.finalSource === SERVER)) +); + +partitionBidders.before(function (next, adUnits, s2sConfigs) { + const serverBidders = getS2SBidderSet(s2sConfigs); + let serverOnly = false; + + s2sConfigs.forEach((s2sConfig) => { + if (doingS2STesting(s2sConfig)) { + s2sTesting.calculateBidSources(s2sConfig); + const bidderMap = s2sTesting.getSourceBidderMap(adUnits, [...serverBidders]); + // get all adapters doing client testing + bidderMap[CLIENT].forEach((bidder) => s2sTesting.clientTestBidders.add(bidder)) + } + if (isTestingServerOnly(s2sConfig) && adUnitsContainServerRequests(adUnits, s2sConfig)) { + logWarn('testServerOnly: True. All client requests will be suppressed.'); + serverOnly = true; + } + }); + + next.bail(getBidderCodes(adUnits).reduce((memo, bidder) => { + if (serverBidders.has(bidder)) { + memo[SERVER].push(bidder); + } + if (!serverOnly && (!serverBidders.has(bidder) || s2sTesting.clientTestBidders.has(bidder))) { + memo[CLIENT].push(bidder); + } + return memo; + }, {[CLIENT]: [], [SERVER]: []})); +}); + +filterBidsForAdUnit.before(function(next, bids, s2sConfig) { + if (s2sConfig == null) { + next.bail(bids.filter((bid) => !s2sTesting.clientTestBidders.size || bid.finalSource !== SERVER)); + } else { + const serverBidders = getS2SBidderSet(s2sConfig); + next.bail(bids.filter((bid) => serverBidders.has(bid.bidder) && + (!doingS2STesting(s2sConfig) || bid.finalSource !== CLIENT))); + } +}); export default s2sTesting; diff --git a/modules/saambaaBidAdapter.js b/modules/saambaaBidAdapter.js index 2810853532d..36ab50bfddd 100644 --- a/modules/saambaaBidAdapter.js +++ b/modules/saambaaBidAdapter.js @@ -1,420 +1,419 @@ -import { deepAccess, isFn, generateUUID, parseUrl, isEmpty, parseSizesInput } from '../src/utils.js'; -import { config } from '../src/config.js'; -import { registerBidder } from '../src/adapters/bidderFactory.js'; -import { VIDEO, BANNER } from '../src/mediaTypes.js'; -import find from 'core-js-pure/features/array/find.js'; -import includes from 'core-js-pure/features/array/includes.js'; - -const ADAPTER_VERSION = '1.0'; -const BIDDER_CODE = 'saambaa'; - -export const VIDEO_ENDPOINT = 'https://nep.advangelists.com/xp/get?pubid='; -export const BANNER_ENDPOINT = 'https://nep.advangelists.com/xp/get?pubid='; -export const OUTSTREAM_SRC = 'https://player-cdn.beachfrontmedia.com/playerapi/loader/outstream.js'; -export const VIDEO_TARGETING = ['mimes', 'playbackmethod', 'maxduration', 'skip', 'playerSize', 'context']; -export const DEFAULT_MIMES = ['video/mp4', 'application/javascript']; - -let pubid = ''; - -export const spec = { - code: BIDDER_CODE, - supportedMediaTypes: [BANNER, VIDEO], - - isBidRequestValid(bidRequest) { - if (typeof bidRequest != 'undefined') { - if (bidRequest.bidder !== BIDDER_CODE && typeof bidRequest.params === 'undefined') { return false; } - if (bidRequest === '' || bidRequest.params.placement === '' || bidRequest.params.pubid === '') { return false; } - return true; - } else { return false; } - }, - - buildRequests(bids, bidderRequest) { - let requests = []; - let videoBids = bids.filter(bid => isVideoBidValid(bid)); - let bannerBids = bids.filter(bid => isBannerBidValid(bid)); - videoBids.forEach(bid => { - pubid = getVideoBidParam(bid, 'pubid'); - requests.push({ - method: 'POST', - url: VIDEO_ENDPOINT + pubid, - data: createVideoRequestData(bid, bidderRequest), - bidRequest: bid - }); - }); - - bannerBids.forEach(bid => { - pubid = getBannerBidParam(bid, 'pubid'); - - requests.push({ - method: 'POST', - url: BANNER_ENDPOINT + pubid, - data: createBannerRequestData(bid, bidderRequest), - bidRequest: bid - }); - }); - return requests; - }, - - interpretResponse(serverResponse, {bidRequest}) { - let response = serverResponse.body; - if (response !== null && isEmpty(response) == false) { - if (isVideoBid(bidRequest)) { - let bidResponse = { - requestId: response.id, - bidderCode: BIDDER_CODE, - cpm: response.seatbid[0].bid[0].price, - width: response.seatbid[0].bid[0].w, - height: response.seatbid[0].bid[0].h, - ttl: response.seatbid[0].bid[0].ttl || 60, - creativeId: response.seatbid[0].bid[0].crid, - currency: response.cur, - meta: { 'advertiserDomains': response.seatbid[0].bid[0].adomain }, - mediaType: VIDEO, - netRevenue: true - } - - if (response.seatbid[0].bid[0].adm) { - bidResponse.vastXml = response.seatbid[0].bid[0].adm; - bidResponse.adResponse = { - content: response.seatbid[0].bid[0].adm - }; - } else { - bidResponse.vastUrl = response.seatbid[0].bid[0].nurl; - } - - return bidResponse; - } else { - return { - requestId: response.id, - bidderCode: BIDDER_CODE, - cpm: response.seatbid[0].bid[0].price, - width: response.seatbid[0].bid[0].w, - height: response.seatbid[0].bid[0].h, - ad: response.seatbid[0].bid[0].adm, - ttl: response.seatbid[0].bid[0].ttl || 60, - creativeId: response.seatbid[0].bid[0].crid, - currency: response.cur, - meta: { 'advertiserDomains': response.seatbid[0].bid[0].adomain }, - mediaType: BANNER, - netRevenue: true - } - } - } - } -}; - -function isBannerBid(bid) { - return deepAccess(bid, 'mediaTypes.banner') || !isVideoBid(bid); -} - -function isVideoBid(bid) { - return deepAccess(bid, 'mediaTypes.video'); -} - -function getBannerBidFloor(bid) { - let floorInfo = isFn(bid.getFloor) ? bid.getFloor({ currency: 'USD', mediaType: 'banner', size: '*' }) : {}; - return floorInfo.floor || getBannerBidParam(bid, 'bidfloor'); -} - -function getVideoBidFloor(bid) { - let floorInfo = isFn(bid.getFloor) ? bid.getFloor({ currency: 'USD', mediaType: 'video', size: '*' }) : {}; - return floorInfo.floor || getVideoBidParam(bid, 'bidfloor'); -} - -function isVideoBidValid(bid) { - return isVideoBid(bid) && getVideoBidParam(bid, 'pubid') && getVideoBidParam(bid, 'placement'); -} - -function isBannerBidValid(bid) { - return isBannerBid(bid) && getBannerBidParam(bid, 'pubid') && getBannerBidParam(bid, 'placement'); -} - -function getVideoBidParam(bid, key) { - return deepAccess(bid, 'params.video.' + key) || deepAccess(bid, 'params.' + key); -} - -function getBannerBidParam(bid, key) { - return deepAccess(bid, 'params.banner.' + key) || deepAccess(bid, 'params.' + key); -} - -function isMobile() { - return (/(ios|ipod|ipad|iphone|android)/i).test(navigator.userAgent); -} - -function isConnectedTV() { - return (/(smart[-]?tv|hbbtv|appletv|googletv|hdmi|netcast\.tv|viera|nettv|roku|\bdtv\b|sonydtv|inettvbrowser|\btv\b)/i).test(navigator.userAgent); -} - -function getDoNotTrack() { - return navigator.doNotTrack === '1' || window.doNotTrack === '1' || navigator.msDoNoTrack === '1' || navigator.doNotTrack === 'yes'; -} - -function findAndFillParam(o, key, value) { - try { - if (typeof value === 'function') { - o[key] = value(); - } else { - o[key] = value; - } - } catch (ex) {} -} - -function getOsVersion() { - let clientStrings = [ - { s: 'Android', r: /Android/ }, - { s: 'iOS', r: /(iPhone|iPad|iPod)/ }, - { s: 'Mac OS X', r: /Mac OS X/ }, - { s: 'Mac OS', r: /(MacPPC|MacIntel|Mac_PowerPC|Macintosh)/ }, - { s: 'Linux', r: /(Linux|X11)/ }, - { s: 'Windows 10', r: /(Windows 10.0|Windows NT 10.0)/ }, - { s: 'Windows 8.1', r: /(Windows 8.1|Windows NT 6.3)/ }, - { s: 'Windows 8', r: /(Windows 8|Windows NT 6.2)/ }, - { s: 'Windows 7', r: /(Windows 7|Windows NT 6.1)/ }, - { s: 'Windows Vista', r: /Windows NT 6.0/ }, - { s: 'Windows Server 2003', r: /Windows NT 5.2/ }, - { s: 'Windows XP', r: /(Windows NT 5.1|Windows XP)/ }, - { s: 'UNIX', r: /UNIX/ }, - { s: 'Search Bot', r: /(nuhk|Googlebot|Yammybot|Openbot|Slurp|MSNBot|Ask Jeeves\/Teoma|ia_archiver)/ } - ]; - let cs = find(clientStrings, cs => cs.r.test(navigator.userAgent)); - return cs ? cs.s : 'unknown'; -} - -function getFirstSize(sizes) { - return (sizes && sizes.length) ? sizes[0] : { w: undefined, h: undefined }; -} - -function parseSizes(sizes) { - return parseSizesInput(sizes).map(size => { - let [ width, height ] = size.split('x'); - return { - w: parseInt(width, 10) || undefined, - h: parseInt(height, 10) || undefined - }; - }); -} - -function getVideoSizes(bid) { - return parseSizes(deepAccess(bid, 'mediaTypes.video.playerSize') || bid.sizes); -} - -function getBannerSizes(bid) { - return parseSizes(deepAccess(bid, 'mediaTypes.banner.sizes') || bid.sizes); -} - -function getTopWindowReferrer() { - try { - return window.top.document.referrer; - } catch (e) { - return ''; - } -} - -function getVideoTargetingParams(bid) { - const result = {}; - const excludeProps = ['playerSize', 'context', 'w', 'h']; - Object.keys(Object(bid.mediaTypes.video)) - .filter(key => !includes(excludeProps, key)) - .forEach(key => { - result[ key ] = bid.mediaTypes.video[ key ]; - }); - Object.keys(Object(bid.params.video)) - .filter(key => includes(VIDEO_TARGETING, key)) - .forEach(key => { - result[ key ] = bid.params.video[ key ]; - }); - return result; -} - -function createVideoRequestData(bid, bidderRequest) { - let topLocation = getTopWindowLocation(bidderRequest); - let topReferrer = getTopWindowReferrer(); - - // if size is explicitly given via adapter params - let paramSize = getVideoBidParam(bid, 'size'); - let sizes = []; - let coppa = config.getConfig('coppa'); - - if (typeof paramSize !== 'undefined' && paramSize != '') { - sizes = parseSizes(paramSize); - } else { - sizes = getVideoSizes(bid); - } - const firstSize = getFirstSize(sizes); - let floor = (getVideoBidFloor(bid) == null || typeof getVideoBidFloor(bid) == 'undefined') ? 0.5 : getVideoBidFloor(bid); - let video = getVideoTargetingParams(bid); - const o = { - 'device': { - 'langauge': (global.navigator.language).split('-')[0], - 'dnt': (global.navigator.doNotTrack === 1 ? 1 : 0), - 'devicetype': isMobile() ? 4 : isConnectedTV() ? 3 : 2, - 'js': 1, - 'os': getOsVersion() - }, - 'at': 2, - 'site': {}, - 'tmax': 3000, - 'cur': ['USD'], - 'id': bid.bidId, - 'imp': [], - 'regs': { - 'ext': { - } - }, - 'user': { - 'ext': { - } - } - }; - - o.site['page'] = topLocation.href; - o.site['domain'] = topLocation.hostname; - o.site['search'] = topLocation.search; - o.site['domain'] = topLocation.hostname; - o.site['ref'] = topReferrer; - o.site['mobile'] = isMobile() ? 1 : 0; - const secure = topLocation.protocol.indexOf('https') === 0 ? 1 : 0; - - o.device['dnt'] = getDoNotTrack() ? 1 : 0; - - findAndFillParam(o.site, 'name', function() { - return global.top.document.title; - }); - - findAndFillParam(o.device, 'h', function() { - return global.screen.height; - }); - findAndFillParam(o.device, 'w', function() { - return global.screen.width; - }); - - let placement = getVideoBidParam(bid, 'placement'); - - for (let j = 0; j < sizes.length; j++) { - o.imp.push({ - 'id': '' + j, - 'displaymanager': '' + BIDDER_CODE, - 'displaymanagerver': '' + ADAPTER_VERSION, - 'tagId': placement, - 'bidfloor': floor, - 'bidfloorcur': 'USD', - 'secure': secure, - 'video': Object.assign({ - 'id': generateUUID(), - 'pos': 0, - 'w': firstSize.w, - 'h': firstSize.h, - 'mimes': DEFAULT_MIMES - }, video) - - }); - } - if (coppa) { - o.regs.ext = {'coppa': 1}; - } - if (bidderRequest && bidderRequest.gdprConsent) { - let { gdprApplies, consentString } = bidderRequest.gdprConsent; - o.regs.ext = {'gdpr': gdprApplies ? 1 : 0}; - o.user.ext = {'consent': consentString}; - } - - return o; -} - -function getTopWindowLocation(bidderRequest) { - let url = bidderRequest && bidderRequest.refererInfo && bidderRequest.refererInfo.referer; - return parseUrl(config.getConfig('pageUrl') || url, { decodeSearchAsString: true }); -} - -function createBannerRequestData(bid, bidderRequest) { - let topLocation = getTopWindowLocation(bidderRequest); - let topReferrer = getTopWindowReferrer(); - - // if size is explicitly given via adapter params - - let paramSize = getBannerBidParam(bid, 'size'); - let sizes = []; - let coppa = config.getConfig('coppa'); - if (typeof paramSize !== 'undefined' && paramSize != '') { - sizes = parseSizes(paramSize); - } else { - sizes = getBannerSizes(bid); - } - - let floor = (getBannerBidFloor(bid) == null || typeof getBannerBidFloor(bid) == 'undefined') ? 0.1 : getBannerBidFloor(bid); - const o = { - 'device': { - 'langauge': (global.navigator.language).split('-')[0], - 'dnt': (global.navigator.doNotTrack === 1 ? 1 : 0), - 'devicetype': isMobile() ? 4 : isConnectedTV() ? 3 : 2, - 'js': 1 - }, - 'at': 2, - 'site': {}, - 'tmax': 3000, - 'cur': ['USD'], - 'id': bid.bidId, - 'imp': [], - 'regs': { - 'ext': { - } - }, - 'user': { - 'ext': { - } - } - }; - - o.site['page'] = topLocation.href; - o.site['domain'] = topLocation.hostname; - o.site['search'] = topLocation.search; - o.site['domain'] = topLocation.hostname; - o.site['ref'] = topReferrer; - o.site['mobile'] = isMobile() ? 1 : 0; - const secure = topLocation.protocol.indexOf('https') === 0 ? 1 : 0; - - o.device['dnt'] = getDoNotTrack() ? 1 : 0; - - findAndFillParam(o.site, 'name', function() { - return global.top.document.title; - }); - - findAndFillParam(o.device, 'h', function() { - return global.screen.height; - }); - findAndFillParam(o.device, 'w', function() { - return global.screen.width; - }); - - let placement = getBannerBidParam(bid, 'placement'); - for (let j = 0; j < sizes.length; j++) { - let size = sizes[j]; - - o.imp.push({ - 'id': '' + j, - 'displaymanager': '' + BIDDER_CODE, - 'displaymanagerver': '' + ADAPTER_VERSION, - 'tagId': placement, - 'bidfloor': floor, - 'bidfloorcur': 'USD', - 'secure': secure, - 'banner': { - 'id': generateUUID(), - 'pos': 0, - 'w': size['w'], - 'h': size['h'] - } - }); - } - if (coppa) { - o.regs.ext = {'coppa': 1}; - } - if (bidderRequest && bidderRequest.gdprConsent) { - let { gdprApplies, consentString } = bidderRequest.gdprConsent; - o.regs.ext = {'gdpr': gdprApplies ? 1 : 0}; - o.user.ext = {'consent': consentString}; - } - - return o; -} -registerBidder(spec); +import {deepAccess, generateUUID, isEmpty, isFn, parseSizesInput, parseUrl} from '../src/utils.js'; +import {config} from '../src/config.js'; +import {registerBidder} from '../src/adapters/bidderFactory.js'; +import {BANNER, VIDEO} from '../src/mediaTypes.js'; +import {find, includes} from '../src/polyfill.js'; + +const ADAPTER_VERSION = '1.0'; +const BIDDER_CODE = 'saambaa'; + +export const VIDEO_ENDPOINT = 'https://nep.advangelists.com/xp/get?pubid='; +export const BANNER_ENDPOINT = 'https://nep.advangelists.com/xp/get?pubid='; +export const OUTSTREAM_SRC = 'https://player-cdn.beachfrontmedia.com/playerapi/loader/outstream.js'; +export const VIDEO_TARGETING = ['mimes', 'playbackmethod', 'maxduration', 'skip', 'playerSize', 'context']; +export const DEFAULT_MIMES = ['video/mp4', 'application/javascript']; + +let pubid = ''; + +export const spec = { + code: BIDDER_CODE, + supportedMediaTypes: [BANNER, VIDEO], + + isBidRequestValid(bidRequest) { + if (typeof bidRequest != 'undefined') { + if (bidRequest.bidder !== BIDDER_CODE && typeof bidRequest.params === 'undefined') { return false; } + if (bidRequest === '' || bidRequest.params.placement === '' || bidRequest.params.pubid === '') { return false; } + return true; + } else { return false; } + }, + + buildRequests(bids, bidderRequest) { + let requests = []; + let videoBids = bids.filter(bid => isVideoBidValid(bid)); + let bannerBids = bids.filter(bid => isBannerBidValid(bid)); + videoBids.forEach(bid => { + pubid = getVideoBidParam(bid, 'pubid'); + requests.push({ + method: 'POST', + url: VIDEO_ENDPOINT + pubid, + data: createVideoRequestData(bid, bidderRequest), + bidRequest: bid + }); + }); + + bannerBids.forEach(bid => { + pubid = getBannerBidParam(bid, 'pubid'); + + requests.push({ + method: 'POST', + url: BANNER_ENDPOINT + pubid, + data: createBannerRequestData(bid, bidderRequest), + bidRequest: bid + }); + }); + return requests; + }, + + interpretResponse(serverResponse, {bidRequest}) { + let response = serverResponse.body; + if (response !== null && isEmpty(response) == false) { + if (isVideoBid(bidRequest)) { + let bidResponse = { + requestId: response.id, + bidderCode: BIDDER_CODE, + cpm: response.seatbid[0].bid[0].price, + width: response.seatbid[0].bid[0].w, + height: response.seatbid[0].bid[0].h, + ttl: response.seatbid[0].bid[0].ttl || 60, + creativeId: response.seatbid[0].bid[0].crid, + currency: response.cur, + meta: { 'advertiserDomains': response.seatbid[0].bid[0].adomain }, + mediaType: VIDEO, + netRevenue: true + } + + if (response.seatbid[0].bid[0].adm) { + bidResponse.vastXml = response.seatbid[0].bid[0].adm; + bidResponse.adResponse = { + content: response.seatbid[0].bid[0].adm + }; + } else { + bidResponse.vastUrl = response.seatbid[0].bid[0].nurl; + } + + return bidResponse; + } else { + return { + requestId: response.id, + bidderCode: BIDDER_CODE, + cpm: response.seatbid[0].bid[0].price, + width: response.seatbid[0].bid[0].w, + height: response.seatbid[0].bid[0].h, + ad: response.seatbid[0].bid[0].adm, + ttl: response.seatbid[0].bid[0].ttl || 60, + creativeId: response.seatbid[0].bid[0].crid, + currency: response.cur, + meta: { 'advertiserDomains': response.seatbid[0].bid[0].adomain }, + mediaType: BANNER, + netRevenue: true + } + } + } + } +}; + +function isBannerBid(bid) { + return deepAccess(bid, 'mediaTypes.banner') || !isVideoBid(bid); +} + +function isVideoBid(bid) { + return deepAccess(bid, 'mediaTypes.video'); +} + +function getBannerBidFloor(bid) { + let floorInfo = isFn(bid.getFloor) ? bid.getFloor({ currency: 'USD', mediaType: 'banner', size: '*' }) : {}; + return floorInfo.floor || getBannerBidParam(bid, 'bidfloor'); +} + +function getVideoBidFloor(bid) { + let floorInfo = isFn(bid.getFloor) ? bid.getFloor({ currency: 'USD', mediaType: 'video', size: '*' }) : {}; + return floorInfo.floor || getVideoBidParam(bid, 'bidfloor'); +} + +function isVideoBidValid(bid) { + return isVideoBid(bid) && getVideoBidParam(bid, 'pubid') && getVideoBidParam(bid, 'placement'); +} + +function isBannerBidValid(bid) { + return isBannerBid(bid) && getBannerBidParam(bid, 'pubid') && getBannerBidParam(bid, 'placement'); +} + +function getVideoBidParam(bid, key) { + return deepAccess(bid, 'params.video.' + key) || deepAccess(bid, 'params.' + key); +} + +function getBannerBidParam(bid, key) { + return deepAccess(bid, 'params.banner.' + key) || deepAccess(bid, 'params.' + key); +} + +function isMobile() { + return (/(ios|ipod|ipad|iphone|android)/i).test(navigator.userAgent); +} + +function isConnectedTV() { + return (/(smart[-]?tv|hbbtv|appletv|googletv|hdmi|netcast\.tv|viera|nettv|roku|\bdtv\b|sonydtv|inettvbrowser|\btv\b)/i).test(navigator.userAgent); +} + +function getDoNotTrack() { + return navigator.doNotTrack === '1' || window.doNotTrack === '1' || navigator.msDoNoTrack === '1' || navigator.doNotTrack === 'yes'; +} + +function findAndFillParam(o, key, value) { + try { + if (typeof value === 'function') { + o[key] = value(); + } else { + o[key] = value; + } + } catch (ex) {} +} + +function getOsVersion() { + let clientStrings = [ + { s: 'Android', r: /Android/ }, + { s: 'iOS', r: /(iPhone|iPad|iPod)/ }, + { s: 'Mac OS X', r: /Mac OS X/ }, + { s: 'Mac OS', r: /(MacPPC|MacIntel|Mac_PowerPC|Macintosh)/ }, + { s: 'Linux', r: /(Linux|X11)/ }, + { s: 'Windows 10', r: /(Windows 10.0|Windows NT 10.0)/ }, + { s: 'Windows 8.1', r: /(Windows 8.1|Windows NT 6.3)/ }, + { s: 'Windows 8', r: /(Windows 8|Windows NT 6.2)/ }, + { s: 'Windows 7', r: /(Windows 7|Windows NT 6.1)/ }, + { s: 'Windows Vista', r: /Windows NT 6.0/ }, + { s: 'Windows Server 2003', r: /Windows NT 5.2/ }, + { s: 'Windows XP', r: /(Windows NT 5.1|Windows XP)/ }, + { s: 'UNIX', r: /UNIX/ }, + { s: 'Search Bot', r: /(nuhk|Googlebot|Yammybot|Openbot|Slurp|MSNBot|Ask Jeeves\/Teoma|ia_archiver)/ } + ]; + let cs = find(clientStrings, cs => cs.r.test(navigator.userAgent)); + return cs ? cs.s : 'unknown'; +} + +function getFirstSize(sizes) { + return (sizes && sizes.length) ? sizes[0] : { w: undefined, h: undefined }; +} + +function parseSizes(sizes) { + return parseSizesInput(sizes).map(size => { + let [ width, height ] = size.split('x'); + return { + w: parseInt(width, 10) || undefined, + h: parseInt(height, 10) || undefined + }; + }); +} + +function getVideoSizes(bid) { + return parseSizes(deepAccess(bid, 'mediaTypes.video.playerSize') || bid.sizes); +} + +function getBannerSizes(bid) { + return parseSizes(deepAccess(bid, 'mediaTypes.banner.sizes') || bid.sizes); +} + +function getTopWindowReferrer() { + try { + return window.top.document.referrer; + } catch (e) { + return ''; + } +} + +function getVideoTargetingParams(bid) { + const result = {}; + const excludeProps = ['playerSize', 'context', 'w', 'h']; + Object.keys(Object(bid.mediaTypes.video)) + .filter(key => !includes(excludeProps, key)) + .forEach(key => { + result[ key ] = bid.mediaTypes.video[ key ]; + }); + Object.keys(Object(bid.params.video)) + .filter(key => includes(VIDEO_TARGETING, key)) + .forEach(key => { + result[ key ] = bid.params.video[ key ]; + }); + return result; +} + +function createVideoRequestData(bid, bidderRequest) { + let topLocation = getTopWindowLocation(bidderRequest); + let topReferrer = getTopWindowReferrer(); + + // if size is explicitly given via adapter params + let paramSize = getVideoBidParam(bid, 'size'); + let sizes = []; + let coppa = config.getConfig('coppa'); + + if (typeof paramSize !== 'undefined' && paramSize != '') { + sizes = parseSizes(paramSize); + } else { + sizes = getVideoSizes(bid); + } + const firstSize = getFirstSize(sizes); + let floor = (getVideoBidFloor(bid) == null || typeof getVideoBidFloor(bid) == 'undefined') ? 0.5 : getVideoBidFloor(bid); + let video = getVideoTargetingParams(bid); + const o = { + 'device': { + 'langauge': (global.navigator.language).split('-')[0], + 'dnt': (global.navigator.doNotTrack === 1 ? 1 : 0), + 'devicetype': isMobile() ? 4 : isConnectedTV() ? 3 : 2, + 'js': 1, + 'os': getOsVersion() + }, + 'at': 2, + 'site': {}, + 'tmax': 3000, + 'cur': ['USD'], + 'id': bid.bidId, + 'imp': [], + 'regs': { + 'ext': { + } + }, + 'user': { + 'ext': { + } + } + }; + + o.site['page'] = topLocation.href; + o.site['domain'] = topLocation.hostname; + o.site['search'] = topLocation.search; + o.site['domain'] = topLocation.hostname; + o.site['ref'] = topReferrer; + o.site['mobile'] = isMobile() ? 1 : 0; + const secure = topLocation.protocol.indexOf('https') === 0 ? 1 : 0; + + o.device['dnt'] = getDoNotTrack() ? 1 : 0; + + findAndFillParam(o.site, 'name', function() { + return global.top.document.title; + }); + + findAndFillParam(o.device, 'h', function() { + return global.screen.height; + }); + findAndFillParam(o.device, 'w', function() { + return global.screen.width; + }); + + let placement = getVideoBidParam(bid, 'placement'); + + for (let j = 0; j < sizes.length; j++) { + o.imp.push({ + 'id': '' + j, + 'displaymanager': '' + BIDDER_CODE, + 'displaymanagerver': '' + ADAPTER_VERSION, + 'tagId': placement, + 'bidfloor': floor, + 'bidfloorcur': 'USD', + 'secure': secure, + 'video': Object.assign({ + 'id': generateUUID(), + 'pos': 0, + 'w': firstSize.w, + 'h': firstSize.h, + 'mimes': DEFAULT_MIMES + }, video) + + }); + } + if (coppa) { + o.regs.ext = {'coppa': 1}; + } + if (bidderRequest && bidderRequest.gdprConsent) { + let { gdprApplies, consentString } = bidderRequest.gdprConsent; + o.regs.ext = {'gdpr': gdprApplies ? 1 : 0}; + o.user.ext = {'consent': consentString}; + } + + return o; +} + +function getTopWindowLocation(bidderRequest) { + let url = bidderRequest && bidderRequest.refererInfo && bidderRequest.refererInfo.referer; + return parseUrl(config.getConfig('pageUrl') || url, { decodeSearchAsString: true }); +} + +function createBannerRequestData(bid, bidderRequest) { + let topLocation = getTopWindowLocation(bidderRequest); + let topReferrer = getTopWindowReferrer(); + + // if size is explicitly given via adapter params + + let paramSize = getBannerBidParam(bid, 'size'); + let sizes = []; + let coppa = config.getConfig('coppa'); + if (typeof paramSize !== 'undefined' && paramSize != '') { + sizes = parseSizes(paramSize); + } else { + sizes = getBannerSizes(bid); + } + + let floor = (getBannerBidFloor(bid) == null || typeof getBannerBidFloor(bid) == 'undefined') ? 0.1 : getBannerBidFloor(bid); + const o = { + 'device': { + 'langauge': (global.navigator.language).split('-')[0], + 'dnt': (global.navigator.doNotTrack === 1 ? 1 : 0), + 'devicetype': isMobile() ? 4 : isConnectedTV() ? 3 : 2, + 'js': 1 + }, + 'at': 2, + 'site': {}, + 'tmax': 3000, + 'cur': ['USD'], + 'id': bid.bidId, + 'imp': [], + 'regs': { + 'ext': { + } + }, + 'user': { + 'ext': { + } + } + }; + + o.site['page'] = topLocation.href; + o.site['domain'] = topLocation.hostname; + o.site['search'] = topLocation.search; + o.site['domain'] = topLocation.hostname; + o.site['ref'] = topReferrer; + o.site['mobile'] = isMobile() ? 1 : 0; + const secure = topLocation.protocol.indexOf('https') === 0 ? 1 : 0; + + o.device['dnt'] = getDoNotTrack() ? 1 : 0; + + findAndFillParam(o.site, 'name', function() { + return global.top.document.title; + }); + + findAndFillParam(o.device, 'h', function() { + return global.screen.height; + }); + findAndFillParam(o.device, 'w', function() { + return global.screen.width; + }); + + let placement = getBannerBidParam(bid, 'placement'); + for (let j = 0; j < sizes.length; j++) { + let size = sizes[j]; + + o.imp.push({ + 'id': '' + j, + 'displaymanager': '' + BIDDER_CODE, + 'displaymanagerver': '' + ADAPTER_VERSION, + 'tagId': placement, + 'bidfloor': floor, + 'bidfloorcur': 'USD', + 'secure': secure, + 'banner': { + 'id': generateUUID(), + 'pos': 0, + 'w': size['w'], + 'h': size['h'] + } + }); + } + if (coppa) { + o.regs.ext = {'coppa': 1}; + } + if (bidderRequest && bidderRequest.gdprConsent) { + let { gdprApplies, consentString } = bidderRequest.gdprConsent; + o.regs.ext = {'gdpr': gdprApplies ? 1 : 0}; + o.user.ext = {'consent': consentString}; + } + + return o; +} +registerBidder(spec); diff --git a/modules/seedingAllianceBidAdapter.js b/modules/seedingAllianceBidAdapter.js index 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..182ff384fef 100644 --- a/modules/sirdataRtdProvider.js +++ b/modules/sirdataRtdProvider.js @@ -7,12 +7,12 @@ * @requires module:modules/realTimeData */ import {getGlobal} from '../src/prebidGlobal.js'; -import { deepAccess, logError, deepEqual, deepSetValue, isEmpty, mergeDeep } from '../src/utils.js'; +import {deepAccess, deepEqual, deepSetValue, isEmpty, logError, mergeDeep} from '../src/utils.js'; import {submodule} from '../src/hook.js'; import {ajax} from '../src/ajax.js'; -import findIndex from 'core-js-pure/features/array/find-index.js'; -import { getRefererInfo } from '../src/refererDetection.js'; -import { config } from '../src/config.js'; +import {findIndex} from '../src/polyfill.js'; +import {getRefererInfo} from '../src/refererDetection.js'; +import {config} from '../src/config.js'; /** @type {string} */ const MODULE_NAME = 'realTimeData'; diff --git a/modules/sizeMappingV2.js b/modules/sizeMappingV2.js index 95f0eea4075..405799813eb 100644 --- a/modules/sizeMappingV2.js +++ b/modules/sizeMappingV2.js @@ -4,12 +4,19 @@ * rendering. Read full API documentation on Prebid.org, http://prebid.org/dev-docs/modules/sizeMappingV2.html */ -import { isArray, logError, isArrayOfNums, deepClone, logWarn, getWindowTop, deepEqual, logInfo, isValidMediaTypes, deepAccess, getDefinedParams, getUniqueIdentifierStr, flatten } from '../src/utils.js'; -import { processNativeAdUnitParams } from '../src/native.js'; -import { adunitCounter } from '../src/adUnits.js'; -import includes from 'core-js-pure/features/array/includes.js'; -import { getHook } from '../src/hook.js'; -import { adUnitSetupChecks } from '../src/prebid.js'; +import { + deepClone, + getWindowTop, + isArray, + isArrayOfNums, + isValidMediaTypes, + logError, + logInfo, + logWarn +} from '../src/utils.js'; +import {includes} from '../src/polyfill.js'; +import {getHook} from '../src/hook.js'; +import {adUnitSetupChecks} from '../src/prebid.js'; // Allows for stubbing of these functions while writing unit tests. export const internal = { @@ -21,61 +28,33 @@ export const internal = { isLabelActivated }; -/* - 'sizeMappingInternalStore' contains information on, whether a particular auction is using size mapping V2 (the new size mapping spec), - and it also contains additional information on each adUnit, such as, mediaTypes, activeViewport, etc. This information is required by - the 'getBids' function. -*/ - -export const sizeMappingInternalStore = createSizeMappingInternalStore(); - -function createSizeMappingInternalStore() { - const sizeMappingInternalStore = {}; - - return { - initializeStore: function (auctionId, isUsingSizeMappingBool) { - sizeMappingInternalStore[auctionId] = { - usingSizeMappingV2: isUsingSizeMappingBool, - adUnits: [] - }; - }, - getAuctionDetail: function (auctionId) { - return sizeMappingInternalStore[auctionId]; - }, - setAuctionDetail: function (auctionId, adUnitDetail) { - sizeMappingInternalStore[auctionId].adUnits.push(adUnitDetail); - } - } -} +const V2_ADUNITS = new WeakMap(); /* Returns "true" if at least one of the adUnits in the adUnits array is using an Ad Unit and/or Bidder level sizeConfig, otherwise, returns "false." */ export function isUsingNewSizeMapping(adUnits) { - let isUsingSizeMappingBool = false; - adUnits.forEach(adUnit => { + return !!adUnits.find(adUnit => { + if (V2_ADUNITS.has(adUnit)) return V2_ADUNITS.get(adUnit); if (adUnit.mediaTypes) { // checks for the presence of sizeConfig property at the adUnit.mediaTypes object - Object.keys(adUnit.mediaTypes).forEach(mediaType => { + for (let mediaType of Object.keys(adUnit.mediaTypes)) { if (adUnit.mediaTypes[mediaType].sizeConfig) { - if (isUsingSizeMappingBool === false) { - isUsingSizeMappingBool = true; - } + V2_ADUNITS.set(adUnit, true); + return true; } - }); - - // checks for the presence of sizeConfig property at the adUnit.bids[].bidder object - adUnit.bids && isArray(adUnit.bids) && adUnit.bids.forEach(bidder => { - if (bidder.sizeConfig) { - if (isUsingSizeMappingBool === false) { - isUsingSizeMappingBool = true; - } + } + for (let bid of adUnit.bids && isArray(adUnit.bids) ? adUnit.bids : []) { + if (bid.sizeConfig) { + V2_ADUNITS.set(adUnit, true); + return true; } - }); + } + V2_ADUNITS.set(adUnit, false); + return false; } }); - return isUsingSizeMappingBool; } /** @@ -168,19 +147,12 @@ export function checkAdUnitSetupHook(adUnits) { } const validatedAdUnits = []; adUnits.forEach(adUnit => { - const bids = adUnit.bids; + adUnit = adUnitSetupChecks.validateAdUnit(adUnit); + if (adUnit == null) return; + const mediaTypes = adUnit.mediaTypes; let validatedBanner, validatedVideo, validatedNative; - if (!bids || !isArray(bids)) { - logError(`Detected adUnit.code '${adUnit.code}' did not have 'adUnit.bids' defined or 'adUnit.bids' is not an array. Removing adUnit from auction.`); - return; - } - - if (!mediaTypes || Object.keys(mediaTypes).length === 0) { - logError(`Detected adUnit.code '${adUnit.code}' did not have a 'mediaTypes' object defined. This is a required field for the auction, so this adUnit has been removed.`); - return; - } if (mediaTypes.banner) { if (mediaTypes.banner.sizes) { // Ad unit is using 'mediaTypes.banner.sizes' instead of the new property 'sizeConfig'. Apply the old checks! @@ -290,23 +262,11 @@ export function checkBidderSizeConfigFormat(sizeConfig) { return didCheckPass; } -getHook('getBids').before(function (fn, bidderInfo) { - // check if the adUnit is using sizeMappingV2 specs and store the result in _sizeMappingUsageMap. - if (typeof sizeMappingInternalStore.getAuctionDetail(bidderInfo.auctionId) === 'undefined') { - const isUsingSizeMappingBool = isUsingNewSizeMapping(bidderInfo.adUnits); - - // initialize sizeMappingInternalStore for the first time for a particular auction - sizeMappingInternalStore.initializeStore(bidderInfo.auctionId, isUsingSizeMappingBool); - } - if (sizeMappingInternalStore.getAuctionDetail(bidderInfo.auctionId).usingSizeMappingV2) { - // if adUnit is found using sizeMappingV2 specs, run the getBids function which processes the sizeConfig object - // and returns the bids array for a particular bidder. - - const bids = getBids(bidderInfo); - return fn.bail(bids); +getHook('setupAdUnitMediaTypes').before(function (fn, adUnits, labels) { + if (isUsingNewSizeMapping(adUnits)) { + return fn.bail(setupAdUnitMediaTypes(adUnits, labels)); } else { - // if not using sizeMappingV2, default back to the getBids function defined in adapterManager. - return fn.call(this, bidderInfo); + return fn.call(this, adUnits, labels); } }); @@ -424,8 +384,8 @@ export function getFilteredMediaTypes(mediaTypes) { return sizeBucketToSizeMap; }, {}); - return { mediaTypes, sizeBucketToSizeMap, activeViewport, transformedMediaTypes }; -}; + return { sizeBucketToSizeMap, activeViewport, transformedMediaTypes }; +} /** * Evaluates the given sizeConfig object and checks for various properties to determine if the sizeConfig is active or not. For example, @@ -476,126 +436,87 @@ export function getActiveSizeBucket(sizeConfig, activeViewport) { } export function getRelevantMediaTypesForBidder(sizeConfig, activeViewport) { + const mediaTypes = new Set(); if (internal.checkBidderSizeConfigFormat(sizeConfig)) { const activeSizeBucket = internal.getActiveSizeBucket(sizeConfig, activeViewport); - return sizeConfig.filter(config => config.minViewPort === activeSizeBucket)[0]['relevantMediaTypes']; + sizeConfig.filter(config => config.minViewPort === activeSizeBucket)[0]['relevantMediaTypes'].forEach((mt) => mediaTypes.add(mt)); } - return []; + return mediaTypes; } -// sets sizeMappingInternalStore for a given auctionId with relevant adUnit information returned from the call to 'getFilteredMediaTypes' function -// returns adUnit details object. -export function getAdUnitDetail(auctionId, adUnit, labels) { - // fetch all adUnits for an auction from the sizeMappingInternalStore - const adUnitsForAuction = sizeMappingInternalStore.getAuctionDetail(auctionId).adUnits; - - // check if the adUnit exists already in the sizeMappingInterStore (check for equivalence of 'code' && 'mediaTypes' properties) - const adUnitDetail = adUnitsForAuction.filter(adUnitDetail => adUnitDetail.adUnitCode === adUnit.code && deepEqual(adUnitDetail.mediaTypes, adUnit.mediaTypes)); - - if (adUnitDetail.length > 0) { - adUnitDetail[0].cacheHits++; - return adUnitDetail[0]; - } else { - const identicalAdUnit = adUnitsForAuction.filter(adUnitDetail => adUnitDetail.adUnitCode === adUnit.code); - const adUnitInstance = identicalAdUnit.length > 0 && typeof identicalAdUnit[0].instance === 'number' ? identicalAdUnit[identicalAdUnit.length - 1].instance + 1 : 1; - const isLabelActivated = internal.isLabelActivated(adUnit, labels, adUnit.code, adUnitInstance); - const { mediaTypes = adUnit.mediaTypes, sizeBucketToSizeMap, activeViewport, transformedMediaTypes } = isLabelActivated && internal.getFilteredMediaTypes(adUnit.mediaTypes); - - const adUnitDetail = { - adUnitCode: adUnit.code, - mediaTypes, - sizeBucketToSizeMap, - activeViewport, - transformedMediaTypes, - instance: adUnitInstance, - isLabelActivated, - cacheHits: 0 - }; - - // set adUnitDetail in sizeMappingInternalStore against the correct 'auctionId'. - sizeMappingInternalStore.setAuctionDetail(auctionId, adUnitDetail); - isLabelActivated && logInfo(`Size Mapping V2:: Ad Unit: ${adUnit.code}(${adUnitInstance}) => Active size buckets after filtration: `, sizeBucketToSizeMap); - - return adUnitDetail; - } +export function getAdUnitDetail(adUnit, labels, adUnitInstance) { + const isLabelActivated = internal.isLabelActivated(adUnit, labels, adUnit.code, adUnitInstance); + const { sizeBucketToSizeMap, activeViewport, transformedMediaTypes } = isLabelActivated && internal.getFilteredMediaTypes(adUnit.mediaTypes); + isLabelActivated && logInfo(`Size Mapping V2:: Ad Unit: ${adUnit.code}(${adUnitInstance}) => Active size buckets after filtration: `, sizeBucketToSizeMap); + return { + activeViewport, + transformedMediaTypes, + isLabelActivated, + }; } -export function getBids({ bidderCode, auctionId, bidderRequestId, adUnits, labels, src }) { +export function setupAdUnitMediaTypes(adUnits, labels) { + const duplCounter = {}; return adUnits.reduce((result, adUnit) => { + const instance = (() => { + if (!duplCounter.hasOwnProperty(adUnit.code)) { + duplCounter[adUnit.code] = 1; + } + return duplCounter[adUnit.code]++; + })(); if (adUnit.mediaTypes && isValidMediaTypes(adUnit.mediaTypes)) { - const { activeViewport, transformedMediaTypes, instance: adUnitInstance, isLabelActivated, cacheHits } = internal.getAdUnitDetail(auctionId, adUnit, labels); + const { activeViewport, transformedMediaTypes, isLabelActivated } = internal.getAdUnitDetail(adUnit, labels, instance); if (isLabelActivated) { - // check if adUnit has any active media types remaining, if not drop the adUnit from auction, - // else proceed to evaluate the bids object. if (Object.keys(transformedMediaTypes).length === 0) { - cacheHits === 0 && logInfo(`Size Mapping V2:: Ad Unit: ${adUnit.code}(${adUnitInstance}) => Ad unit disabled since there are no active media types after sizeConfig filtration.`); - return result; - } - result - .push(adUnit.bids.filter(bid => bid.bidder === bidderCode) - .reduce((bids, bid) => { - if (internal.isLabelActivated(bid, labels, adUnit.code, adUnitInstance)) { - // handle native params - const nativeParams = adUnit.nativeParams || deepAccess(adUnit, 'mediaTypes.native'); - if (nativeParams) { - bid = Object.assign({}, bid, { - nativeParams: processNativeAdUnitParams(nativeParams) - }); - } - - bid = Object.assign({}, bid, getDefinedParams(adUnit, ['mediaType', 'renderer'])); - - if (bid.sizeConfig) { - const relevantMediaTypes = internal.getRelevantMediaTypesForBidder(bid.sizeConfig, activeViewport); - if (relevantMediaTypes.length === 0) { - logError(`Size Mapping V2:: Ad Unit: ${adUnit.code}(${adUnitInstance}), Bidder: ${bidderCode} => 'sizeConfig' is not configured properly. This bidder won't be eligible for sizeConfig checks and will remail active.`); - bid = Object.assign({}, bid); - } else if (relevantMediaTypes[0] !== 'none') { - const bidderMediaTypes = Object - .keys(transformedMediaTypes) - .filter(mt => relevantMediaTypes.indexOf(mt) > -1) - .reduce((mediaTypes, mediaType) => { - mediaTypes[mediaType] = transformedMediaTypes[mediaType]; - return mediaTypes; - }, {}); - - if (Object.keys(bidderMediaTypes).length > 0) { - bid = Object.assign({}, bid, { mediaTypes: bidderMediaTypes }); - } else { - logInfo(`Size Mapping V2:: Ad Unit: ${adUnit.code}(${adUnitInstance}), Bidder: ${bid.bidder} => 'relevantMediaTypes' does not match with any of the active mediaTypes at the Ad Unit level. This bidder is disabled.`); - return bids; + logInfo(`Size Mapping V2:: Ad Unit: ${adUnit.code}(${instance}) => Ad unit disabled since there are no active media types after sizeConfig filtration.`); + } else { + adUnit.mediaTypes = transformedMediaTypes; + adUnit.bids = adUnit.bids.reduce((bids, bid) => { + if (internal.isLabelActivated(bid, labels, adUnit.code, instance)) { + if (bid.sizeConfig) { + const relevantMediaTypes = internal.getRelevantMediaTypesForBidder(bid.sizeConfig, activeViewport); + if (relevantMediaTypes.size === 0) { + logError(`Size Mapping V2:: Ad Unit: ${adUnit.code}(${instance}), Bidder: ${bid.bidder} => 'sizeConfig' is not configured properly. This bidder won't be eligible for sizeConfig checks and will remain active.`); + bids.push(bid); + } else if (!relevantMediaTypes.has('none')) { + let modified = false; + const bidderMediaTypes = Object.fromEntries( + Object.entries(transformedMediaTypes) + .filter(([key, val]) => { + if (!relevantMediaTypes.has(key)) { + modified = true; + return false; + } + return true; + }) + ); + if (Object.keys(bidderMediaTypes).length > 0) { + if (modified) { + bid.mediaTypes = bidderMediaTypes; } + bids.push(bid); } else { - logInfo(`Size Mapping V2:: Ad Unit: ${adUnit.code}(${adUnitInstance}), Bidder: ${bid.bidder} => 'relevantMediaTypes' is set to 'none' in sizeConfig for current viewport size. This bidder is disabled.`); - return bids; + logInfo(`Size Mapping V2:: Ad Unit: ${adUnit.code}(${instance}), Bidder: ${bid.bidder} => 'relevantMediaTypes' does not match with any of the active mediaTypes at the Ad Unit level. This bidder is disabled.`); } + } else { + logInfo(`Size Mapping V2:: Ad Unit: ${adUnit.code}(${instance}), Bidder: ${bid.bidder} => 'relevantMediaTypes' is set to 'none' in sizeConfig for current viewport size. This bidder is disabled.`); } - bids.push(Object.assign({}, bid, { - adUnitCode: adUnit.code, - transactionId: adUnit.transactionId, - sizes: deepAccess(transformedMediaTypes, 'banner.sizes') || deepAccess(transformedMediaTypes, 'video.playerSize') || [], - mediaTypes: bid.mediaTypes || transformedMediaTypes, - bidId: bid.bid_id || getUniqueIdentifierStr(), - bidderRequestId, - auctionId, - src, - bidRequestsCount: adunitCounter.getRequestsCounter(adUnit.code), - bidderRequestsCount: adunitCounter.getBidderRequestsCounter(adUnit.code, bid.bidder), - bidderWinsCount: adunitCounter.getBidderWinsCounter(adUnit.code, bid.bidder) - })); - return bids; } else { - logInfo(`Size Mapping V2:: Ad Unit: ${adUnit.code}(${adUnitInstance}), Bidder: ${bid.bidder} => Label check for this bidder has failed. This bidder is disabled.`); - return bids; + bids.push(bid); } - }, [])); + } else { + logInfo(`Size Mapping V2:: Ad Unit: ${adUnit.code}(${instance}), Bidder: ${bid.bidder} => Label check for this bidder has failed. This bidder is disabled.`); + } + return bids; + }, []); + result.push(adUnit); + } } else { - cacheHits === 0 && logInfo(`Size Mapping V2:: Ad Unit: ${adUnit.code}(${adUnitInstance}) => Ad unit is disabled due to failing label check.`); + logInfo(`Size Mapping V2:: Ad Unit: ${adUnit.code}(${instance}) => Ad unit is disabled due to failing label check.`); } } else { logWarn(`Size Mapping V2:: Ad Unit: ${adUnit.code} => Ad unit has declared invalid 'mediaTypes' or has not declared a 'mediaTypes' property`); - return result; } return result; - }, []).reduce(flatten, []).filter(val => val !== ''); + }, []) } diff --git a/modules/slimcutBidAdapter.js b/modules/slimcutBidAdapter.js index c2592137fd8..2d35e09d777 100644 --- a/modules/slimcutBidAdapter.js +++ b/modules/slimcutBidAdapter.js @@ -9,7 +9,8 @@ const BIDDER_CODE = 'slimcut'; const ENDPOINT_URL = 'https://sb.freeskreen.com/pbr'; export const spec = { code: BIDDER_CODE, - aliases: ['scm'], + gvlid: 52, + aliases: [{ code: 'scm', gvlid: 52 }], supportedMediaTypes: ['video', 'banner'], /** * Determines whether or not the given bid request is valid. diff --git a/modules/smaatoBidAdapter.js b/modules/smaatoBidAdapter.js index 68334aed0ab..b792983534d 100644 --- a/modules/smaatoBidAdapter.js +++ b/modules/smaatoBidAdapter.js @@ -16,9 +16,6 @@ const buildOpenRtbBidRequest = (bidRequest, bidderRequest) => { tmax: bidderRequest.timeout, site: { id: window.location.hostname, - publisher: { - id: deepAccess(bidRequest, 'params.publisherId') - }, domain: window.location.hostname, page: window.location.href, ref: bidderRequest.refererInfo.referer @@ -51,6 +48,8 @@ const buildOpenRtbBidRequest = (bidRequest, bidderRequest) => { Object.assign(requestTemplate.user, ortb2.user); Object.assign(requestTemplate.site, ortb2.site); + deepSetValue(requestTemplate, 'site.publisher.id', deepAccess(bidRequest, 'params.publisherId')); + if (bidderRequest.gdprConsent && bidderRequest.gdprConsent.gdprApplies === true) { deepSetValue(requestTemplate, 'regs.ext.gdpr', bidderRequest.gdprConsent.gdprApplies ? 1 : 0); deepSetValue(requestTemplate, 'user.ext.consent', bidderRequest.gdprConsent.consentString); @@ -110,6 +109,7 @@ const buildServerRequest = (validBidRequest, data) => { export const spec = { code: BIDDER_CODE, supportedMediaTypes: [BANNER, VIDEO], + gvlid: 82, /** * Determines whether or not the given bid request is valid. diff --git a/modules/smarthubBidAdapter.js b/modules/smarthubBidAdapter.js new file mode 100644 index 00000000000..a94ed972b2e --- /dev/null +++ b/modules/smarthubBidAdapter.js @@ -0,0 +1,187 @@ +import {deepAccess, isFn, logError, logMessage} from '../src/utils.js'; +import {registerBidder} from '../src/adapters/bidderFactory.js'; +import {BANNER, NATIVE, VIDEO} from '../src/mediaTypes.js'; +import {config} from '../src/config.js'; + +const BIDDER_CODE = 'smarthub'; + +function isBidResponseValid(bid) { + if (!bid.requestId || !bid.cpm || !bid.creativeId || !bid.ttl || !bid.currency || !bid.hasOwnProperty('netRevenue')) { + return false; + } + switch (bid.mediaType) { + case BANNER: + return Boolean(bid.width && bid.height && bid.ad); + case VIDEO: + return Boolean(bid.width && bid.height && (bid.vastUrl || bid.vastXml)); + case NATIVE: + return Boolean(bid.native && bid.native.impressionTrackers && bid.native.impressionTrackers.length); + default: + return false; + } +} + +function getPlacementReqData(bid) { + const { params, bidId, mediaTypes } = bid; + const schain = bid.schain || {}; + const { partnerName, seat, token, iabCat, minBidfloor, pos } = params; + const bidfloor = getBidFloor(bid); + + const placement = { + partnerName: partnerName.toLowerCase(), + seat, + token, + iabCat, + minBidfloor, + pos, + bidId, + schain, + bidfloor + }; + + if (mediaTypes && mediaTypes[BANNER]) { + placement.adFormat = BANNER; + placement.sizes = mediaTypes[BANNER].sizes; + } else if (mediaTypes && mediaTypes[VIDEO]) { + placement.adFormat = VIDEO; + placement.playerSize = mediaTypes[VIDEO].playerSize; + placement.minduration = mediaTypes[VIDEO].minduration; + placement.maxduration = mediaTypes[VIDEO].maxduration; + placement.mimes = mediaTypes[VIDEO].mimes; + placement.protocols = mediaTypes[VIDEO].protocols; + placement.startdelay = mediaTypes[VIDEO].startdelay; + placement.placement = mediaTypes[VIDEO].placement; + placement.skip = mediaTypes[VIDEO].skip; + placement.skipafter = mediaTypes[VIDEO].skipafter; + placement.minbitrate = mediaTypes[VIDEO].minbitrate; + placement.maxbitrate = mediaTypes[VIDEO].maxbitrate; + placement.delivery = mediaTypes[VIDEO].delivery; + placement.playbackmethod = mediaTypes[VIDEO].playbackmethod; + placement.api = mediaTypes[VIDEO].api; + placement.linearity = mediaTypes[VIDEO].linearity; + } else if (mediaTypes && mediaTypes[NATIVE]) { + placement.native = mediaTypes[NATIVE]; + placement.adFormat = NATIVE; + } + + return placement; +} + +function getBidFloor(bid) { + if (!isFn(bid.getFloor)) { + return deepAccess(bid, 'params.bidfloor', 0); + } + + try { + const bidFloor = bid.getFloor({ + currency: 'USD', + mediaType: '*', + size: '*', + }); + return bidFloor.floor; + } catch (e) { + logError(e); + return 0; + } +} + +function buildRequestParams(bidderRequest = {}, placements = []) { + let deviceWidth = 0; + let deviceHeight = 0; + + let winLocation; + try { + const winTop = window.top; + deviceWidth = winTop.screen.width; + deviceHeight = winTop.screen.height; + winLocation = winTop.location; + } catch (e) { + logMessage(e); + winLocation = window.location; + } + + const refferUrl = bidderRequest.refererInfo && bidderRequest.refererInfo.referer; + let refferLocation; + try { + refferLocation = refferUrl && new URL(refferUrl); + } catch (e) { + logMessage(e); + } + + let location = refferLocation || winLocation; + const language = (navigator && navigator.language) ? navigator.language.split('-')[0] : ''; + const host = location.host; + const page = location.pathname; + const secure = location.protocol === 'https:' ? 1 : 0; + return { + deviceWidth, + deviceHeight, + language, + secure, + host, + page, + placements, + coppa: config.getConfig('coppa') === true ? 1 : 0, + ccpa: bidderRequest.uspConsent || undefined, + gdpr: bidderRequest.gdprConsent || undefined, + tmax: config.getConfig('bidderTimeout') + }; +} + +export const spec = { + code: BIDDER_CODE, + supportedMediaTypes: [BANNER, VIDEO, NATIVE], + + isBidRequestValid: (bid = {}) => { + const { params, bidId, mediaTypes } = bid; + let valid = Boolean(bidId && params && params.partnerName && params.seat && params.token); + + if (mediaTypes && mediaTypes[BANNER]) { + valid = valid && Boolean(mediaTypes[BANNER] && mediaTypes[BANNER].sizes); + } else if (mediaTypes && mediaTypes[VIDEO]) { + valid = valid && Boolean(mediaTypes[VIDEO] && mediaTypes[VIDEO].playerSize); + } else if (mediaTypes && mediaTypes[NATIVE]) { + valid = valid && Boolean(mediaTypes[NATIVE]); + } else { + valid = false; + } + return valid; + }, + + buildRequests: (validBidRequests = [], bidderRequest = {}) => { + const tempObj = {}; + + const len = validBidRequests.length; + for (let i = 0; i < len; i++) { + const bid = validBidRequests[i]; + const data = getPlacementReqData(bid); + tempObj[data.partnerName] = tempObj[data.partnerName] || []; + tempObj[data.partnerName].push(data); + } + + return Object.keys(tempObj).map(key => { + const request = buildRequestParams(bidderRequest, tempObj[key]); + return { + method: 'POST', + url: `https://${key}-prebid.smart-hub.io/pbjs`, + data: request, + } + }); + }, + + interpretResponse: (serverResponse) => { + let response = []; + for (let i = 0; i < serverResponse.body.length; i++) { + let resItem = serverResponse.body[i]; + if (isBidResponseValid(resItem)) { + const advertiserDomains = resItem.adomain && resItem.adomain.length ? resItem.adomain : []; + resItem.meta = { ...resItem.meta, advertiserDomains }; + + response.push(resItem); + } + } + return response; + } +}; + +registerBidder(spec); diff --git a/modules/smarthubBidAdapter.md b/modules/smarthubBidAdapter.md new file mode 100644 index 00000000000..c09855303e2 --- /dev/null +++ b/modules/smarthubBidAdapter.md @@ -0,0 +1,94 @@ +# Overview + +``` +Module Name: SmartHub Bidder Adapter +Module Type: SmartHub Bidder Adapter +Maintainer: support@smart-hub.io +``` + +# Description + +Connects to SmartHub exchange for bids. + +SmartHub bid adapter supports Banner, Video (instream and outstream) and Native. + +# Test Parameters +``` + var adUnits = [ + // Will return static test banner + { + code: 'adunit1', + mediaTypes: { + banner: { + sizes: [ [300, 250], [320, 50] ], + } + }, + bids: [ + { + bidder: 'smarthub', + params: { + partnerName: 'pbjstest', + seat: 'testSeat', + token: 'testBanner', + iabCat: ['IAB1-1', 'IAB3-1', 'IAB4-3'], + minBidfloor: 10, + pos: 1, + } + } + ] + }, + { + code: 'addunit2', + mediaTypes: { + video: { + playerSize: [ [640, 480] ], + minduration: 5, + maxduration: 60, + } + }, + bids: [ + { + bidder: 'smarthub', + params: { + partnerName: 'pbjstest', + seat: 'testSeat', + token: 'testVideo', + iabCat: ['IAB1-1', 'IAB3-1', 'IAB4-3'], + minBidfloor: 10, + pos: 1, + } + } + ] + }, + { + code: 'addunit3', + mediaTypes: { + native: { + title: { + required: true + }, + body: { + required: true + }, + icon: { + required: true, + size: [64, 64] + } + } + }, + bids: [ + { + bidder: 'smarthub', + params: { + partnerName: 'pbjstest', + seat: 'testSeat', + token: 'testNative', + iabCat: ['IAB1-1', 'IAB3-1', 'IAB4-3'], + minBidfloor: 10, + pos: 1, + } + } + ] + } + ]; +``` diff --git a/modules/smarticoBidAdapter.js b/modules/smarticoBidAdapter.js index 2399a12f932..edb774f812f 100644 --- a/modules/smarticoBidAdapter.js +++ b/modules/smarticoBidAdapter.js @@ -1,6 +1,6 @@ -import { registerBidder } from '../src/adapters/bidderFactory.js'; -import { BANNER } from '../src/mediaTypes.js'; -import find from 'core-js-pure/features/array/find.js'; +import {registerBidder} from '../src/adapters/bidderFactory.js'; +import {BANNER} from '../src/mediaTypes.js'; +import {find} from '../src/polyfill.js'; const SMARTICO_CONFIG = { bidRequestUrl: 'https://trmads.eu/preBidRequest', diff --git a/modules/sortableAnalyticsAdapter.js b/modules/sortableAnalyticsAdapter.js index 76d3ca63d69..4580ce6dbb8 100644 --- a/modules/sortableAnalyticsAdapter.js +++ b/modules/sortableAnalyticsAdapter.js @@ -5,6 +5,7 @@ import adapterManager from '../src/adapterManager.js'; import {ajax} from '../src/ajax.js'; import {getGlobal} from '../src/prebidGlobal.js'; import { config } from '../src/config.js'; +import {bidderSettings} from '../src/bidderSettings.js'; const DEFAULT_PROTOCOL = 'https'; const DEFAULT_HOST = 'pa.deployads.com'; @@ -182,11 +183,11 @@ function getFactor(bidder) { } function getBiddersFactors() { - const pb = getGlobal(); const result = {}; - if (pb && pb.bidderSettings) { - Object.keys(pb.bidderSettings).forEach(bidderKey => { - const bidder = pb.bidderSettings[bidderKey]; + const settings = bidderSettings.getSettings(); + if (settings) { + Object.keys(settings).forEach(bidderKey => { + const bidder = settings[bidderKey]; const factor = getFactor(bidder); if (factor !== null) { result[bidderKey] = factor; diff --git a/modules/sovrnAnalyticsAdapter.js b/modules/sovrnAnalyticsAdapter.js index aee7ddd2690..065cfaa58bc 100644 --- a/modules/sovrnAnalyticsAdapter.js +++ b/modules/sovrnAnalyticsAdapter.js @@ -1,11 +1,10 @@ -import { logError, timestamp } from '../src/utils.js'; -import adapter from '../src/AnalyticsAdapter.js' -import adaptermanager from '../src/adapterManager.js' -import CONSTANTS from '../src/constants.json' -import {ajaxBuilder} from '../src/ajax.js' -import {config} from '../src/config.js' -import find from 'core-js-pure/features/array/find.js' -import includes from 'core-js-pure/features/array/includes.js' +import {logError, timestamp} from '../src/utils.js'; +import adapter from '../src/AnalyticsAdapter.js'; +import adaptermanager from '../src/adapterManager.js'; +import CONSTANTS from '../src/constants.json'; +import {ajaxBuilder} from '../src/ajax.js'; +import {config} from '../src/config.js'; +import {find, includes} from '../src/polyfill.js'; const ajax = ajaxBuilder(0) diff --git a/modules/sovrnBidAdapter.js b/modules/sovrnBidAdapter.js index 38788036ce0..23571373147 100644 --- a/modules/sovrnBidAdapter.js +++ b/modules/sovrnBidAdapter.js @@ -1,12 +1,46 @@ -import { _each, getBidIdParameter, isArray, deepClone, parseUrl, getUniqueIdentifierStr, deepSetValue, logError, deepAccess } from '../src/utils.js'; +import { _each, getBidIdParameter, isArray, deepClone, parseUrl, getUniqueIdentifierStr, deepSetValue, logError, deepAccess, isInteger, logWarn } from '../src/utils.js'; import { registerBidder } from '../src/adapters/bidderFactory.js' -import { BANNER } from '../src/mediaTypes.js' -import { createEidsArray } from './userId/eids.js'; -import {config} from '../src/config.js'; +import { ADPOD, BANNER, VIDEO } from '../src/mediaTypes.js' +import { createEidsArray } from './userId/eids.js' +import {config} from '../src/config.js' + +const ORTB_VIDEO_PARAMS = { + 'mimes': (value) => Array.isArray(value) && value.length > 0 && value.every(v => typeof v === 'string'), + 'minduration': (value) => isInteger(value), + 'maxduration': (value) => isInteger(value), + 'protocols': (value) => Array.isArray(value) && value.every(v => v >= 1 && v <= 10), + 'w': (value) => isInteger(value), + 'h': (value) => isInteger(value), + 'startdelay': (value) => isInteger(value), + 'placement': (value) => isInteger(value) && value >= 1 && value <= 5, + 'linearity': (value) => [1, 2].indexOf(value) !== -1, + 'skip': (value) => [0, 1].indexOf(value) !== -1, + 'skipmin': (value) => isInteger(value), + 'skipafter': (value) => isInteger(value), + 'sequence': (value) => isInteger(value), + 'battr': (value) => Array.isArray(value) && value.every(v => v >= 1 && v <= 17), + 'maxextended': (value) => isInteger(value), + 'minbitrate': (value) => isInteger(value), + 'maxbitrate': (value) => isInteger(value), + 'boxingallowed': (value) => [0, 1].indexOf(value) !== -1, + 'playbackmethod': (value) => Array.isArray(value) && value.every(v => v >= 1 && v <= 6), + 'playbackend': (value) => [1, 2, 3].indexOf(value) !== -1, + 'delivery': (value) => Array.isArray(value) && value.every(v => v >= 1 && v <= 3), + 'pos': (value) => isInteger(value) && value >= 1 && value <= 7, + 'api': (value) => Array.isArray(value) && value.every(v => v >= 1 && v <= 6) +} + +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], + supportedMediaTypes: [BANNER, VIDEO], gvlid: 13, /** @@ -14,8 +48,18 @@ export const spec = { * @param {object} bid the Sovrn bid to validate * @return boolean for whether or not a bid is valid */ - isBidRequestValid: function(bid) { - return !!(bid.params.tagid && !isNaN(parseFloat(bid.params.tagid)) && isFinite(bid.params.tagid)) + isBidRequestValid: function (bid) { + const video = bid?.mediaTypes?.video + return !!( + bid.params.tagid && + !isNaN(parseFloat(bid.params.tagid)) && + isFinite(bid.params.tagid) && ( + !video || ( + Object.keys(REQUIRED_VIDEO_PARAMS) + .every(key => REQUIRED_VIDEO_PARAMS[key](video[key])) + ) + ) + ) }, /** @@ -50,13 +94,9 @@ export const spec = { } iv = iv || getBidIdParameter('iv', bid.params) - let bidSizes = (bid.mediaTypes && bid.mediaTypes.banner && bid.mediaTypes.banner.sizes) || bid.sizes - bidSizes = ((isArray(bidSizes) && isArray(bidSizes[0])) ? bidSizes : [bidSizes]) - bidSizes = bidSizes.filter(size => isArray(size)) - const processedSizes = bidSizes.map(size => ({w: parseInt(size[0], 10), h: parseInt(size[1], 10)})) const floorInfo = (bid.getFloor && typeof bid.getFloor === 'function') ? bid.getFloor({ currency: 'USD', - mediaType: 'banner', + mediaType: bid.mediaTypes && bid.mediaTypes.banner ? 'banner' : 'video', size: '*' }) : {} floorInfo.floor = floorInfo.floor || getBidIdParameter('bidfloor', bid.params) @@ -64,13 +104,24 @@ export const spec = { const imp = { adunitcode: bid.adUnitCode, id: bid.bidId, - banner: { + tagid: String(getBidIdParameter('tagid', bid.params)), + bidfloor: floorInfo.floor + } + + if (deepAccess(bid, 'mediaTypes.banner')) { + let bidSizes = deepAccess(bid, 'mediaTypes.banner.sizes') || bid.sizes + bidSizes = (isArray(bidSizes) && isArray(bidSizes[0])) ? bidSizes : [bidSizes] + bidSizes = bidSizes.filter(size => isArray(size)) + const processedSizes = bidSizes.map(size => ({w: parseInt(size[0], 10), h: parseInt(size[1], 10)})) + + imp.banner = { format: processedSizes, w: 1, h: 1, - }, - tagid: String(getBidIdParameter('tagid', bid.params)), - bidfloor: floorInfo.floor + }; + } + if (deepAccess(bid, 'mediaTypes.video')) { + imp.video = _buildVideoRequestObj(bid); } imp.ext = getBidIdParameter('ext', bid.ortb2Imp) || undefined @@ -149,7 +200,7 @@ export const spec = { seatbid[0].bid && seatbid[0].bid.length > 0) { seatbid[0].bid.map(sovrnBid => { - sovrnBidResponses.push({ + const bid = { requestId: sovrnBid.impid, cpm: parseFloat(sovrnBid.price), width: parseInt(sovrnBid.w), @@ -158,11 +209,18 @@ export const spec = { dealId: sovrnBid.dealid || null, currency: 'USD', netRevenue: true, - mediaType: BANNER, - ad: decodeURIComponent(`${sovrnBid.adm}`), ttl: sovrnBid.ext ? (sovrnBid.ext.ttl || 90) : 90, meta: { advertiserDomains: sovrnBid && sovrnBid.adomain ? sovrnBid.adomain : [] } - }); + } + + if (!sovrnBid.nurl) { + bid.mediaType = VIDEO + bid.vastXml = decodeURIComponent(sovrnBid.adm) + } else { + bid.mediaType = BANNER + bid.ad = decodeURIComponent(`${sovrnBid.adm}`) + } + sovrnBidResponses.push(bid); }); } return sovrnBidResponses @@ -209,4 +267,34 @@ export const spec = { }, } -registerBidder(spec); +function _buildVideoRequestObj(bid) { + const videoObj = {} + const videoAdUnitParams = deepAccess(bid, 'mediaTypes.video', {}) + const videoBidderParams = deepAccess(bid, 'params.video', {}) + const computedParams = {} + + if (Array.isArray(videoAdUnitParams.playerSize)) { + const sizes = (Array.isArray(videoAdUnitParams.playerSize[0])) ? videoAdUnitParams.playerSize[0] : videoAdUnitParams.playerSize + computedParams.w = sizes[0] + computedParams.h = sizes[1] + } + + const videoParams = { + ...computedParams, + ...videoAdUnitParams, + ...videoBidderParams + }; + + Object.keys(ORTB_VIDEO_PARAMS).forEach(paramName => { + if (videoParams.hasOwnProperty(paramName)) { + if (ORTB_VIDEO_PARAMS[paramName](videoParams[paramName])) { + videoObj[paramName] = videoParams[paramName] + } else { + logWarn(`The OpenRTB video param ${paramName} has been skipped due to misformating. Please refer to OpenRTB 2.5 spec.`); + } + } + }) + return videoObj +} + +registerBidder(spec) diff --git a/modules/sovrnBidAdapter.md b/modules/sovrnBidAdapter.md index 2b5d21d5515..53e3158024d 100644 --- a/modules/sovrnBidAdapter.md +++ b/modules/sovrnBidAdapter.md @@ -10,9 +10,9 @@ Maintainer: jrosendahl@sovrn.com Sovrn's adapter integration to the Prebid library. Posts plain-text JSON to the /rtb/bid endpoint. -# Test Parameters +# Banner Test Parameters -``` +```js var adUnits = [ { code: 'test-leaderboard', @@ -45,3 +45,81 @@ var adUnits = [ } ] ``` + +# Video Test Parameters +### Instream +```js +var videoAdUnit = { + code: 'video1', + sizes: [640, 480], + mediaTypes: { + video: { + context: 'instream', + playerSize: [640, 480], + mimes: ['video/mp4'], + protocols: [1, 2, 3, 4, 5, 6, 7, 8], + playbackmethod: [2], + skip: 1, + }, + }, + bids: [ + { + bidder: 'sovrn', + // Prebid Server Bidder Params https://docs.prebid.org/dev-docs/pbs-bidders.html#sovrn + params: { + tagid: '315045', + bidfloor: '0.04', + }, + }, + ], +} +``` +### Outstream +```js +var adUnits = [ + { + code: videoId, + mediaTypes: { + video: { + context: 'outstream', + playerSize: [640, 360], + mimes: ['video/mp4'], + protocols: [1, 2, 3, 4, 5, 6, 7, 8], + playbackmethod: [2], + skip: 1, + }, + }, + bids: [ + { + bidder: 'sovrn', + // Prebid Server Bidder Params https://docs.prebid.org/dev-docs/pbs-bidders.html#sovrn + params: { + tagid: '315045', + bidfloor: '0.04', + }, + }, + ], + renderer: { + url: 'https://acdn.adnxs.com/video/outstream/ANOutstreamVideo.js', + render: function (bid) { + adResponse = { + ad: { + video: { + content: bid.vastXml, + player_height: bid.height, + player_width: bid.width, + }, + }, + } + // push to render queue because ANOutstreamVideo may not be loaded yet. + bid.renderer.push(() => { + ANOutstreamVideo.renderAd({ + targetId: bid.adUnitCode, + adResponse: adResponse, + }) + }) + }, + }, + }, +] +``` diff --git a/modules/spotxBidAdapter.js b/modules/spotxBidAdapter.js index 7d5865684a7..2fd403058d1 100644 --- a/modules/spotxBidAdapter.js +++ b/modules/spotxBidAdapter.js @@ -11,8 +11,7 @@ export const GOOGLE_CONSENT = { consented_providers: ['3', '7', '11', '12', '15' export const spec = { code: BIDDER_CODE, - gvlid: 165, - aliases: ['spotx'], + gvlid: 52, supportedMediaTypes: [VIDEO], /** diff --git a/modules/sspBCBidAdapter.js b/modules/sspBCBidAdapter.js index c8f3c138ce4..e46073a63ba 100644 --- a/modules/sspBCBidAdapter.js +++ b/modules/sspBCBidAdapter.js @@ -1,15 +1,17 @@ -import { isArray, deepAccess, logWarn, parseUrl } from '../src/utils.js'; -import { ajax } from '../src/ajax.js'; -import { registerBidder } from '../src/adapters/bidderFactory.js'; -import { BANNER, NATIVE, VIDEO } from '../src/mediaTypes.js'; -import strIncludes from 'core-js-pure/features/string/includes.js'; +import {deepAccess, isArray, logWarn, parseUrl} from '../src/utils.js'; +import {ajax} from '../src/ajax.js'; +import {registerBidder} from '../src/adapters/bidderFactory.js'; +import {BANNER, NATIVE, VIDEO} from '../src/mediaTypes.js'; +import {includes as strIncludes} from '../src/polyfill.js'; const BIDDER_CODE = 'sspBC'; const BIDDER_URL = 'https://ssp.wp.pl/bidder/'; const SYNC_URL = 'https://ssp.wp.pl/bidder/usersync'; const NOTIFY_URL = 'https://ssp.wp.pl/bidder/notify'; +const TRACKER_URL = 'https://bdr.wpcdn.pl/tag/jstracker.js'; +const GVLID = 676; const TMAX = 450; -const BIDDER_VERSION = '5.3'; +const BIDDER_VERSION = '5.41'; const W = window; const { navigator } = W; const oneCodeDetection = {}; @@ -29,8 +31,8 @@ const getNotificationPayload = bidData => { const result = { requestId: undefined, siteId: [], - adUnit: [], slotId: [], + tagid: [], } bids.forEach(bid => { let params = isArray(bid.params) ? bid.params[0] : bid.params; @@ -55,7 +57,7 @@ const getNotificationPayload = bidData => { result.adomain = meta.advertiserDomains && meta.advertiserDomains[0]; result.networkName = meta.networkName; } - result.adUnit.push(bid.adUnitCode) + result.tagid.push(bid.adUnitCode); result.requestId = bid.auctionId || result.requestId; result.timeout = bid.timeout || result.timeout; }) @@ -123,7 +125,16 @@ const applyClientHints = ortbRequest => { ] }]; - ortbRequest.user = Object.assign(ortbRequest.user, { data }); + const ch = { data }; + ortbRequest.user = { ...ortbRequest.user, ...ch }; +}; + +const applyUserIds = (validBidRequest, ortbRequest) => { + const eids = validBidRequest.userIdAsEids + if (eids && eids.length) { + const ids = { eids }; + ortbRequest.user = { ...ortbRequest.user, ...ids }; + } }; /** @@ -135,8 +146,8 @@ const applyGdpr = (bidderRequest, ortbRequest) => { if (gdprConsent) { const { apiVersion, gdprApplies, consentString } = gdprConsent; consentApiVersion = apiVersion; - ortbRequest.regs = Object.assign(ortbRequest.regs, { '[ortb_extensions.gdpr]': gdprApplies ? 1 : 0 }); - ortbRequest.user = Object.assign(ortbRequest.user, { '[ortb_extensions.consent]': consentString }); + ortbRequest.regs = Object.assign(ortbRequest.regs, { 'gdpr': gdprApplies ? 1 : 0 }); + ortbRequest.user = Object.assign(ortbRequest.user, { 'consent': consentString }); } } @@ -299,7 +310,7 @@ const mapImpression = slot => { ext.data = Object.assign({ pbsize: adUnitsCalled[adUnitCode] }, ext.data); const imp = { - id: id && siteId ? id : 'bidid-' + bidId, + id: id && siteId ? id.padStart(3, '0') : 'bidid-' + bidId, banner: mapBanner(slot), native: mapNative(slot), video: mapVideo(slot), @@ -342,7 +353,7 @@ const isVideoAd = bid => { const isNativeAd = bid => { const xmlTester = new RegExp(/^{['"]native['"]/); - return bid.adm && bid.adm.match(xmlTester); + return bid.admNative || (bid.adm && bid.adm.match(xmlTester)); } const parseNative = nativeData => { @@ -450,12 +461,10 @@ const renderCreative = (site, auctionId, bid, seat, request) => { window.gdpr = ${JSON.stringify(request.gdprConsent)}; window.page = "${site.page}"; window.ref = "${site.ref}"; + window.adlabel = "${site.adLabel ? site.adLabel : ''}"; + window.pubid = "${site.publisherId ? site.publisherId : ''}"; `; - if (gam) { - adcode += `window.gam = ${JSON.stringify(gam)};`; - } - adcode += ` @@ -469,6 +478,7 @@ const renderCreative = (site, auctionId, bid, seat, request) => { const spec = { code: BIDDER_CODE, + gvlid: GVLID, aliases: [], supportedMediaTypes: [BANNER, NATIVE, VIDEO], isBidRequestValid(bid) { @@ -513,6 +523,7 @@ const spec = { applyGdpr(bidderRequest, payload); applyClientHints(payload); + applyUserIds(validBidRequests[0], payload); return { method: 'POST', @@ -558,10 +569,14 @@ const spec = { /* bid response might include ext object containing siteId / slotId, as detected by OneCode update site / slot data in this case + + ext also might contain publisherId and custom ad label */ - const { siteid, slotid } = ext; + const { siteid, slotid, pubid, adlabel } = ext; site.id = siteid || site.id; site.slot = slotid || site.slot; + site.publisherId = pubid; + site.adLabel = adlabel; } if (bidRequest && site.id && !strIncludes(site.id, 'bidid')) { @@ -598,10 +613,28 @@ const spec = { bid.mediaType = 'native'; // check native object try { - const nativeData = JSON.parse(serverBid.adm).native; + const nativeData = serverBid.admNative || JSON.parse(serverBid.adm).native; bid.native = parseNative(nativeData); bid.width = 1; bid.height = 1; + + // append viewability tracker + const jsData = { + rid: bidRequest.auctionId, + crid: bid.creativeId, + adunit: bidRequest.adUnitCode, + url: bid.native.clickUrl, + vendor: seat, + site: site.id, + slot: site.slot, + cpm: bid.cpm.toPrecision(4), + } + const jsTracker = ' + 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 @@ - + + + - + - +