diff --git a/.github/release-drafter.yml b/.github/release-drafter.yml new file mode 100644 index 00000000000..8984252f4c3 --- /dev/null +++ b/.github/release-drafter.yml @@ -0,0 +1,28 @@ + +name-template: 'Prebid $RESOLVED_VERSION Release' +tag-template: '$RESOLVED_VERSION' +categories: + - title: '🚀 New Features' + label: 'feature' + - title: '🛠 Maintenance' + label: 'maintenance' + - title: '🐛 Bug Fixes' + labels: + - 'fix' + - 'bugfix' + - 'bug' +change-template: '- $TITLE (#$NUMBER)' +version-resolver: + major: + labels: + - 'major' + minor: + labels: + - 'minor' + patch: + labels: + - 'patch' + default: patch +template: | + ## In This Release + $CHANGES diff --git a/.github/workflows/release-drafter.yml b/.github/workflows/release-drafter.yml new file mode 100644 index 00000000000..8152b61275d --- /dev/null +++ b/.github/workflows/release-drafter.yml @@ -0,0 +1,18 @@ +name: Release Drafter + +on: + push: + # branches to consider in the event; optional, defaults to all + branches: + - master + +jobs: + update_release_draft: + runs-on: ubuntu-latest + steps: + # Drafts your next Release notes as Pull Requests are merged into "master" + - uses: release-drafter/release-drafter@v5 + with: + config-name: release-drafter.yml + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/PR_REVIEW.md b/PR_REVIEW.md index f991a0254f5..0519cbb7b6e 100644 --- a/PR_REVIEW.md +++ b/PR_REVIEW.md @@ -5,6 +5,17 @@ If the PR is for a standard bid adapter or a standard analytics adapter, just th For modules and core platform updates, the initial reviewer should request an additional team member to review as a sanity check. Merge should only happen when the PR has 2 `LGTM` from the core team and a documentation PR if required. +### Running Tests and Verifying Integrations + +General gulp commands include separate commands for serving the codebase on a built in webserver, creating code coverage reports and allowing serving integration examples. The `review-start` gulp command combinese those into one command. + +- Run `gulp review-start`, adding the host parameter `gulp review-start --host=0.0.0.0` will bind to all IPs on the machine + - A page will open which provides a hub for common reviewer tools. + - If you need to manually acceess the tools: + - Navigate to build/coverage/lcov-report/index.html to view coverage + - Navigate to integrationExamples/gpt/hellow_world.html for basic integration testing + - The hello_world.html and other exampls can be edited and used as needed to verify functionality + ### General PR review Process - All required global and bidder-adapter rules defined in the [Module Rules](https://docs.prebid.org/dev-docs/module-rules.html) must be followed. Please review these rules often - we depend on reviewers to enforce them. - Checkout the branch (these instructions are available on the github PR page as well). @@ -18,7 +29,10 @@ For modules and core platform updates, the initial reviewer should request an ad - If the change results in needing updates to docs (such as public API change, module interface etc), add a label for "needs docs" and inform the submitter they must submit a docs PR to update the appropriate area of Prebid.org **before the PR can merge**. Help them with finding where the docs are located on prebid.org if needed. - If all above is good, add a `LGTM` comment and, if the change is in PBS-core or is an important module like the prebidServerBidAdapter, request 1 additional core member to review. - Once there are 2 `LGTM` on the PR, merge to master -- Add a line into the [draft release](https://github.com/prebid/Prebid.js/releases) notes for this submission. If no draft release is available, create one using [this template]( https://gist.github.com/mkendall07/c3af6f4691bed8a46738b3675cb5a479) +- The [draft release](https://github.com/prebid/Prebid.js/releases) notes are managed by [release drafter](https://github.com/release-drafter/release-drafter). To get the PR added to the release notes do the steps below. A github action will use that information to build the release notes. + - Adjust the PR Title to be appropriate for release notes + - Add a label for `feature`, `maintenance`, `fix`, `bugfix` or `bug` to categorize the PR + - Add a semver label of `major`, `minor` or `patch` to indicate the scope of change ### Reviewing a New or Updated Bid Adapter Documentation they're supposed to be following is https://docs.prebid.org/dev-docs/bidder-adaptor.html diff --git a/README.md b/README.md index 40df62ccee4..3e541080bf9 100644 --- a/README.md +++ b/README.md @@ -202,6 +202,11 @@ To run the unit tests: gulp test ``` +To run the unit tests for a perticular file (example for pubmaticBidAdapter_spec.js): +```bash +gulp test --file "test/spec/modules/pubmaticBidAdapter_spec.js" +``` + To generate and view the code coverage reports: ```bash diff --git a/RELEASE_SCHEDULE.md b/RELEASE_SCHEDULE.md index 7b2c6244bd7..bfbd0772c3e 100644 --- a/RELEASE_SCHEDULE.md +++ b/RELEASE_SCHEDULE.md @@ -1,6 +1,14 @@ **Table of Contents** - [Release Schedule](#release-schedule) - [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) - [Beta Releases](#beta-releases) - [FAQs](#faqs) @@ -9,7 +17,7 @@ We aim to push a new release of Prebid.js every week on Tuesday. While the releases will be available immediately for those using direct Git access, -it will be about a week before the Prebid Org [Download Page](http://prebid.org/download.html) will be updated. +it will be about a week before the Prebid Org [Download Page](http://prebid.org/download.html) will be updated. You can determine what is in a given build using the [releases page](https://github.com/prebid/Prebid.js/releases) @@ -19,14 +27,20 @@ Announcements regarding releases will be made to the #headerbidding-dev channel _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 all browserstack tests are passing. On PR merge to master CircleCI will run unit tests on browserstack. Checking the last CircleCI build [here](https://circleci.com/gh/prebid/Prebid.js) for master branch will show you detailed results. - - In case of failure do following, +### 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. + +### 2. Make sure all browserstack tests are passing + + On PR merge to master, CircleCI will run unit tests on browserstack. Checking the last CircleCI build [here](https://circleci.com/gh/prebid/Prebid.js) for master branch will show you detailed results.** + + In case of failure do following, - Try to fix the failing tests. - If you are not able to fix tests in time. Skip the test, create issue and tag contributor. - #### How to run tests in browserstack - + **How to run tests in browserstack** + _Note: the following browserstack information is only relevant for debugging purposes, if you will not be debugging then it can be skipped._ Set the environment variables. You may want to add these to your `~/.bashrc` for convenience. @@ -35,40 +49,40 @@ _Note: If `github.com/prebid/Prebid.js` is not configured as the git origin for export BROWSERSTACK_USERNAME="my browserstack username" export BROWSERSTACK_ACCESS_KEY="my browserstack access key" ``` - + ``` gulp test --browserstack >> prebid_test.log - + vim prebid_test.log // Will show the test results ``` -2. Prepare Prebid Code +### 3. Prepare Prebid Code Update the package.json version to become the current release. Then commit your changes. ``` - git commit -m "Prebid 1.x.x Release" + git commit -m "Prebid 4.x.x Release" git push ``` -3. Verify Release +### 4. Verify the Release Make sure your there are no more merges to master branch. Prebid code is clean and up to date. -4. Create a GitHub release +### 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. - Edit the most recent [release notes](https://github.com/prebid/Prebid.js/releases) draft and make sure the correct tag is in the dropdown. Click `Publish`. GitHub will create release tag. - - Pull these changes locally by running command + Pull these changes locally by running command ``` git pull git fetch --tags - ``` - + ``` + and verify the tag. -5. Update coveralls _(skip for legacy)_ +### 6. Update coveralls _(skip for legacy)_ We use https://coveralls.io/ to show parts of code covered by unit tests. @@ -80,35 +94,23 @@ _Note: If `github.com/prebid/Prebid.js` is not configured as the git origin for Run `gulp coveralls` to update code coverage history. -6. Distribute the code +### 7. Distribute the code - _Note: do not go to step 7 until step 6 has been verified completed._ + _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 + // 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. -7. Post Release Version - - Update the version - Manually edit Prebid's package.json to become "1.x.x-pre" (using the values for the next release). Then commit your changes. +### 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 ``` - -8. Create new release draft - - Go to [github releases](https://github.com/prebid/Prebid.js/releases) and add a new draft for the next version of Prebid.js with the following template: -``` -## 🚀New Features - -## 🛠Maintenance - -## 🐛Bug Fixes -``` ## Beta Releases @@ -129,11 +131,8 @@ Characteristics of a `GA` release: ## FAQs **1. Is there flexibility in the schedule?** - -If a major bug is found in the current release, a maintenance patch will be done as soon as possible. - -It is unlikely that we will put out a maintenance patch at the request of a given bid adapter or module owner. +* If a major bug is found in the current release, a maintenance patch will be done as soon as possible. +* It is unlikely that we will put out a maintenance patch at the request of a given bid adapter or module owner. **2. What Pull Requests make it into a release?** - -Every PR that's merged into master will be part of a release. Here are the [PR review guidelines](https://github.com/prebid/Prebid.js/blob/master/PR_REVIEW.md). +* Every PR that's merged into master will be part of a release. Here are the [PR review guidelines](https://github.com/prebid/Prebid.js/blob/master/PR_REVIEW.md). diff --git a/gulpfile.js b/gulpfile.js index 879e34ae588..51536992bd0 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -31,7 +31,7 @@ const execa = require('execa'); 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 banner = '/* <%= prebid.name %> v<%= prebid.version %>\n' + dateString + '\nModules: <%= modules %> */\n'; var port = 9999; const FAKE_SERVER_HOST = argv.host ? argv.host : 'localhost'; const FAKE_SERVER_PORT = 4444; @@ -86,7 +86,8 @@ function viewCoverage(done) { connect.server({ port: coveragePort, root: 'build/coverage/lcov-report', - livereload: false + livereload: false, + debug: true }); opens('http://' + mylocalhost + ':' + coveragePort); done(); @@ -94,6 +95,19 @@ function viewCoverage(done) { viewCoverage.displayName = 'view-coverage'; +// View the reviewer tools page +function viewReview(done) { + var mylocalhost = (argv.host) ? argv.host : 'localhost'; + var reviewUrl = 'http://' + mylocalhost + ':' + port + '/integrationExamples/reviewerTools/index.html'; // reuse the main port from 9999 + + // console.log(`stdout: opening` + reviewUrl); + + opens(reviewUrl); + done(); +}; + +viewReview.displayName = 'view-review'; + // Watch Task with Live Reload function watch(done) { var mainWatcher = gulp.watch([ @@ -143,12 +157,13 @@ function makeWebpackPkg() { const analyticsSources = helpers.getAnalyticsSources(); const moduleSources = helpers.getModulePaths(externalModules); + const modulesString = (externalModules.length > 0) ? externalModules.join(', ') : 'All available modules in current version.'; return gulp.src([].concat(moduleSources, analyticsSources, 'src/prebid.js')) .pipe(helpers.nameModules(externalModules)) .pipe(webpackStream(cloned, webpack)) .pipe(uglify()) - .pipe(gulpif(file => file.basename === 'prebid-core.js', header(banner, { prebid: prebid }))) + .pipe(gulpif(file => file.basename === 'prebid-core.js', header(banner, { prebid: prebid, modules: modulesString }))) .pipe(gulp.dest('build/dist')); } @@ -383,4 +398,8 @@ gulp.task('e2e-test', gulp.series(clean, setupE2e, gulp.parallel('build-bundle-p gulp.task(bundleToStdout); gulp.task('bundle', gulpBundle.bind(null, false)); // used for just concatenating pre-built files with no build step +// build task for reviewers, runs test-coverage, serves, without watching +gulp.task(viewReview); +gulp.task('review-start', gulp.series(clean, lint, gulp.parallel('build-bundle-dev', watch, testCoverage), viewReview)); + module.exports = nodeBundle; diff --git a/integrationExamples/gpt/permutiveRtdProvider_example.html b/integrationExamples/gpt/permutiveRtdProvider_example.html new file mode 100644 index 00000000000..0814dcece5b --- /dev/null +++ b/integrationExamples/gpt/permutiveRtdProvider_example.html @@ -0,0 +1,223 @@ + + + + + + + + + + + +

+

Basic Prebid.js Example

+
Div-1
+
+ +
+ +
+ +
Div-2
+
+ +
+ + + + diff --git a/integrationExamples/gpt/userId_example.html b/integrationExamples/gpt/userId_example.html index 7375293fdf0..71299a4a6e1 100644 --- a/integrationExamples/gpt/userId_example.html +++ b/integrationExamples/gpt/userId_example.html @@ -1,334 +1,337 @@ + - - + + + ] + } + ]; - + - var adUnits = [ - { - code: 'test-div', - mediaTypes: { - banner: { - sizes: [[300,250],[300,600],[728,90]] - } - }, - bids: [ - { - bidder: 'rubicon', - params: { - accountId: '1001', - siteId: '113932', - zoneId: '535510' - } - } - ] - } - ]; + - + pbjs.que.push(function() { + pbjs.setConfig({ + debug: true, + consentManagement: { + cmpApi: 'iab', + timeout: 1000, + defaultGdprScope: true + }, + // consentManagement: { + // cmpApi: 'static', + // consentData: { + // consentString: 'BOEFEAyOEFEAyAHABDENAI4AAAB9vABAASA' + // vendorData: { + // purposeConsents: { + // '1': true + // } + // } + // } + // }, + userSync: { + userIds: [{ + name: "pubProvidedId", + params: { + eids: [{ + source: "domain.com", + uids:[{ + id: "value read from cookie or local storage", + atype: 1, + ext: { + stype: "ppuid" // allowable options are sha256email, DMP, ppuid for now + } + }] + },{ + source: "3rdpartyprovided.com", + uids:[{ + id: "value read from cookie or local storage", + atype: 3, + ext: { + stype: "sha256email" + } + }] + }], + eidsFunction: getHashedEmail // any user defined function that exists in the page + } + },{ + name: "unifiedId", + params: { + partner: "prebid", + url: "http://match.adsrvr.org/track/rid?ttd_pid=prebid&fmt=json" + }, + storage: { + type: "html5", + name: "unifiedid", + expires: 30 + }, + },{ + name: "intentIqId", + params: { + partner: 0, //Set your real IntentIQ partner ID here for production. + }, + storage: { + type: "cookie", + name: "intentIqId", + expires: 30 + }, + }, + { + name: "id5Id", + params: { + partner: 173 //Set your real ID5 partner ID here for production, please ask for one at http://id5.io/prebid + }, + storage: { + type: "cookie", + name: "id5id", + expires: 90, + refreshInSeconds: 8*3600 // Refresh frequency of cookies, defaulting to 'expires' + }, - + function sendAdserverRequest() { + if (pbjs.adserverRequestSent) return; + pbjs.adserverRequestSent = true; + googletag.cmd.push(function() { + pbjs.que.push(function() { + pbjs.setTargetingForGPTAsync(); + googletag.pubads().refresh(); + }); + }); + } - + setTimeout(function() { + sendAdserverRequest(); + }, FAILSAFE_TIMEOUT); + - - + - -

Rubicon Project Prebid

+ + -
- -
- + +

Rubicon Project Prebid

+
+ +
+ diff --git a/integrationExamples/postbid/postbid_prebidServer_example.html b/integrationExamples/postbid/postbid_prebidServer_example.html new file mode 100644 index 00000000000..3af7b010872 --- /dev/null +++ b/integrationExamples/postbid/postbid_prebidServer_example.html @@ -0,0 +1,86 @@ + + + + + + + + + + + diff --git a/integrationExamples/reviewerTools/index.html b/integrationExamples/reviewerTools/index.html new file mode 100755 index 00000000000..2732cb4fd88 --- /dev/null +++ b/integrationExamples/reviewerTools/index.html @@ -0,0 +1,40 @@ + + + + + + + Prebid Reviewer Tools + + + + +
+
+
+

Reviewer Tools

+

Below are links to the most common tool used by Prebid reviewers. For more info on PR review processes check out the General PR Review Process page on Github.

+

Common

+ +

Other Tools

+ +

Documentation & Training Material

+ +
+
+
+ + \ No newline at end of file diff --git a/modules/.submodules.json b/modules/.submodules.json index 4e02391129a..fc69dd276a3 100644 --- a/modules/.submodules.json +++ b/modules/.submodules.json @@ -19,7 +19,8 @@ "idxIdSystem", "fabrickIdSystem", "verizonMediaIdSystem", - "pubProvidedIdSystem" + "pubProvidedIdSystem", + "tapadIdSystem" ], "adpod": [ "freeWheelAdserverVideo", diff --git a/modules/adWMGBidAdapter.js b/modules/adWMGBidAdapter.js new file mode 100644 index 00000000000..87c40db51e6 --- /dev/null +++ b/modules/adWMGBidAdapter.js @@ -0,0 +1,301 @@ +'use strict'; + +import * as utils from '../src/utils.js'; +import { registerBidder } from '../src/adapters/bidderFactory.js'; +import { config } from '../src/config.js'; +import { BANNER } from '../src/mediaTypes.js'; + +const BIDDER_CODE = 'adWMG'; +const ENDPOINT = 'https://rtb.adwmg.com/prebid'; +let SYNC_ENDPOINT = 'https://rtb.adwmg.com/cphb.html?'; + +export const spec = { + code: BIDDER_CODE, + aliases: ['wmg'], + supportedMediaTypes: [BANNER], + isBidRequestValid: (bid) => { + if (bid.bidder !== BIDDER_CODE) { + return false; + } + + if (!(bid.params.publisherId)) { + return false; + } + + return true; + }, + buildRequests: (validBidRequests, bidderRequest) => { + const timeout = bidderRequest.timeout || 0; + const debug = config.getConfig('debug') || false; + const referrer = bidderRequest.refererInfo.referer; + const locale = window.navigator.language && window.navigator.language.length > 0 ? window.navigator.language.substr(0, 2) : ''; + const domain = config.getConfig('publisherDomain') || (window.location && window.location.host ? window.location.host : ''); + const ua = window.navigator.userAgent.toLowerCase(); + const additional = spec.parseUserAgent(ua); + + return validBidRequests.map(bidRequest => { + const adUnit = { + code: bidRequest.adUnitCode, + bids: { + bidder: bidRequest.bidder, + params: bidRequest.params + }, + mediaTypes: bidRequest.mediaTypes + }; + + if (bidRequest.hasOwnProperty('sizes') && bidRequest.sizes.length > 0) { + adUnit.sizes = bidRequest.sizes; + } + + const request = { + auctionId: bidRequest.auctionId, + requestId: bidRequest.bidId, + bidRequestsCount: bidRequest.bidRequestsCount, + bidderRequestId: bidRequest.bidderRequestId, + transactionId: bidRequest.transactionId, + referrer: referrer, + timeout: timeout, + adUnit: adUnit, + locale: locale, + domain: domain, + os: additional.os, + osv: additional.osv, + devicetype: additional.devicetype + }; + + if (bidderRequest.gdprConsent) { + request.gdpr = { + applies: bidderRequest.gdprConsent.gdprApplies, + consentString: bidderRequest.gdprConsent.consentString + }; + } + + /* if (bidderRequest.uspConsent) { + request.uspConsent = bidderRequest.uspConsent; + } + */ + if (bidRequest.userId && bidRequest.userId.pubcid) { + request.userId = { + pubcid: bidRequest.userId.pubcid + }; + } + + if (debug) { + request.debug = debug; + } + + return { + method: 'POST', + url: ENDPOINT, + data: JSON.stringify(request) + } + }); + }, + interpretResponse: (serverResponse) => { + const bidResponses = []; + + if (serverResponse.body) { + const response = serverResponse.body; + const bidResponse = { + requestId: response.requestId, + cpm: response.cpm, + width: response.width, + height: response.height, + creativeId: response.creativeId, + currency: response.currency, + netRevenue: response.netRevenue, + ttl: response.ttl, + ad: response.ad, + }; + bidResponses.push(bidResponse); + } + + return bidResponses; + }, + getUserSyncs: (syncOptions, serverResponses, gdprConsent, uspConsent) => { + if (gdprConsent && SYNC_ENDPOINT.indexOf('gdpr') === -1) { + SYNC_ENDPOINT = utils.tryAppendQueryString(SYNC_ENDPOINT, 'gdpr', (gdprConsent.gdprApplies ? 1 : 0)); + } + + if (gdprConsent && typeof gdprConsent.consentString === 'string' && SYNC_ENDPOINT.indexOf('gdpr_consent') === -1) { + SYNC_ENDPOINT = utils.tryAppendQueryString(SYNC_ENDPOINT, 'gdpr_consent', gdprConsent.consentString); + } + + if (SYNC_ENDPOINT.slice(-1) === '&') { + SYNC_ENDPOINT = SYNC_ENDPOINT.slice(0, -1); + } + + /* if (uspConsent) { + SYNC_ENDPOINT = utils.tryAppendQueryString(SYNC_ENDPOINT, 'us_privacy', uspConsent); + } */ + let syncs = []; + if (syncOptions.iframeEnabled) { + syncs.push({ + type: 'iframe', + url: SYNC_ENDPOINT + }); + } + return syncs; + }, + parseUserAgent: (ua) => { + function detectDevice() { + 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 2; + } + + function detectOs() { + const module = { + options: [], + header: [navigator.platform, ua, navigator.appVersion, navigator.vendor, window.opera], + dataos: [{ + name: 'Windows Phone', + value: 'Windows Phone', + version: 'OS' + }, + { + name: 'Windows', + value: 'Win', + version: 'NT' + }, + { + name: 'iOS', + value: 'iPhone', + version: 'OS' + }, + { + name: 'iOS', + value: 'iPad', + version: 'OS' + }, + { + name: 'Kindle', + value: 'Silk', + version: 'Silk' + }, + { + name: 'Android', + value: 'Android', + version: 'Android' + }, + { + name: 'PlayBook', + value: 'PlayBook', + version: 'OS' + }, + { + name: 'BlackBerry', + value: 'BlackBerry', + version: '/' + }, + { + name: 'Macintosh', + value: 'Mac', + version: 'OS X' + }, + { + name: 'Linux', + value: 'Linux', + version: 'rv' + }, + { + name: 'Palm', + value: 'Palm', + version: 'PalmOS' + } + ], + init: function () { + var agent = this.header.join(' '); + var os = this.matchItem(agent, this.dataos); + return { + os + }; + }, + + getVersion: function (name, version) { + if (name === 'Windows') { + switch (parseFloat(version).toFixed(1)) { + case '5.0': + return '2000'; + case '5.1': + return 'XP'; + case '5.2': + return 'Server 2003'; + case '6.0': + return 'Vista'; + case '6.1': + return '7'; + case '6.2': + return '8'; + case '6.3': + return '8.1'; + default: + return version || 'other'; + } + } else return version || 'other'; + }, + + matchItem: function (string, data) { + var i = 0; + var j = 0; + var regex, regexv, match, matches, version; + + for (i = 0; i < data.length; i += 1) { + regex = new RegExp(data[i].value, 'i'); + match = regex.test(string); + if (match) { + regexv = new RegExp(data[i].version + '[- /:;]([\\d._]+)', 'i'); + matches = string.match(regexv); + version = ''; + if (matches) { + if (matches[1]) { + matches = matches[1]; + } + } + if (matches) { + matches = matches.split(/[._]+/); + for (j = 0; j < matches.length; j += 1) { + if (j === 0) { + version += matches[j] + '.'; + } else { + version += matches[j]; + } + } + } else { + version = 'other'; + } + return { + name: data[i].name, + version: this.getVersion(data[i].name, version) + }; + } + } + return { + name: 'unknown', + version: 'other' + }; + } + }; + + var e = module.init(); + + return { + os: e.os.name || '', + osv: e.os.version || '' + } + } + + return {devicetype: detectDevice(), os: detectOs().os, osv: detectOs().osv} + } +} +registerBidder(spec); diff --git a/modules/adWMGBidAdapter.md b/modules/adWMGBidAdapter.md new file mode 100644 index 00000000000..ec5541e6168 --- /dev/null +++ b/modules/adWMGBidAdapter.md @@ -0,0 +1,35 @@ +# Overview + +``` +Module Name: adWMG Adapter +Module Type: Bidder Adapter +Maintainer: wbid@adwmg.com +``` + +# Description + +Module that connects to adWMG demand sources to fetch bids. Supports 'banner' ad format. + +# Bid Parameters + +| Key | Required | Example | Description | +| --------------- | -------- | -----------------------------| ------------------------------- | +| `publisherId` | yes | `'5cebea3c9eea646c7b623d5e'` | publisher ID from WMG Dashboard | +| `IABCategories` | no | `['IAB1', 'IAB5']` | IAB ad categories for adUnit | +| `floorCPM` | no | `0.5` | Floor price for adUnit | + + +# Test Parameters + +```javascript +var adUnits = [{ + code: 'wmg-test-div', + sizes: [[300, 250]], + bids: [{ + bidder: 'adWMG', + params: { + publisherId: '5cebea3c9eea646c7b623d5e', + IABCategories: ['IAB1', 'IAB5'] + }, + }] +}] \ No newline at end of file diff --git a/modules/adagioBidAdapter.js b/modules/adagioBidAdapter.js index cab233d5387..6f7feec59c9 100644 --- a/modules/adagioBidAdapter.js +++ b/modules/adagioBidAdapter.js @@ -495,6 +495,12 @@ function autoDetectEnvironment() { return environment; }; +function supportIObs() { + const currentWindow = internal.getCurrentWindow(); + return !!(currentWindow && currentWindow.IntersectionObserver && currentWindow.IntersectionObserverEntry && + currentWindow.IntersectionObserverEntry.prototype && 'intersectionRatio' in currentWindow.IntersectionObserverEntry.prototype); +} + function getFeatures(bidRequest, bidderRequest) { const { adUnitCode, params } = bidRequest; const { adUnitElementId } = params; @@ -569,6 +575,7 @@ export const internal = { getRefererInfo, adagioScriptFromLocalStorageCb, getCurrentWindow, + supportIObs, canAccessTopWindow, isRendererPreferredFromPublisher }; @@ -692,15 +699,21 @@ export const spec = { return false; } - const { organizationId, site, placement } = params; - const adUnitElementId = params.adUnitElementId || internal.autoDetectAdUnitElementId(adUnitCode); + const { organizationId, site } = params; + const adUnitElementId = (params.useAdUnitCodeAsAdUnitElementId === true) + ? adUnitCode + : params.adUnitElementId || internal.autoDetectAdUnitElementId(adUnitCode); + const placement = (params.useAdUnitCodeAsPlacement === true) ? adUnitCode : params.placement; const environment = params.environment || internal.autoDetectEnvironment(); + const supportIObs = internal.supportIObs(); // insure auto-detected params are kept in `bid` object. bid.params = { ...params, adUnitElementId, - environment + environment, + placement, + supportIObs }; const debugData = () => ({ diff --git a/modules/adagioBidAdapter.md b/modules/adagioBidAdapter.md index c55a24f1115..aa79338d79e 100644 --- a/modules/adagioBidAdapter.md +++ b/modules/adagioBidAdapter.md @@ -38,6 +38,8 @@ Connects to Adagio demand source to fetch bids. category: 'sport', // Recommended. Category of the content displayed in the page. subcategory: 'handball', // Optional. Subcategory of the content displayed in the page. postBid: false, // Optional. Use it in case of Post-bid integration only. + useAdUnitCodeAsAdUnitElementId: false // Optional. Use it by-pass adUnitElementId and use the adUnit code as value + useAdUnitCodeAsPlacement: false // Optional. Use it to by-pass placement and use the adUnit code as value // Optional debug mode, used to get a bid response with expected cpm. debug: { enabled: true, @@ -76,6 +78,8 @@ Connects to Adagio demand source to fetch bids. category: 'sport', // Recommended. Category of the content displayed in the page. subcategory: 'handball', // Optional. Subcategory of the content displayed in the page. postBid: false, // Optional. Use it in case of Post-bid integration only. + useAdUnitCodeAsAdUnitElementId: false // Optional. Use it by-pass adUnitElementId and use the adUnit code as value + useAdUnitCodeAsPlacement: false // Optional. Use it to by-pass placement and use the adUnit code as value video: { skip: 0 // OpenRTB 2.5 video options defined here override ones defined in mediaTypes. diff --git a/modules/adblender.md b/modules/adblender.md new file mode 100644 index 00000000000..e70b2a4a8ed --- /dev/null +++ b/modules/adblender.md @@ -0,0 +1,36 @@ +# Overview + +Module Name: AdBlender Bidder Adapter +Module Type: Bidder Adapter +Maintainer: contact@ad-blender.com + +# Description + +Connects to AdBlender demand source to fetch bids. +Banner and Video formats are supported. +Please use ```adblender``` as the bidder code. +#Bidder Config +You can set an alternate endpoint url `pbjs.setBidderConfig` for the bidder `adblender` +``` +pbjs.setBidderConfig({ + bidders: ["adblender"], + config: {"adblender": { "endpoint_url": "https://inv-nets.admixer.net/adblender.1.1.aspx"}} + }) +``` +# Ad Unit Example +``` + var adUnits = [ + { + code: 'desktop-banner-ad-div', + sizes: [[300, 250]], // a display size + bids: [ + { + bidder: "adblender", + params: { + zone: 'fb3d34d0-7a88-4a4a-a5c9-8088cd7845f4' + } + } + ] + } + ]; +``` diff --git a/modules/adhashBidAdapter.js b/modules/adhashBidAdapter.js new file mode 100644 index 00000000000..b30f9cebbc1 --- /dev/null +++ b/modules/adhashBidAdapter.js @@ -0,0 +1,102 @@ +import { registerBidder } from '../src/adapters/bidderFactory.js'; +import includes from 'core-js-pure/features/array/includes.js'; +import { BANNER } from '../src/mediaTypes.js'; + +const VERSION = '1.0'; + +export const spec = { + code: 'adhash', + url: 'https://bidder.adhash.org/rtb?version=' + VERSION + '&prebid=true', + supportedMediaTypes: [ BANNER ], + + isBidRequestValid: (bid) => { + try { + const { publisherId, platformURL } = bid.params; + return ( + includes(Object.keys(bid.mediaTypes), BANNER) && + typeof publisherId === 'string' && + publisherId.length === 42 && + typeof platformURL === 'string' && + platformURL.length >= 13 + ); + } catch (error) { + return false; + } + }, + + buildRequests: (validBidRequests, bidderRequest) => { + const { gdprConsent } = bidderRequest; + const { url } = spec; + const bidRequests = []; + let referrer = ''; + if (bidderRequest && bidderRequest.refererInfo) { + referrer = bidderRequest.refererInfo.referer; + } + for (var i = 0; i < validBidRequests.length; i++) { + var index = Math.floor(Math.random() * validBidRequests[i].sizes.length); + var size = validBidRequests[i].sizes[index].join('x'); + bidRequests.push({ + method: 'POST', + url: url, + bidRequest: validBidRequests[i], + data: { + timezone: new Date().getTimezoneOffset() / 60, + location: referrer, + publisherId: validBidRequests[i].params.publisherId, + size: { + screenWidth: window.screen.width, + screenHeight: window.screen.height + }, + navigator: { + platform: window.navigator.platform, + language: window.navigator.language, + userAgent: window.navigator.userAgent + }, + creatives: [{ + size: size, + position: validBidRequests[i].adUnitCode + }], + blockedCreatives: [], + currentTimestamp: new Date().getTime(), + recentAds: [], + GDPR: gdprConsent + }, + options: { + withCredentials: false, + crossOrigin: true + }, + }); + } + return bidRequests; + }, + + interpretResponse: (serverResponse, request) => { + const responseBody = serverResponse ? serverResponse.body : {}; + + if (!responseBody.creatives || responseBody.creatives.length === 0) { + return []; + } + + const publisherURL = JSON.stringify(request.bidRequest.params.platformURL); + const oneTimeId = request.bidRequest.adUnitCode + Math.random().toFixed(16).replace('0.', '.'); + const bidderResponse = JSON.stringify({ responseText: JSON.stringify(responseBody) }); + const requestData = JSON.stringify(request.data); + + return [{ + requestId: request.bidRequest.bidId, + cpm: responseBody.creatives[0].costEUR, + ad: + `
+ + `, + width: request.bidRequest.sizes[0][0], + height: request.bidRequest.sizes[0][1], + creativeId: request.bidRequest.adUnitCode, + netRevenue: true, + currency: 'EUR', + ttl: 60 + }]; + } +}; + +registerBidder(spec); diff --git a/modules/adhashBidAdapter.md b/modules/adhashBidAdapter.md new file mode 100644 index 00000000000..c1093e0ccd7 --- /dev/null +++ b/modules/adhashBidAdapter.md @@ -0,0 +1,43 @@ +# Overview + +``` +Module Name: AdHash Bidder Adapter +Module Type: Bidder Adapter +Maintainer: damyan@adhash.org +``` + +# Description + +Here is what you need for Prebid integration with AdHash: +1. Register with AdHash. +2. Once registered and approved, you will receive a Publisher ID and Platform URL. +3. Use the Publisher ID and Platform URL as parameters in params. + +Please note that a number of AdHash functionalities are not supported in the Prebid.js integration: +* Cookie-less frequency and recency capping; +* Audience segments; +* Price floors and passback tags, as they are not needed in the Prebid.js setup; +* Reservation for direct deals only, as bids are evaluated based on their price. + +# Test Parameters +``` + var adUnits = [ + { + code: '/19968336/header-bid-tag-1', + mediaTypes: { + banner: { + sizes: [[300, 250]] + } + }, + bids: [ + { + bidder: 'adhash', + params: { + publisherId: '0x1234567890123456789012345678901234567890', + platformURL: 'https://adhash.org/p/struma/' + } + } + ] + } + ]; +``` \ No newline at end of file diff --git a/modules/adheseBidAdapter.js b/modules/adheseBidAdapter.js index 80758668a95..4fae85e82ad 100644 --- a/modules/adheseBidAdapter.js +++ b/modules/adheseBidAdapter.js @@ -22,16 +22,24 @@ export const spec = { } const { gdprConsent, refererInfo } = bidderRequest; - const targets = validBidRequests.map(bid => bid.params.data).reduce(mergeTargets, {}); const gdprParams = (gdprConsent && gdprConsent.consentString) ? { xt: [gdprConsent.consentString] } : {}; const refererParams = (refererInfo && refererInfo.referer) ? { xf: [base64urlEncode(refererInfo.referer)] } : {}; - const id5Params = (getId5Id(validBidRequests)) ? { x5: [getId5Id(validBidRequests)] } : {}; - const slots = validBidRequests.map(bid => ({ slotname: bidToSlotName(bid) })); + const commonParams = { ...gdprParams, ...refererParams }; + + const slots = validBidRequests.map(bid => ({ + slotname: bidToSlotName(bid), + parameters: cleanTargets(bid.params.data) + })); const payload = { slots: slots, - parameters: { ...targets, ...gdprParams, ...refererParams, ...id5Params } - } + parameters: commonParams, + user: { + ext: { + eids: getEids(validBidRequests), + } + } + }; const account = getAccount(validBidRequests); const uri = 'https://ads-' + account + '.adhese.com/json'; @@ -110,7 +118,8 @@ function adResponse(bid, ad) { return bidResponse; } -function mergeTargets(targets, target) { +function cleanTargets(target) { + const targets = {}; if (target) { Object.keys(target).forEach(function (key) { const val = target[key]; @@ -149,9 +158,9 @@ function getAccount(validBidRequests) { return validBidRequests[0].params.account; } -function getId5Id(validBidRequests) { - if (validBidRequests[0] && validBidRequests[0].userId && validBidRequests[0].userId.id5id && validBidRequests[0].userId.id5id.uid) { - return validBidRequests[0].userId.id5id.uid; +function getEids(validBidRequests) { + if (validBidRequests[0] && validBidRequests[0].userIdAsEids) { + return validBidRequests[0].userIdAsEids; } } diff --git a/modules/adkernelBidAdapter.js b/modules/adkernelBidAdapter.js index c34902eda46..03fa5c2b2d9 100644 --- a/modules/adkernelBidAdapter.js +++ b/modules/adkernelBidAdapter.js @@ -52,7 +52,7 @@ const NATIVE_INDEX = NATIVE_MODEL.reduce((acc, val, idx) => { export const spec = { code: 'adkernel', - aliases: ['headbidding', 'adsolut', 'oftmediahb', 'audiencemedia', 'waardex_ak', 'roqoon', 'andbeyond', 'adbite', 'houseofpubs'], + aliases: ['headbidding', 'adsolut', 'oftmediahb', 'audiencemedia', 'waardex_ak', 'roqoon', 'andbeyond', 'adbite', 'houseofpubs', 'torchad', 'stringads', 'bcm'], supportedMediaTypes: [BANNER, VIDEO, NATIVE], /** diff --git a/modules/admixerBidAdapter.js b/modules/admixerBidAdapter.js index 405dd81cc8c..d2a6f9a639e 100644 --- a/modules/admixerBidAdapter.js +++ b/modules/admixerBidAdapter.js @@ -1,8 +1,9 @@ import * as utils from '../src/utils.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; +import {config} from '../src/config.js'; const BIDDER_CODE = 'admixer'; -const ALIASES = ['go2net']; +const ALIASES = ['go2net', 'adblender']; const ENDPOINT_URL = 'https://inv-nets.admixer.net/prebid.1.1.aspx'; export const spec = { code: BIDDER_CODE, @@ -20,8 +21,12 @@ export const spec = { buildRequests: function (validRequest, bidderRequest) { const payload = { imps: [], + fpd: config.getConfig('fpd') }; + let endpointUrl; if (bidderRequest) { + const {bidderCode} = bidderRequest; + endpointUrl = config.getConfig(`${bidderCode}.endpoint_url`); if (bidderRequest.refererInfo && bidderRequest.refererInfo.referer) { payload.referrer = encodeURIComponent(bidderRequest.refererInfo.referer); } @@ -42,7 +47,7 @@ export const spec = { const payloadString = JSON.stringify(payload); return { method: 'GET', - url: ENDPOINT_URL, + url: endpointUrl || ENDPOINT_URL, data: `data=${payloadString}`, }; }, diff --git a/modules/adprimeBidAdapter.js b/modules/adprimeBidAdapter.js index 50303b82979..12d0410a821 100644 --- a/modules/adprimeBidAdapter.js +++ b/modules/adprimeBidAdapter.js @@ -4,6 +4,7 @@ import * as utils from '../src/utils.js'; const BIDDER_CODE = 'adprime'; const AD_URL = 'https://delta.adprime.com/?c=o&m=multi'; +const SYNC_URL = 'https://delta.adprime.com/?c=rtb&m=sync'; function isBidResponseValid(bid) { if (!bid.requestId || !bid.cpm || !bid.creativeId || @@ -104,6 +105,25 @@ export const spec = { } return response; }, + + getUserSyncs: (syncOptions, serverResponses, gdprConsent, uspConsent) => { + let syncUrl = SYNC_URL + 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}`; + } + return [{ + type: 'image', + url: syncUrl + }]; + } + }; registerBidder(spec); diff --git a/modules/adtelligentBidAdapter.js b/modules/adtelligentBidAdapter.js index 51138a2cac7..669feeedb04 100644 --- a/modules/adtelligentBidAdapter.js +++ b/modules/adtelligentBidAdapter.js @@ -12,15 +12,17 @@ const HOST_GETTERS = { default: (function () { let num = 0; return function () { - return 'ghb' + subdomainSuffixes[num++ % subdomainSuffixes.length] + '.adtelligent.com' + return 'ghb' + subdomainSuffixes[num++ % subdomainSuffixes.length] + '.adtelligent.com'; } }()), - appaloosa: function () { - return 'hb.appaloosa.media' - } + navelix: () => 'ghb.hb.navelix.com', + appaloosa: () => 'ghb.hb.appaloosa.media', + onefiftytwomedia: () => 'ghb.ads.152media.com', + mediafuse: () => 'ghb.hbmp.mediafuse.com', } const getUri = function (bidderCode) { - let getter = HOST_GETTERS[bidderCode] || HOST_GETTERS['default']; + let bidderWithoutSuffix = bidderCode.split('_')[0]; + let getter = HOST_GETTERS[bidderWithoutSuffix] || HOST_GETTERS['default']; return PROTOCOL + getter() + AUCTION_PATH } const OUTSTREAM_SRC = 'https://player.adtelligent.com/outstream-unit/2.01/outstream.min.js'; @@ -32,7 +34,13 @@ const syncsCache = {}; export const spec = { code: BIDDER_CODE, gvlid: 410, - aliases: ['onefiftytwomedia', 'selectmedia', 'appaloosa'], + aliases: ['onefiftytwomedia', 'selectmedia', 'appaloosa', + { code: 'navelix', gvlid: 380 }, + { + code: 'mediafuse', + skipPbsAliasing: true + } + ], supportedMediaTypes: [VIDEO, BANNER], isBidRequestValid: function (bid) { return !!utils.deepAccess(bid, 'params.aid'); @@ -195,6 +203,11 @@ function prepareBidRequests(bidReq) { 'AdType': mediaType, 'Sizes': utils.parseSizesInput(sizes).join(',') }; + + bidReqParams.PlacementId = bidReq.adUnitCode; + if (bidReq.params.vpb_placement_id) { + bidReqParams.PlacementId = bidReq.params.vpb_placement_id; + } if (mediaType === VIDEO) { const context = utils.deepAccess(bidReq, 'mediaTypes.video.context'); if (context === ADPOD) { diff --git a/modules/adtrueBidAdapter.js b/modules/adtrueBidAdapter.js new file mode 100644 index 00000000000..fa83f9f29f7 --- /dev/null +++ b/modules/adtrueBidAdapter.js @@ -0,0 +1,637 @@ +import * as utils from '../src/utils.js'; +import {registerBidder} from '../src/adapters/bidderFactory.js'; +import {BANNER, NATIVE, VIDEO} from '../src/mediaTypes.js'; +import {config} from '../src/config.js'; +import {getStorageManager} from '../src/storageManager.js'; + +const storage = getStorageManager(); +const BIDDER_CODE = 'adtrue'; +const ADTRUE_CURRENCY = 'USD'; +const ENDPOINT_URL = 'https://hb.adtrue.com/prebid/auction'; +const LOG_WARN_PREFIX = 'AdTrue: '; +const AUCTION_TYPE = 1; +const UNDEFINED = undefined; +const DEFAULT_WIDTH = 0; +const DEFAULT_HEIGHT = 0; +const NET_REVENUE = false; +const USER_SYNC_URL_IFRAME = 'https://hb.adtrue.com/prebid/usersync?t=iframe&p='; +const USER_SYNC_URL_IMAGE = 'https://hb.adtrue.com/prebid/usersync?t=img&p='; +let publisherId = 0; +let NATIVE_ASSET_ID_TO_KEY_MAP = {}; +const DATA_TYPES = { + 'NUMBER': 'number', + 'STRING': 'string', + 'BOOLEAN': 'boolean', + 'ARRAY': 'array', + 'OBJECT': 'object' +}; +const VIDEO_CUSTOM_PARAMS = { + 'mimes': DATA_TYPES.ARRAY, + 'minduration': DATA_TYPES.NUMBER, + 'maxduration': DATA_TYPES.NUMBER, + 'startdelay': DATA_TYPES.NUMBER, + 'playbackmethod': DATA_TYPES.ARRAY, + 'api': DATA_TYPES.ARRAY, + 'protocols': DATA_TYPES.ARRAY, + 'w': DATA_TYPES.NUMBER, + 'h': DATA_TYPES.NUMBER, + 'battr': DATA_TYPES.ARRAY, + 'linearity': DATA_TYPES.NUMBER, + 'placement': DATA_TYPES.NUMBER, + 'minbitrate': DATA_TYPES.NUMBER, + 'maxbitrate': DATA_TYPES.NUMBER +} + +const NATIVE_ASSETS = { + 'TITLE': {ID: 1, KEY: 'title', TYPE: 0}, + 'IMAGE': {ID: 2, KEY: 'image', TYPE: 0}, + 'ICON': {ID: 3, KEY: 'icon', TYPE: 0}, + 'SPONSOREDBY': {ID: 4, KEY: 'sponsoredBy', TYPE: 1}, // please note that type of SPONSORED is also 1 + 'BODY': {ID: 5, KEY: 'body', TYPE: 2}, // please note that type of DESC is also set to 2 + 'CLICKURL': {ID: 6, KEY: 'clickUrl', TYPE: 0}, + 'VIDEO': {ID: 7, KEY: 'video', TYPE: 0}, + 'EXT': {ID: 8, KEY: 'ext', TYPE: 0}, + 'DATA': {ID: 9, KEY: 'data', TYPE: 0}, + 'LOGO': {ID: 10, KEY: 'logo', TYPE: 0}, + 'SPONSORED': {ID: 11, KEY: 'sponsored', TYPE: 1}, // please note that type of SPONSOREDBY is also set to 1 + 'DESC': {ID: 12, KEY: 'data', TYPE: 2}, // please note that type of BODY is also set to 2 + 'RATING': {ID: 13, KEY: 'rating', TYPE: 3}, + 'LIKES': {ID: 14, KEY: 'likes', TYPE: 4}, + 'DOWNLOADS': {ID: 15, KEY: 'downloads', TYPE: 5}, + 'PRICE': {ID: 16, KEY: 'price', TYPE: 6}, + 'SALEPRICE': {ID: 17, KEY: 'saleprice', TYPE: 7}, + 'PHONE': {ID: 18, KEY: 'phone', TYPE: 8}, + 'ADDRESS': {ID: 19, KEY: 'address', TYPE: 9}, + 'DESC2': {ID: 20, KEY: 'desc2', TYPE: 10}, + 'DISPLAYURL': {ID: 21, KEY: 'displayurl', TYPE: 11}, + 'CTA': {ID: 22, KEY: 'cta', TYPE: 12} +}; + +function _getDomainFromURL(url) { + let anchor = document.createElement('a'); + anchor.href = url; + return anchor.hostname; +} + +function _parseSlotParam(paramName, paramValue) { + switch (paramName) { + case 'reserve': + return parseFloat(paramValue) || UNDEFINED; + default: + return paramValue; + } +} + +let platform = (function getPlatform() { + var ua = navigator.userAgent; + if (ua.indexOf('Android') > -1 || ua.indexOf('Adr') > -1) { + return 'Android' + } + if (ua.match(/\(i[^;]+;( U;)? CPU.+Mac OS X/)) { + return 'iOS' + } + return 'windows' +})(); + +function _generateGUID() { + var d = new Date().getTime(); + var guid = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) { + var r = (d + Math.random() * 16) % 16 | 0; + d = Math.floor(d / 16); + return (c == 'x' ? r : (r & 0x3 | 0x8)).toString(16); + }) + return guid; +} + +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 _parseAdSlot(bid) { + bid.params.width = 0; + bid.params.height = 0; + // check if size is mentioned in sizes array. in that case do not check for @ in adslot + if (bid.hasOwnProperty('mediaTypes') && + bid.mediaTypes.hasOwnProperty(BANNER) && + bid.mediaTypes.banner.hasOwnProperty('sizes')) { + var i = 0; + var sizeArray = []; + for (; i < bid.mediaTypes.banner.sizes.length; i++) { + if (bid.mediaTypes.banner.sizes[i].length === 2) { // sizes[i].length will not be 2 in case where size is set as fluid, we want to skip that entry + sizeArray.push(bid.mediaTypes.banner.sizes[i]); + } + } + bid.mediaTypes.banner.sizes = sizeArray; + if (bid.mediaTypes.banner.sizes.length >= 1) { + // set the first size in sizes array in bid.params.width and bid.params.height. These will be sent as primary size. + // The rest of the sizes will be sent in format array. + bid.params.width = bid.mediaTypes.banner.sizes[0][0]; + bid.params.height = bid.mediaTypes.banner.sizes[0][1]; + bid.mediaTypes.banner.sizes = bid.mediaTypes.banner.sizes.splice(1, bid.mediaTypes.banner.sizes.length - 1); + } + } +} + +function _initConf(refererInfo) { + return { + pageURL: (refererInfo && refererInfo.referer) ? refererInfo.referer : window.location.href, + refURL: window.document.referrer + }; +} + +function _getLanguage() { + const language = navigator.language ? 'language' : 'userLanguage'; + return navigator[language].split('-')[0]; +} + +function _createOrtbTemplate(conf) { + var guid; + if (storage.getDataFromLocalStorage('adtrue_user_id') == null) { + storage.setDataInLocalStorage('adtrue_user_id', _generateGUID()) + } + guid = storage.getDataFromLocalStorage('adtrue_user_id') + + return { + id: '' + new Date().getTime(), + at: AUCTION_TYPE, + cur: [ADTRUE_CURRENCY], + imp: [], + site: { + page: conf.pageURL, + ref: conf.refURL, + publisher: {} + }, + device: { + ip: '', + ua: navigator.userAgent, + os: platform, + js: 1, + dnt: (navigator.doNotTrack == 'yes' || navigator.doNotTrack == '1' || navigator.msDoNotTrack == '1') ? 1 : 0, + h: screen.height, + w: screen.width, + language: _getLanguage(), + devicetype: _isMobile() ? 1 : _isConnectedTV() ? 3 : 2, + geo: { + country: '{country_code}', + type: 0, + ipservice: 1, + region: '', + city: '', + }, + }, + user: { + id: guid + }, + ext: {} + }; +} + +function _checkParamDataType(key, value, datatype) { + var errMsg = 'Ignoring param key: ' + key + ', expects ' + datatype + ', found ' + typeof value; + var functionToExecute; + switch (datatype) { + case DATA_TYPES.BOOLEAN: + functionToExecute = utils.isBoolean; + break; + case DATA_TYPES.NUMBER: + functionToExecute = utils.isNumber; + break; + case DATA_TYPES.STRING: + functionToExecute = utils.isStr; + break; + case DATA_TYPES.ARRAY: + functionToExecute = utils.isArray; + break; + } + if (functionToExecute(value)) { + return value; + } + utils.logWarn(LOG_WARN_PREFIX + errMsg); + return UNDEFINED; +} + +function _parseNativeResponse(bid, newBid) { + newBid.native = {}; + if (bid.hasOwnProperty('adm')) { + var adm = ''; + try { + adm = JSON.parse(bid.adm.replace(/\\/g, '')); + } catch (ex) { + // utils.logWarn(LOG_WARN_PREFIX + 'Error: Cannot parse native reponse for ad response: ' + newBid.adm); + return; + } + if (adm && adm.native && adm.native.assets && adm.native.assets.length > 0) { + newBid.mediaType = NATIVE; + for (let i = 0, len = adm.native.assets.length; i < len; i++) { + switch (adm.native.assets[i].id) { + case NATIVE_ASSETS.TITLE.ID: + newBid.native.title = adm.native.assets[i].title && adm.native.assets[i].title.text; + break; + case NATIVE_ASSETS.IMAGE.ID: + newBid.native.image = { + url: adm.native.assets[i].img && adm.native.assets[i].img.url, + height: adm.native.assets[i].img && adm.native.assets[i].img.h, + width: adm.native.assets[i].img && adm.native.assets[i].img.w, + }; + break; + case NATIVE_ASSETS.ICON.ID: + newBid.native.icon = { + url: adm.native.assets[i].img && adm.native.assets[i].img.url, + height: adm.native.assets[i].img && adm.native.assets[i].img.h, + width: adm.native.assets[i].img && adm.native.assets[i].img.w, + }; + break; + case NATIVE_ASSETS.SPONSOREDBY.ID: + case NATIVE_ASSETS.BODY.ID: + case NATIVE_ASSETS.LIKES.ID: + case NATIVE_ASSETS.DOWNLOADS.ID: + case NATIVE_ASSETS.PRICE: + case NATIVE_ASSETS.SALEPRICE.ID: + case NATIVE_ASSETS.PHONE.ID: + case NATIVE_ASSETS.ADDRESS.ID: + case NATIVE_ASSETS.DESC2.ID: + case NATIVE_ASSETS.CTA.ID: + case NATIVE_ASSETS.RATING.ID: + case NATIVE_ASSETS.DISPLAYURL.ID: + newBid.native[NATIVE_ASSET_ID_TO_KEY_MAP[adm.native.assets[i].id]] = adm.native.assets[i].data && adm.native.assets[i].data.value; + break; + } + } + newBid.native.clickUrl = adm.native.link && adm.native.link.url; + newBid.native.clickTrackers = (adm.native.link && adm.native.link.clicktrackers) || []; + newBid.native.impressionTrackers = adm.native.imptrackers || []; + newBid.native.jstracker = adm.native.jstracker || []; + if (!newBid.width) { + newBid.width = DEFAULT_WIDTH; + } + if (!newBid.height) { + newBid.height = DEFAULT_HEIGHT; + } + } + } +} + +function _createBannerRequest(bid) { + var sizes = bid.mediaTypes.banner.sizes; + var format = []; + var bannerObj; + if (sizes !== UNDEFINED && utils.isArray(sizes)) { + bannerObj = {}; + if (!bid.params.width && !bid.params.height) { + if (sizes.length === 0) { + // i.e. since bid.params does not have width or height, and length of sizes is 0, need to ignore this banner imp + bannerObj = UNDEFINED; + utils.logWarn(LOG_WARN_PREFIX + 'Error: mediaTypes.banner.size missing for adunit: ' + bid.params.adUnit + '. Ignoring the banner impression in the adunit.'); + return bannerObj; + } else { + bannerObj.w = parseInt(sizes[0][0], 10); + bannerObj.h = parseInt(sizes[0][1], 10); + sizes = sizes.splice(1, sizes.length - 1); + } + } else { + bannerObj.w = bid.params.width; + bannerObj.h = bid.params.height; + } + if (sizes.length > 0) { + format = []; + sizes.forEach(function (size) { + if (size.length > 1) { + format.push({w: size[0], h: size[1]}); + } + }); + if (format.length > 0) { + bannerObj.format = format; + } + } + bannerObj.pos = 0; + bannerObj.topframe = utils.inIframe() ? 0 : 1; + } else { + utils.logWarn(LOG_WARN_PREFIX + 'Error: mediaTypes.banner.size missing for adunit: ' + bid.params.adUnit + '. Ignoring the banner impression in the adunit.'); + bannerObj = UNDEFINED; + } + return bannerObj; +} + +function _createVideoRequest(bid) { + var videoData = bid.params.video; + var videoObj; + + if (videoData !== UNDEFINED) { + videoObj = {}; + for (var key in VIDEO_CUSTOM_PARAMS) { + if (videoData.hasOwnProperty(key)) { + videoObj[key] = _checkParamDataType(key, videoData[key], VIDEO_CUSTOM_PARAMS[key]); + } + } + // read playersize and assign to h and w. + if (utils.isArray(bid.mediaTypes.video.playerSize[0])) { + videoObj.w = parseInt(bid.mediaTypes.video.playerSize[0][0], 10); + videoObj.h = parseInt(bid.mediaTypes.video.playerSize[0][1], 10); + } else if (utils.isNumber(bid.mediaTypes.video.playerSize[0])) { + videoObj.w = parseInt(bid.mediaTypes.video.playerSize[0], 10); + videoObj.h = parseInt(bid.mediaTypes.video.playerSize[1], 10); + } + if (bid.params.video.hasOwnProperty('skippable')) { + videoObj.ext = { + 'video_skippable': bid.params.video.skippable ? 1 : 0 + }; + } + } else { + videoObj = UNDEFINED; + utils.logWarn(LOG_WARN_PREFIX + 'Error: Video config params missing for adunit: ' + bid.params.adUnit + ' with mediaType set as video. Ignoring video impression in the adunit.'); + } + return videoObj; +} + +function _checkMediaType(adm, newBid) { + var admStr = ''; + var videoRegex = new RegExp(/VAST\s+version/); + newBid.mediaType = BANNER; + if (videoRegex.test(adm)) { + newBid.mediaType = VIDEO; + } else { + try { + admStr = JSON.parse(adm.replace(/\\/g, '')); + if (admStr && admStr.native) { + newBid.mediaType = NATIVE; + } + } catch (e) { + utils.logWarn(LOG_WARN_PREFIX + 'Error: Cannot parse native reponse for ad response: ' + adm); + } + } +} + +function _createImpressionObject(bid, conf) { + var impObj = {}; + var bannerObj; + var videoObj; + var sizes = bid.hasOwnProperty('sizes') ? bid.sizes : []; + var mediaTypes = ''; + var format = []; + + impObj = { + id: bid.bidId, + tagid: String(bid.params.zoneId || undefined), + bidfloor: _parseSlotParam('reserve', bid.params.reserve), + secure: 1, + ext: {}, + bidfloorcur: ADTRUE_CURRENCY + }; + + if (bid.hasOwnProperty('mediaTypes')) { + for (mediaTypes in bid.mediaTypes) { + switch (mediaTypes) { + case BANNER: + bannerObj = _createBannerRequest(bid); + if (bannerObj !== UNDEFINED) { + impObj.banner = bannerObj; + } + break; + case VIDEO: + videoObj = _createVideoRequest(bid); + if (videoObj !== UNDEFINED) { + impObj.video = videoObj; + } + break; + } + } + } else { + // mediaTypes is not present, so this is a banner only impression + // this part of code is required for older testcases with no 'mediaTypes' to run succesfully. + bannerObj = { + pos: 0, + w: bid.params.width, + h: bid.params.height, + topframe: utils.inIframe() ? 0 : 1 + }; + if (utils.isArray(sizes) && sizes.length > 1) { + sizes = sizes.splice(1, sizes.length - 1); + sizes.forEach(size => { + format.push({ + w: size[0], + h: size[1] + }); + }); + bannerObj.format = format; + } + impObj.banner = bannerObj; + } + + return impObj.hasOwnProperty(BANNER) || + impObj.hasOwnProperty(NATIVE) || + impObj.hasOwnProperty(VIDEO) ? impObj : UNDEFINED; +} + +export const spec = { + code: BIDDER_CODE, + supportedMediaTypes: ['banner', 'video'], + + isBidRequestValid: function (bid) { + if (bid && bid.params) { + if (!bid.params.zoneId) { + utils.logWarn(LOG_WARN_PREFIX + 'Error: missing zoneId'); + return false; + } + if (!bid.params.publisherId) { + utils.logWarn(LOG_WARN_PREFIX + 'Error: missing publisherId'); + return false; + } + return true; + } + return false; + }, + + buildRequests: function (validBidRequests, bidderRequest) { + let refererInfo; + if (bidderRequest && bidderRequest.refererInfo) { + refererInfo = bidderRequest.refererInfo; + } + let conf = _initConf(refererInfo); + let payload = _createOrtbTemplate(conf); + let bidCurrency = ''; + let bid; + validBidRequests.forEach(originalBid => { + bid = utils.deepClone(originalBid); + _parseAdSlot(bid); + + conf.zoneId = conf.zoneId || bid.params.zoneId; + conf.pubId = conf.pubId || bid.params.publisherId; + + conf.transactionId = bid.transactionId; + if (bidCurrency === '') { + bidCurrency = bid.params.currency || UNDEFINED; + } else if (bid.params.hasOwnProperty('currency') && bidCurrency !== bid.params.currency) { + utils.logWarn(LOG_WARN_PREFIX + 'Currency specifier ignored. Only one currency permitted.'); + } + bid.params.currency = bidCurrency; + + var impObj = _createImpressionObject(bid, conf); + if (impObj) { + payload.imp.push(impObj); + } + }); + if (payload.imp.length == 0) { + return; + } + publisherId = conf.pubId.trim(); + + payload.site.publisher.id = conf.pubId.trim(); + payload.ext.wrapper = {}; + + payload.ext.wrapper.transactionId = conf.transactionId; + payload.ext.wrapper.wiid = conf.wiid || bidderRequest.auctionId; + payload.ext.wrapper.wp = 'pbjs'; + + payload.user.geo = {}; + payload.device.geo = payload.user.geo; + payload.site.page = conf.pageURL; + payload.site.domain = _getDomainFromURL(payload.site.page); + + if (typeof config.getConfig('content') === 'object') { + payload.site.content = config.getConfig('content'); + } + + if (typeof config.getConfig('device') === 'object') { + payload.device = Object.assign(payload.device, config.getConfig('device')); + } + + utils.deepSetValue(payload, 'source.tid', conf.transactionId); + + // test bids + if (window.location.href.indexOf('adtrueTest=true') !== -1) { + payload.test = 1; + } + // adding schain object + if (validBidRequests[0].schain) { + utils.deepSetValue(payload, 'source.ext.schain', validBidRequests[0].schain); + } + + // Attaching GDPR Consent Params + if (bidderRequest && bidderRequest.gdprConsent) { + utils.deepSetValue(payload, 'user.ext.consent', bidderRequest.gdprConsent.consentString); + utils.deepSetValue(payload, 'regs.ext.gdpr', (bidderRequest.gdprConsent.gdprApplies ? 1 : 0)); + } + + // CCPA + if (bidderRequest && bidderRequest.uspConsent) { + utils.deepSetValue(payload, 'regs.ext.us_privacy', bidderRequest.uspConsent); + } + + // coppa compliance + if (config.getConfig('coppa') === true) { + utils.deepSetValue(payload, 'regs.coppa', 1); + } + + return { + method: 'POST', + url: ENDPOINT_URL, + data: JSON.stringify(payload), + bidderRequests: bidderRequest + }; + }, + interpretResponse: function (serverResponses, bidderRequest) { + const bidResponses = []; + var respCur = ADTRUE_CURRENCY; + let parsedRequest = JSON.parse(bidderRequest.data); + let parsedReferrer = parsedRequest.site && parsedRequest.site.ref ? parsedRequest.site.ref : ''; + try { + if (serverResponses.body && serverResponses.body.seatbid && utils.isArray(serverResponses.body.seatbid)) { + // Supporting multiple bid responses for same adSize + respCur = serverResponses.body.cur || respCur; + serverResponses.body.seatbid.forEach(seatbidder => { + seatbidder.bid && + utils.isArray(seatbidder.bid) && + seatbidder.bid.forEach(bid => { + let newBid = { + requestId: bid.impid, + cpm: (parseFloat(bid.price) || 0).toFixed(2), + width: bid.w, + height: bid.h, + creativeId: bid.crid || bid.id, + dealId: bid.dealid, + currency: respCur, + netRevenue: NET_REVENUE, + ttl: 300, + referrer: parsedReferrer, + ad: bid.adm, + partnerImpId: bid.id || '' // partner impression Id + }; + if (parsedRequest.imp && parsedRequest.imp.length > 0) { + parsedRequest.imp.forEach(req => { + if (bid.impid === req.id) { + _checkMediaType(bid.adm, newBid); + switch (newBid.mediaType) { + case BANNER: + break; + case VIDEO: + break; + case NATIVE: + _parseNativeResponse(bid, newBid); + break; + } + } + }); + } + + newBid.meta = {}; + if (bid.ext && bid.ext.dspid) { + newBid.meta.networkId = bid.ext.dspid; + } + if (bid.ext && bid.ext.advid) { + newBid.meta.buyerId = bid.ext.advid; + } + if (bid.adomain && bid.adomain.length > 0) { + newBid.meta.advertiserDomains = bid.adomain; + newBid.meta.clickUrl = bid.adomain[0]; + } + + // adserverTargeting + if (seatbidder.ext && seatbidder.ext.buyid) { + newBid.adserverTargeting = { + 'hb_buyid_adtrue': seatbidder.ext.buyid + }; + } + + bidResponses.push(newBid); + }); + }); + } + } catch (error) { + utils.logError(error); + } + return bidResponses; + }, + getUserSyncs: function (syncOptions, responses, gdprConsent, uspConsent) { + let syncurl = '' + publisherId; + + if (gdprConsent) { + syncurl += '&gdpr=' + (gdprConsent.gdprApplies ? 1 : 0); + syncurl += '&gdpr_consent=' + encodeURIComponent(gdprConsent.consentString || ''); + } + if (uspConsent) { + syncurl += '&us_privacy=' + encodeURIComponent(uspConsent); + } + + // coppa compliance + if (config.getConfig('coppa') === true) { + syncurl += '&coppa=1'; + } + + if (syncOptions.iframeEnabled) { + return [{ + type: 'iframe', + url: USER_SYNC_URL_IFRAME + syncurl + }]; + } else { + return [{ + type: 'image', + url: USER_SYNC_URL_IMAGE + syncurl + }]; + } + } +}; +registerBidder(spec); diff --git a/modules/adtrueBidAdapter.md b/modules/adtrueBidAdapter.md new file mode 100644 index 00000000000..6d0c6f2c125 --- /dev/null +++ b/modules/adtrueBidAdapter.md @@ -0,0 +1,36 @@ +# Overview + +``` +Module Name: AdTrue Bid Adapter +Module Type: Bidder Adapter +Maintainer: ssp@adtrue.com +``` + +# Description + +Connects to AdTrue exchange for bids. +AdTrue bid adapter supports Banner currently. + +# Test Parameters +``` +var adUnits = [ + { + code: 'banner-div', + mediaTypes: { + banner: { + sizes: [[300, 250]] + } + }, + bids: [ + { + bidder: 'adtrue', + params: { + zoneId: '21423', // required, Zone Id provided by AdTrue + publisherId: '1491', // required, Publisher Id provided by AdTrue + reserve: 0.1 // optional + } + } + ] + } +]; +``` diff --git a/modules/adxpremiumAnalyticsAdapter.js b/modules/adxpremiumAnalyticsAdapter.js index 0aa0ca32374..f11e3b8d4e5 100644 --- a/modules/adxpremiumAnalyticsAdapter.js +++ b/modules/adxpremiumAnalyticsAdapter.js @@ -3,6 +3,7 @@ import adapter from '../src/AnalyticsAdapter.js'; import adapterManager from '../src/adapterManager.js'; import CONSTANTS from '../src/constants.json'; import * as utils from '../src/utils.js'; +import includes from 'core-js-pure/features/array/includes.js'; const analyticsType = 'endpoint'; const defaultUrl = 'https://adxpremium.services/graphql'; @@ -211,7 +212,7 @@ function deviceType() { } function clearSlot(elementId) { - if (elementIds.includes(elementId)) { elementIds.splice(elementIds.indexOf(elementId), 1); utils.logInfo('AdxPremium Analytics - Done with: ' + elementId); } + if (includes(elementIds, elementId)) { elementIds.splice(elementIds.indexOf(elementId), 1); utils.logInfo('AdxPremium Analytics - Done with: ' + elementId); } if (elementIds.length == 0 && !requestSent && !timeoutBased) { requestSent = true; sendEvent(completeObject); diff --git a/modules/adyoulikeBidAdapter.js b/modules/adyoulikeBidAdapter.js index 40d3cf84369..308d474d2c4 100644 --- a/modules/adyoulikeBidAdapter.js +++ b/modules/adyoulikeBidAdapter.js @@ -2,6 +2,7 @@ import * as utils 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} from '../src/mediaTypes.js'; const VERSION = '1.0'; const BIDDER_CODE = 'adyoulike'; @@ -9,6 +10,7 @@ const DEFAULT_DC = 'hb-api'; export const spec = { code: BIDDER_CODE, + supportedMediaTypes: [BANNER, NATIVE], aliases: ['ayl'], // short code /** * Determines whether or not the given bid request is valid. @@ -18,10 +20,11 @@ export const spec = { */ isBidRequestValid: function (bid) { const sizes = getSize(getSizeArray(bid)); - if (!bid.params || !bid.params.placement || !sizes.width || !sizes.height) { - return false; - } - return true; + const sizeValid = sizes.width > 0 && sizes.height > 0; + + // allows no size fornative only + return (bid.params && bid.params.placement && + (sizeValid || (bid.mediaTypes && bid.mediaTypes.native))); }, /** * Make a server request from the list of BidRequests. @@ -41,6 +44,7 @@ export const spec = { accumulator[bid.bidId].Width = size.width; accumulator[bid.bidId].Height = size.height; accumulator[bid.bidId].AvailableSizes = sizesArray.join(','); + if (bid.mediaTypes && bid.mediaTypes.native) accumulator[bid.bidId].Native = bid.mediaTypes.native; return accumulator; }, {}), PageRefreshed: getPageRefreshed() @@ -171,9 +175,9 @@ function createEndpointQS(bidderRequest) { } function getSizeArray(bid) { - let inputSize = bid.sizes; + let inputSize = bid.sizes || []; if (bid.mediaTypes && bid.mediaTypes.banner) { - inputSize = bid.mediaTypes.banner.sizes; + inputSize = bid.mediaTypes.banner.sizes || []; } return utils.parseSizesInput(inputSize); @@ -203,34 +207,179 @@ function getSize(sizesArray) { return parsed; } +function getInternalImgUrl(uid) { + if (!uid) return ''; + return 'https://blobs.omnitagjs.com/blobs/' + uid.substr(16, 2) + '/' + uid.substr(16) + '/' + uid; +} + +function getImageUrl(config, resource, width, height) { + let url = ''; + + switch (resource.Kind) { + case 'INTERNAL': + url = getInternalImgUrl(resource.Data.Internal.BlobReference.Uid); + + break; + + case 'EXTERNAL': + const dynPrefix = config.DynamicPrefix; + let extUrl = resource.Data.External.Url; + extUrl = extUrl.replace(/\[height\]/i, '' + height); + extUrl = extUrl.replace(/\[width\]/i, '' + width); + + if (extUrl.indexOf(dynPrefix) >= 0) { + const urlmatch = (/.*url=([^&]*)/gm).exec(extUrl); + url = urlmatch ? urlmatch[1] : ''; + if (!url) { + url = getInternalImgUrl((/.*key=([^&]*)/gm).exec(extUrl)[1]); + } + } else { + url = extUrl; + } + + break; + } + + return url; +} + +function getTrackers(eventsArray, jsTrackers) { + const result = []; + + if (!eventsArray) return result; + + eventsArray.map((item, index) => { + if ((jsTrackers && item.Kind === 'JAVASCRIPT_URL') || + (!jsTrackers && item.Kind === 'PIXEL_URL')) { + result.push(item.Url); + } + }); + return result; +} + +function getNativeAssets(response, nativeConfig) { + const native = {}; + + var adJson = {}; + var textsJson = {}; + if (typeof response.Ad === 'string') { + adJson = JSON.parse(response.Ad.match(/\/\*PREBID\*\/(.*)\/\*PREBID\*\//)[1]); + textsJson = adJson.Content.Preview.Text; + + var impressionUrl = adJson.TrackingPrefix + + '/pixel?event_kind=IMPRESSION&attempt=' + adJson.Attempt; + + if (adJson.Campaign) { + impressionUrl += '&campaign=' + adJson.Campaign; + } + + native.clickUrl = adJson.TrackingPrefix + '/ar?event_kind=CLICK&attempt=' + adJson.Attempt + + '&campaign=' + adJson.Campaign + '&url=' + encodeURIComponent(adJson.Content.Landing.Url); + + native.clickTrackers = getTrackers(adJson.OnEvents['CLICK']); + native.impressionTrackers = getTrackers(adJson.OnEvents['IMPRESSION']); + native.impressionTrackers.push(impressionUrl); + native.javascriptTrackers = getTrackers(adJson.OnEvents['IMPRESSION'], true); + } + + Object.keys(nativeConfig).map(function(key, index) { + if (typeof response.Native === 'object') { + native[key] = response.Native[key]; + } else { + switch (key) { + case 'title': + native[key] = textsJson.TITLE; + break; + case 'body': + native[key] = textsJson.DESCRIPTION; + break; + case 'cta': + native[key] = textsJson.CALLTOACTION; + break; + case 'sponsoredBy': + native[key] = adJson.Content.Preview.Sponsor.Name; + break; + case 'image': + // main image requested size + const imgSize = nativeConfig.image.sizes || []; + if (!imgSize.length) { + imgSize[0] = response.Width || 300; + imgSize[1] = response.Height || 250; + } + + native[key] = { + url: getImageUrl(adJson, adJson.Content.Preview.Thumbnail.Image, imgSize[0], imgSize[1]), + width: imgSize[0], + height: imgSize[1] + }; + break; + case 'icon': + if (adJson.HasSponsorImage) { + // icon requested size + const iconSize = nativeConfig.icon.sizes || []; + if (!iconSize.length) { + iconSize[0] = 50; + iconSize[1] = 50; + } + + native[key] = { + url: getImageUrl(adJson, adJson.Content.Preview.Sponsor.Logo.Resource, iconSize[0], iconSize[1]), + width: iconSize[0], + height: iconSize[1] + }; + } + break; + case 'privacyIcon': + native[key] = getImageUrl(adJson, adJson.Content.Preview.Credit.Logo.Resource, 25, 25); + break; + case 'privacyLink': + native[key] = adJson.Content.Preview.Credit.Url; + break; + } + } + }); + + return native; +} + /* Create bid from response */ function createBid(response, bidRequests) { - if (!response || !response.Ad) { + if (!response || (!response.Ad && !response.Native)) { return } + const request = bidRequests && bidRequests[response.BidID]; + // In case we don't retreive the size from the adserver, use the given one. - if (bidRequests && bidRequests[response.BidID]) { + if (request) { if (!response.Width || response.Width === '0') { - response.Width = bidRequests[response.BidID].Width; + response.Width = request.Width; } if (!response.Height || response.Height === '0') { - response.Height = bidRequests[response.BidID].Height; + response.Height = request.Height; } } - return { + const bid = { requestId: response.BidID, - width: response.Width, - height: response.Height, - ad: response.Ad, ttl: 3600, creativeId: response.CreativeID, cpm: response.Price, netRevenue: true, currency: 'USD' }; + + if (request && request.Native) { + bid.native = getNativeAssets(response, request.Native); + bid.mediaType = 'native'; + } else { + bid.width = response.Width; + bid.height = response.Height; + bid.ad = response.Ad; + } + + return bid; } registerBidder(spec); diff --git a/modules/adyoulikeBidAdapter.md b/modules/adyoulikeBidAdapter.md index 120bee3bcbb..d0e7fa8883b 100644 --- a/modules/adyoulikeBidAdapter.md +++ b/modules/adyoulikeBidAdapter.md @@ -11,19 +11,49 @@ Banner formats are supported. # Test Parameters ``` - var adUnits = [ - { - code: 'test-div', - sizes: [[300, 250]], - bids: [ - { - bidder: "adyoulike", - params: { - placement: 194f787b85c829fb8822cdaf1ae64435, - DC: 'fra01', // Optional for set the data center name - } - } - ] - } - ]; + var adUnits = { + "code": "test-div", + "mediaTypes": { + "banner": { + "sizes": ["300x250"] + }, + "native": { + "image": { + "required": true, + }, + "title": { + "required": true, + "len": 80 + }, + "cta": { + "required": false + }, + "sponsoredBy": { + "required": true + }, + "clickUrl": { + "required": true + }, + "privacyIcon": { + "required": false + }, + "privacyLink": { + "required": false + }, + "body": { + "required": true + }, + "icon": { + "required": true, + "sizes": [] + } + } + bids: [{ + bidder: "adyoulike", + params: { + placement: 194 f787b85c829fb8822cdaf1ae64435, + DC: "fra01", // Optional for set the data center name + } + }] + }; ``` diff --git a/modules/appnexusBidAdapter.js b/modules/appnexusBidAdapter.js index 203835db611..6feec56e919 100644 --- a/modules/appnexusBidAdapter.js +++ b/modules/appnexusBidAdapter.js @@ -107,7 +107,19 @@ export const spec = { .filter(param => includes(USER_PARAMS, param)) .forEach((param) => { let uparam = utils.convertCamelToUnderscore(param); - userObj[uparam] = userObjBid.params.user[param] + if (param === 'segments' && utils.isArray(userObjBid.params.user[param])) { + let segs = []; + userObjBid.params.user[param].forEach(val => { + if (utils.isNumber(val)) { + segs.push({'id': val}); + } else if (utils.isPlainObject(val)) { + segs.push(val); + } + }); + userObj[uparam] = segs; + } else if (param !== 'segments') { + userObj[uparam] = userObjBid.params.user[param]; + } }); } @@ -228,26 +240,17 @@ export const spec = { }); } - const criteoId = utils.deepAccess(bidRequests[0], `userId.criteoId`); - let eids = []; - if (criteoId) { - eids.push({ - source: 'criteo.com', - id: criteoId - }); - } + if (bidRequests[0].userId) { + let eids = []; - const tdid = utils.deepAccess(bidRequests[0], `userId.tdid`); - if (tdid) { - eids.push({ - source: 'adserver.org', - id: tdid, - rti_partner: 'TDID' - }); - } + addUserId(eids, utils.deepAccess(bidRequests[0], `userId.criteoId`), 'criteo.com', null); + addUserId(eids, utils.deepAccess(bidRequests[0], `userId.netId`), 'netid.de', null); + addUserId(eids, utils.deepAccess(bidRequests[0], `userId.idl_env`), 'liveramp.com', null); + addUserId(eids, utils.deepAccess(bidRequests[0], `userId.tdid`), 'adserver.org', 'TDID'); - if (eids.length) { - payload.eids = eids; + if (eids.length) { + payload.eids = eids; + } } if (tags[0].publisher_id) { @@ -486,6 +489,12 @@ function formatRequest(payload, bidderRequest) { } } + if (utils.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 = utils.deepClone(payload); @@ -973,9 +982,22 @@ function hidedfpContainer(elementId) { } } +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) { - // push to render queue because ANOutstreamVideo may not be loaded yet 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, @@ -1003,4 +1025,15 @@ function parseMediaType(rtbBid) { } } +function addUserId(eids, id, source, rti) { + if (id) { + if (rti) { + eids.push({ source, id, rti_partner: rti }); + } else { + eids.push({ source, id }); + } + } + return eids; +} + registerBidder(spec); diff --git a/modules/astraoneBidAdapter.js b/modules/astraoneBidAdapter.js index 7e98c1022d2..2fec3892d27 100644 --- a/modules/astraoneBidAdapter.js +++ b/modules/astraoneBidAdapter.js @@ -31,7 +31,9 @@ function buildBid(bidData) { creativeId: bidData.content.seanceId, currency: bidData.currency, netRevenue: true, - mediaType: BANNER, + meta: { + mediaType: BANNER, + }, ttl: TTL, content: bidData.content }; @@ -62,7 +64,7 @@ function wrapAd(bid, bidData) { parentDocument.style.height = "100%"; parentDocument.style.width = "100%"; } - var _html = "${encodeURIComponent(JSON.stringify(bid))}"; + var _html = "${encodeURIComponent(JSON.stringify({...bid, content: bidData.content}))}"; window._ao_ssp.registerInImage(JSON.parse(decodeURIComponent(_html))); @@ -126,14 +128,9 @@ export const spec = { * @return {Bid[]} An array of bids which were nested inside the server. */ interpretResponse: function(serverResponse) { - const serverBody = serverResponse.body; - if (serverBody && utils.isArray(serverBody)) { - return utils._map(serverBody, function(bid) { - return buildBid(bid); - }); - } else { - return []; - } + const bids = serverResponse.body && serverResponse.body.bids; + + return Array.isArray(bids) ? bids.map(bid => buildBid(bid)) : [] } } diff --git a/modules/astraoneBidAdapter.md b/modules/astraoneBidAdapter.md index a7eaeeef5a4..e090cfe1e54 100644 --- a/modules/astraoneBidAdapter.md +++ b/modules/astraoneBidAdapter.md @@ -18,17 +18,17 @@ About us: https://astraone.io var adUnits = [{ code: 'test-div', mediaTypes: { - banner: { - sizes: [1, 1] - } + banner: { + sizes: [1, 1], + } }, bids: [{ - bidder: "astraone", - params: { - placement: "inImage", - placeId: "5af45ad34d506ee7acad0c26", - imageUrl: "https://creative.astraone.io/files/default_image-1-600x400.jpg" - } + bidder: "astraone", + params: { + placement: "inImage", + placeId: "5f477bf94d506ebe2c4240f3", + imageUrl: "https://creative.astraone.io/files/default_image-1-600x400.jpg" + } }] }]; ``` @@ -39,66 +39,69 @@ var adUnits = [{ - - Prebid.js Banner Example - - + + }, + bids: [{ + bidder: "astraone", + params: { + placement: "inImage", + placeId: "5f477bf94d506ebe2c4240f3", + imageUrl: "https://creative.astraone.io/files/default_image-1-600x400.jpg" + } + }] + }]; + + var pbjs = pbjs || {}; + pbjs.que = pbjs.que || []; + + pbjs.que.push(function() { + pbjs.addAdUnits(adUnits); + pbjs.requestBids({ + bidsBackHandler: function (e) { + if (pbjs.adserverRequestSent) return; + pbjs.adserverRequestSent = true; + var params = pbjs.getAdserverTargetingForAdUnitCode("test-div"); + var iframe = document.getElementById('test-div'); + + if (params && params['hb_adid']) { + iframe.parentElement.style.position = "relative"; + iframe.style.display = "block"; + pbjs.renderAd(iframe.contentDocument, params['hb_adid']); + } + } + }); + }); + -

Prebid.js InImage Banner Test

+

Prebid.js InImage Banner Test

-
- - -
+
+ + +
@@ -109,90 +112,91 @@ var adUnits = [{ - - Prebid.js Banner Example - - - - + + Prebid.js Banner gpt Example + + + + -

Prebid.js Banner Ad Unit Test

+

Prebid.js InImage Banner gpt Test

-
- +
+ - -
+ +
``` diff --git a/modules/atsAnalyticsAdapter.js b/modules/atsAnalyticsAdapter.js index a5bb3797bbf..31e46dead89 100644 --- a/modules/atsAnalyticsAdapter.js +++ b/modules/atsAnalyticsAdapter.js @@ -3,26 +3,235 @@ import CONSTANTS from '../src/constants.json'; import adaptermanager from '../src/adapterManager.js'; import * as utils from '../src/utils.js'; import {ajax} from '../src/ajax.js'; +import {getStorageManager} from '../src/storageManager.js'; + +export const storage = getStorageManager(); + +/** + * Analytics adapter for - https://liveramp.com + * Maintainer - prebid@liveramp.com + */ const analyticsType = 'endpoint'; +// dev endpoints +// const preflightUrl = 'https://analytics-check.publishersite.xyz/check/'; +// export const analyticsUrl = 'https://analyticsv2.publishersite.xyz'; + +const preflightUrl = 'https://check.analytics.rlcdn.com/check/'; +export const analyticsUrl = 'https://analytics.rlcdn.com'; let handlerRequest = []; let handlerResponse = []; -let host = ''; + +let atsAnalyticsAdapterVersion = 1; + +let browsersList = [ + /* Googlebot */ + { + test: /googlebot/i, + name: 'Googlebot' + }, + + /* Opera < 13.0 */ + { + test: /opera/i, + name: 'Opera', + }, + + /* Opera > 13.0 */ + { + test: /opr\/|opios/i, + name: 'Opera' + }, + { + test: /SamsungBrowser/i, + name: 'Samsung Internet for Android', + }, + { + test: /Whale/i, + name: 'NAVER Whale Browser', + }, + { + test: /MZBrowser/i, + name: 'MZ Browser' + }, + { + test: /focus/i, + name: 'Focus', + }, + { + test: /swing/i, + name: 'Swing', + }, + { + test: /coast/i, + name: 'Opera Coast', + }, + { + test: /opt\/\d+(?:.?_?\d+)+/i, + name: 'Opera Touch', + }, + { + test: /yabrowser/i, + name: 'Yandex Browser', + }, + { + test: /ucbrowser/i, + name: 'UC Browser', + }, + { + test: /Maxthon|mxios/i, + name: 'Maxthon', + }, + { + test: /epiphany/i, + name: 'Epiphany', + }, + { + test: /puffin/i, + name: 'Puffin', + }, + { + test: /sleipnir/i, + name: 'Sleipnir', + }, + { + test: /k-meleon/i, + name: 'K-Meleon', + }, + { + test: /micromessenger/i, + name: 'WeChat', + }, + { + test: /qqbrowser/i, + name: (/qqbrowserlite/i).test(window.navigator.userAgent) ? 'QQ Browser Lite' : 'QQ Browser', + }, + { + test: /msie|trident/i, + name: 'Internet Explorer', + }, + { + test: /\sedg\//i, + name: 'Microsoft Edge', + }, + { + test: /edg([ea]|ios)/i, + name: 'Microsoft Edge', + }, + { + test: /vivaldi/i, + name: 'Vivaldi', + }, + { + test: /seamonkey/i, + name: 'SeaMonkey', + }, + { + test: /sailfish/i, + name: 'Sailfish', + }, + { + test: /silk/i, + name: 'Amazon Silk', + }, + { + test: /phantom/i, + name: 'PhantomJS', + }, + { + test: /slimerjs/i, + name: 'SlimerJS', + }, + { + test: /blackberry|\bbb\d+/i, + name: 'BlackBerry', + }, + { + test: /(web|hpw)[o0]s/i, + name: 'WebOS Browser', + }, + { + test: /bada/i, + name: 'Bada', + }, + { + test: /tizen/i, + name: 'Tizen', + }, + { + test: /qupzilla/i, + name: 'QupZilla', + }, + { + test: /firefox|iceweasel|fxios/i, + name: 'Firefox', + }, + { + test: /electron/i, + name: 'Electron', + }, + { + test: /MiuiBrowser/i, + name: 'Miui', + }, + { + test: /chromium/i, + name: 'Chromium', + }, + { + test: /chrome|crios|crmo/i, + name: 'Chrome', + }, + { + test: /GSA/i, + name: 'Google Search', + }, + + /* Android Browser */ + { + test: /android/i, + name: 'Android Browser', + }, + + /* PlayStation 4 */ + { + test: /playstation 4/i, + name: 'PlayStation 4', + }, + + /* Safari */ + { + test: /safari|applewebkit/i, + name: 'Safari', + }, +]; + +function setSamplingCookie(samplRate) { + let now = new Date(); + now.setTime(now.getTime() + 3600000); + storage.setCookie('_lr_sampling_rate', samplRate, now.toUTCString()); +} + +let listOfSupportedBrowsers = ['Safari', 'Chrome', 'Firefox', 'Microsoft Edge']; function bidRequestedHandler(args) { + let envelopeSourceCookieValue = storage.getCookie('_lr_env_src_ats'); + let envelopeSource = envelopeSourceCookieValue === 'true'; let requests; requests = args.bids.map(function(bid) { return { + envelope_source: envelopeSource, has_envelope: bid.userId ? !!bid.userId.idl_env : false, bidder: bid.bidder, bid_id: bid.bidId, auction_id: args.auctionId, - user_browser: checkUserBrowser(), + user_browser: parseBrowser(), user_platform: navigator.platform, auction_start: new Date(args.auctionStart).toJSON(), domain: window.location.hostname, pid: atsAnalyticsAdapter.context.pid, + adapter_version: atsAnalyticsAdapterVersion }; }); return requests; @@ -38,58 +247,44 @@ function bidResponseHandler(args) { }; } -export function checkUserBrowser() { - let firefox = browserIsFirefox(); - let chrome = browserIsChrome(); - let edge = browserIsEdge(); - let safari = browserIsSafari(); - if (firefox) { - return firefox; - } if (chrome) { - return chrome; - } if (edge) { - return edge; - } if (safari) { - return safari; - } else { - return 'Unknown' +export function parseBrowser() { + let ua = atsAnalyticsAdapter.getUserAgent(); + try { + let result = browsersList.filter(function(obj) { + return obj.test.test(ua); + }); + let browserName = result && result.length ? result[0].name : ''; + return (listOfSupportedBrowsers.indexOf(browserName) >= 0) ? browserName : 'Unknown'; + } catch (err) { + utils.logError('ATS Analytics - Error while checking user browser!', err); } } -export function browserIsFirefox() { - if (typeof InstallTrigger !== 'undefined') { - return 'Firefox'; - } else { - return false; +function sendDataToAnalytic () { + // send data to ats analytic endpoint + try { + let dataToSend = {'Data': atsAnalyticsAdapter.context.events}; + let strJSON = JSON.stringify(dataToSend); + utils.logInfo('ATS Analytics - tried to send analytics data!'); + ajax(analyticsUrl, function () { + }, strJSON, {method: 'POST', contentType: 'application/json'}); + } catch (err) { + utils.logError('ATS Analytics - request encounter an error: ', err); } } -export function browserIsIE() { - return !!document.documentMode; -} - -export function browserIsEdge() { - if (!browserIsIE() && !!window.StyleMedia) { - return 'Edge'; - } else { - return false; - } -} - -export function browserIsChrome() { - if ((!!window.chrome && (!!window.chrome.webstore || !!window.chrome.runtime)) || (/Android/i.test(navigator.userAgent) && !!window.chrome)) { - return 'Chrome'; - } else { - return false; - } -} - -export function browserIsSafari() { - if (window.safari !== undefined) { - return 'Safari' - } else { - return false; - } +// preflight request, to check did publisher have permission to send data to analytics endpoint +function preflightRequest (envelopeSourceCookieValue) { + ajax(preflightUrl + atsAnalyticsAdapter.context.pid, function (data) { + let samplingRateObject = JSON.parse(data); + utils.logInfo('ATS Analytics - Sampling Rate: ', samplingRateObject); + let samplingRate = samplingRateObject['samplingRate']; + setSamplingCookie(samplingRate); + let samplingRateNumber = Number(samplingRate); + if (data && samplingRate && atsAnalyticsAdapter.shouldFireRequest(samplingRateNumber) && envelopeSourceCookieValue != null) { + sendDataToAnalytic(); + } + }, undefined, { method: 'GET', crossOrigin: true }); } function callHandler(evtype, args) { @@ -117,7 +312,6 @@ function callHandler(evtype, args) { let atsAnalyticsAdapter = Object.assign(adapter( { - host, analyticsType }), { @@ -126,16 +320,19 @@ let atsAnalyticsAdapter = Object.assign(adapter( callHandler(eventType, args); } if (eventType === CONSTANTS.EVENTS.AUCTION_END) { - if (atsAnalyticsAdapter.shouldFireRequest()) { - // send data to ats analytic endpoint - try { - let dataToSend = {'Data': atsAnalyticsAdapter.context.events}; - let strJSON = JSON.stringify(dataToSend); - utils.logInfo('atsAnalytics tried to send analytics data!'); - ajax(atsAnalyticsAdapter.context.host, function () { - }, strJSON, {method: 'POST', contentType: 'application/json'}); - } catch (err) { + let envelopeSourceCookieValue = storage.getCookie('_lr_env_src_ats'); + try { + utils.logInfo('ATS Analytics - preflight request!'); + let samplingRateCookie = storage.getCookie('_lr_sampling_rate'); + if (!samplingRateCookie) { + preflightRequest(envelopeSourceCookieValue); + } else { + if (atsAnalyticsAdapter.shouldFireRequest(parseInt(samplingRateCookie)) && envelopeSourceCookieValue != null) { + sendDataToAnalytic(); + } } + } catch (err) { + utils.logError('ATS Analytics - preflight request encounter an error: ', err); } } } @@ -145,26 +342,23 @@ let atsAnalyticsAdapter = Object.assign(adapter( atsAnalyticsAdapter.originEnableAnalytics = atsAnalyticsAdapter.enableAnalytics; // add check to not fire request every time, but instead to send 1/10 events -atsAnalyticsAdapter.shouldFireRequest = function () { - return (Math.floor((Math.random() * 11)) === 10); -} +atsAnalyticsAdapter.shouldFireRequest = function (samplingRate) { + let shouldFireRequestValue = (Math.floor((Math.random() * samplingRate + 1)) === samplingRate); + utils.logInfo('ATS Analytics - Should Fire Request: ', shouldFireRequestValue); + return shouldFireRequestValue; +}; +atsAnalyticsAdapter.getUserAgent = function () { + return window.navigator.userAgent; +}; // override enableAnalytics so we can get access to the config passed in from the page atsAnalyticsAdapter.enableAnalytics = function (config) { if (!config.options.pid) { - utils.logError('Publisher ID (pid) option is not defined. Analytics won\'t work'); + utils.logError('ATS Analytics - Publisher ID (pid) option is not defined. Analytics won\'t work'); return; } - - if (!config.options.host) { - utils.logError('Host option is not defined. Analytics won\'t work'); - return; - } - - host = config.options.host; atsAnalyticsAdapter.context = { events: [], - host: config.options.host, pid: config.options.pid }; let initOptions = config.options; diff --git a/modules/atsAnalyticsAdapter.md b/modules/atsAnalyticsAdapter.md index 560ad237aa0..7c634f39ae2 100644 --- a/modules/atsAnalyticsAdapter.md +++ b/modules/atsAnalyticsAdapter.md @@ -17,7 +17,6 @@ Analytics adapter for Authenticated Traffic Solution(ATS), provided by LiveRamp. provider: 'atsAnalytics', options: { pid: '999', // publisher ID - host: 'https://example.com' // host is provided to publisher } } ``` diff --git a/modules/axonixBidAdapter.js b/modules/axonixBidAdapter.js new file mode 100644 index 00000000000..161ea8c6715 --- /dev/null +++ b/modules/axonixBidAdapter.js @@ -0,0 +1,178 @@ +import { registerBidder } from '../src/adapters/bidderFactory.js'; +import { BANNER, VIDEO } from '../src/mediaTypes.js'; +import { config } from '../src/config.js'; +import * as utils from '../src/utils.js'; +import { ajax } from '../src/ajax.js'; + +const BIDDER_CODE = 'axonix'; +const BIDDER_VERSION = '1.0.0'; + +const CURRENCY = 'USD'; +const DEFAULT_REGION = 'us-east-1'; + +function getBidFloor(bidRequest) { + let floorInfo = {}; + + if (typeof bidRequest.getFloor === 'function') { + floorInfo = bidRequest.getFloor({ + currency: CURRENCY, + mediaType: '*', + size: '*' + }); + } + + return floorInfo.floor || 0; +} + +function getPageUrl(bidRequest, bidderRequest) { + let pageUrl = config.getConfig('pageUrl'); + + if (bidRequest.params.referrer) { + pageUrl = bidRequest.params.referrer; + } else if (!pageUrl) { + pageUrl = bidderRequest.refererInfo.referer; + } + + return bidRequest.params.secure ? pageUrl.replace(/^http:/i, 'https:') : pageUrl; +} + +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 getURL(params, path) { + let { supplyId, region, endpoint } = params; + let url; + + if (endpoint) { + url = endpoint; + } else if (region) { + url = `https://openrtb-${region}.axonix.com/supply/${path}/${supplyId}`; + } else { + url = `https://openrtb-${DEFAULT_REGION}.axonix.com/supply/${path}/${supplyId}` + } + + return url; +} + +export const spec = { + code: BIDDER_CODE, + version: BIDDER_VERSION, + supportedMediaTypes: [BANNER, VIDEO], + + isBidRequestValid: function(bid) { + // video bid request validation + if (bid.hasOwnProperty('mediaTypes') && bid.mediaTypes.hasOwnProperty(VIDEO)) { + if (!bid.mediaTypes[VIDEO].hasOwnProperty('mimes') || + !utils.isArray(bid.mediaTypes[VIDEO].mimes) || + bid.mediaTypes[VIDEO].mimes.length === 0) { + utils.logError('mimes are mandatory for video bid request. Ad Unit: ', JSON.stringify(bid)); + + return false; + } + } + + return !!(bid.params && bid.params.supplyId); + }, + + buildRequests: function(validBidRequests, bidderRequest) { + // device.connectiontype + let connection = navigator.connection || navigator.webkitConnection; + let connectiontype = 'unknown'; + + if (connection && connection.effectiveType) { + connectiontype = connection.effectiveType; + } + + const requests = validBidRequests.map(validBidRequest => { + // app/site + let app; + let site; + + if (typeof config.getConfig('app') === 'object') { + app = config.getConfig('app'); + } else { + site = { + page: getPageUrl(validBidRequest, bidderRequest) + } + } + + const data = { + app, + site, + validBidRequest, + connectiontype, + devicetype: isMobile() ? 1 : isConnectedTV() ? 3 : 2, + bidfloor: getBidFloor(validBidRequest), + dnt: (navigator.doNotTrack === 'yes' || navigator.doNotTrack === '1' || navigator.msDoNotTrack === '1') ? 1 : 0, + language: navigator.language, + prebidVersion: '$prebid.version$', + screenHeight: screen.height, + screenWidth: screen.width, + tmax: config.getConfig('bidderTimeout'), + ua: navigator.userAgent, + }; + + return { + method: 'POST', + url: getURL(validBidRequest.params, 'prebid'), + options: { + withCredentials: false, + contentType: 'application/json' + }, + data + }; + }); + + return requests; + }, + + interpretResponse: function(serverResponse) { + if (!utils.isArray(serverResponse)) { + return []; + } + + const responses = []; + + for (const response of serverResponse) { + if (response.requestId) { + responses.push(Object.assign(response, { + ttl: config.getConfig('_bidderTimeout') + })); + } + } + + return responses; + }, + + onTimeout: function(timeoutData) { + const params = utils.deepAccess(timeoutData, '0.params.0'); + + if (!utils.isEmpty(params)) { + ajax(getURL(params, 'prebid/timeout'), null, timeoutData[0], { + method: 'POST', + options: { + withCredentials: false, + contentType: 'application/json' + } + }); + } + }, + + onBidWon: function(bids) { + for (const bid of bids) { + const { nurl } = bid || {}; + + if (bid.nurl) { + utils.replaceAuctionPrice(nurl, bid.cpm) + utils.triggerPixel(nurl); + }; + } + } +} + +registerBidder(spec); diff --git a/modules/axonixBidAdapter.md b/modules/axonixBidAdapter.md new file mode 100644 index 00000000000..1ff59f17828 --- /dev/null +++ b/modules/axonixBidAdapter.md @@ -0,0 +1,140 @@ +# Overview + +``` +Module Name : Axonix Bidder Adapter +Module Type : Bidder Adapter +Maintainer : support-prebid@axonix.com +``` + +# Description + +Module that connects to Axonix's exchange for bids. + +# Parameters + +| Name | Scope | Description | Example | +| :------------ | :------- | :---------------------------------------------- | :------------------------------------- | +| `supplyId` | required | Supply UUID | `"2c426f78-bb18-4a16-abf4-62c6cd0ee8de"` | +| `region` | optional | Cloud region | `"us-east-1"` | +| `endpoint` | optional | Supply custom endpoint | `"https://open-rtb.axonix.com/custom"` | +| `instl` | optional | Set to 1 if using interstitial (default: 0) | `1` | + +# Test Parameters + +## Banner + +```javascript +var bannerAdUnit = { + code: 'test-banner', + mediaTypes: { + banner: { + sizes: [[120, 600], [300, 250], [320, 50], [468, 60], [728, 90]] + } + }, + bids: [{ + bidder: 'axonix', + params: { + supplyId: 'abc', + region: 'def', + endpoint: 'url' + } + }] +}; +``` + +## Video + +```javascript +var videoAdUnit = { + code: 'test-video', + mediaTypes: { + video: { + protocols: [1, 2, 3, 4, 5, 6, 7, 8] + } + }, + bids: [{ + bidder: 'axonix', + params: { + supplyId: 'abc', + region: 'def', + endpoint: 'url' + } + }] +}; +``` + +## Native + +```javascript +var nativeAdUnit = { + code: 'test-native', + mediaTypes: { + native: { + + } + }, + bids: [{ + bidder: 'axonix', + params: { + supplyId: 'abc', + region: 'def', + endpoint: 'url' + } + }] +}; +``` + +## Multiformat + +```javascript +var adUnits = [ +{ + code: 'test-banner', + mediaTypes: { + banner: { + sizes: [[120, 600], [300, 250], [320, 50], [468, 60], [728, 90]] + } + }, + bids: [{ + bidder: 'axonix', + params: { + supplyId: 'abc', + region: 'def', + endpoint: 'url' + } + }] +}, +{ + code: 'test-video', + mediaTypes: { + video: { + protocols: [1, 2, 3, 4, 5, 6, 7, 8] + } + }, + bids: [{ + bidder: 'axonix', + params: { + supplyId: 'abc', + region: 'def', + endpoint: 'url' + } + }] +}, +{ + code: 'test-native', + mediaTypes: { + native: { + + } + }, + bids: [{ + bidder: 'axonix', + params: { + supplyId: 'abc', + region: 'def', + endpoint: 'url' + } + }] +} +]; +``` diff --git a/modules/beachfrontBidAdapter.js b/modules/beachfrontBidAdapter.js index 4b30f47e2cf..44755a78864 100644 --- a/modules/beachfrontBidAdapter.js +++ b/modules/beachfrontBidAdapter.js @@ -6,7 +6,7 @@ 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.14'; +const ADAPTER_VERSION = '1.15'; const ADAPTER_NAME = 'BFIO_PREBID'; const OUTSTREAM = 'outstream'; @@ -387,6 +387,7 @@ function createBannerRequestData(bids, bidderRequest) { slot: bid.adUnitCode, id: getBannerBidParam(bid, 'appId'), bidfloor: getBannerBidParam(bid, 'bidfloor'), + tagid: getBannerBidParam(bid, 'tagid'), sizes: getBannerSizes(bid) }; }); diff --git a/modules/betweenBidAdapter.js b/modules/betweenBidAdapter.js index 0ed05717391..f445c9dfd5d 100644 --- a/modules/betweenBidAdapter.js +++ b/modules/betweenBidAdapter.js @@ -3,6 +3,7 @@ import { getAdUnitSizes, parseSizesInput } from '../src/utils.js'; import { getRefererInfo } from '../src/refererDetection.js'; const BIDDER_CODE = 'between'; +const ENDPOINT = 'https://ads.betweendigital.com/adjson?t=prebid'; export const spec = { code: BIDDER_CODE, @@ -30,7 +31,7 @@ export const spec = { validBidRequests.forEach(i => { let params = { - sizes: parseSizesInput(getAdUnitSizes(i)).join('%2C'), + sizes: parseSizesInput(getAdUnitSizes(i)), jst: 'hb', ord: Math.random() * 10000000000000000, tz: getTz(), @@ -74,9 +75,14 @@ export const spec = { } } - requests.push({method: 'GET', url: 'https://ads.betweendigital.com/adjson', data: params}) + requests.push({data: params}) }) - return requests; + return { + method: 'POST', + url: ENDPOINT, + data: JSON.stringify(requests) + } + // return requests; }, /** * Unpack the response from the server into a list of bids. diff --git a/modules/bidViewability.js b/modules/bidViewability.js new file mode 100644 index 00000000000..c3b72cda8d4 --- /dev/null +++ b/modules/bidViewability.js @@ -0,0 +1,97 @@ +// This module, when included, will trigger a BID_VIEWABLE event which can be consumed by Bidders and Analytics adapters +// 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 * as events from '../src/events.js'; +import { EVENTS } 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'; + +const MODULE_NAME = 'bidViewability'; +const CONFIG_ENABLED = 'enabled'; +const CONFIG_FIRE_PIXELS = 'firePixels'; +const CONFIG_CUSTOM_MATCH = 'customMatchFunction'; +const BID_VURL_ARRAY = 'vurls'; +const GPT_IMPRESSION_VIEWABLE_EVENT = 'impressionViewable'; + +export let isBidAdUnitCodeMatchingSlot = (bid, slot) => { + return (slot.getAdUnitPath() === bid.adUnitCode || slot.getSlotElementId() === bid.adUnitCode); +} + +export let getMatchingWinningBidForGPTSlot = (globalModuleConfig, slot) => { + return find(getGlobal().getAllWinningBids(), + // supports custom match function from config + bid => isFn(globalModuleConfig[CONFIG_CUSTOM_MATCH]) + ? globalModuleConfig[CONFIG_CUSTOM_MATCH](bid, slot) + : isBidAdUnitCodeMatchingSlot(bid, slot) + ) || null; +}; + +export let fireViewabilityPixels = (globalModuleConfig, bid) => { + if (globalModuleConfig[CONFIG_FIRE_PIXELS] === true && bid.hasOwnProperty(BID_VURL_ARRAY)) { + let queryParams = {}; + + const gdprConsent = gdprDataHandler.getConsentData(); + if (gdprConsent) { + if (typeof gdprConsent.gdprApplies === 'boolean') { queryParams.gdpr = Number(gdprConsent.gdprApplies); } + if (gdprConsent.consentString) { queryParams.gdpr_consent = gdprConsent.consentString; } + if (gdprConsent.addtlConsent) { queryParams.addtl_consent = gdprConsent.addtlConsent; } + } + + const uspConsent = uspDataHandler.getConsentData(); + if (uspConsent) { queryParams.us_privacy = uspConsent; } + + bid[BID_VURL_ARRAY].forEach(url => { + // add '?' if not present in URL + if (Object.keys(queryParams).length > 0 && url.indexOf('?') === -1) { + url += '?'; + } + // append all query params, `&key=urlEncoded(value)` + url += Object.keys(queryParams).reduce((prev, key) => prev += `&${key}=${encodeURIComponent(queryParams[key])}`, ''); + triggerPixel(url) + }); + } +}; + +export let logWinningBidNotFound = (slot) => { + logWarn(`bid details could not be found for ${slot.getSlotElementId()}, probable reasons: a non-prebid bid is served OR check the prebid.AdUnit.code to GPT.AdSlot relation.`); +}; + +export let impressionViewableHandler = (globalModuleConfig, slot, event) => { + let respectiveBid = getMatchingWinningBidForGPTSlot(globalModuleConfig, slot); + if (respectiveBid === null) { + logWinningBidNotFound(slot); + } else { + // if config is enabled AND VURL array is present then execute each pixel + fireViewabilityPixels(globalModuleConfig, respectiveBid); + // trigger respective bidder's onBidViewable handler + adapterManager.callBidViewableBidder(respectiveBid.bidder, respectiveBid); + // emit the BID_VIEWABLE event with bid details, this event can be consumed by bidders and analytics pixels + events.emit(EVENTS.BID_VIEWABLE, respectiveBid); + } +}; + +export let init = () => { + events.on(EVENTS.AUCTION_INIT, () => { + // read the config for the module + const globalModuleConfig = config.getConfig(MODULE_NAME) || {}; + // do nothing if module-config.enabled is not set to true + // this way we are adding a way for bidders to know (using pbjs.getConfig('bidViewability').enabled === true) whether this module is added in build and is enabled + if (globalModuleConfig[CONFIG_ENABLED] !== true) { + return; + } + // add the GPT event listener + window.googletag = window.googletag || {}; + window.googletag.cmd = window.googletag.cmd || []; + window.googletag.cmd.push(() => { + window.googletag.pubads().addEventListener(GPT_IMPRESSION_VIEWABLE_EVENT, function(event) { + impressionViewableHandler(globalModuleConfig, event.slot, event); + }); + }); + }); +} + +init() diff --git a/modules/bidViewability.md b/modules/bidViewability.md new file mode 100644 index 00000000000..78a1539fb1a --- /dev/null +++ b/modules/bidViewability.md @@ -0,0 +1,49 @@ +# Overview + +Module Name: bidViewability + +Purpose: Track when a bid is viewable + +Maintainer: harshad.mane@pubmatic.com + +# Description +- This module, when included, will trigger a BID_VIEWABLE event which can be consumed by Analytics adapters, bidders will need to implement `onBidViewable` method to capture this event +- Bidderes can check if this module is part of the final build and whether it is enabled or not by accessing ```pbjs.getConfig('bidViewability')``` +- GPT API is used to find when a bid is viewable, https://developers.google.com/publisher-tag/reference#googletag.events.impressionviewableevent . This event is fired when an impression becomes viewable, according to the Active View criteria. +Refer: https://support.google.com/admanager/answer/4524488 +- The module does not work with adserver other than GAM with GPT integration +- Logic used to find a matching pbjs-bid for a GPT slot is ``` (slot.getAdUnitPath() === bid.adUnitCode || slot.getSlotElementId() === bid.adUnitCode) ``` this logic can be changed by using param ```customMatchFunction``` +- When a rendered PBJS bid is viewable the module will trigger BID_VIEWABLE event, which can be consumed by bidders and analytics adapters +- For the viewable bid if ```bid.vurls type array``` param is and module config ``` firePixels: true ``` is set then the URLs mentioned in bid.vurls will be executed. Please note that GDPR and USP related parameters will be added to the given URLs + +# Params +- enabled [required] [type: boolean, default: false], when set to true, the module will emit BID_VIEWABLE when applicable +- firePixels [optional] [type: boolean], when set to true, will fire the urls mentioned in bid.vurls which should be array of urls +- customMatchFunction [optional] [type: function(bid, slot)], when passed this function will be used to `find` the matching winning bid for the GPT slot. Default value is ` (bid, slot) => (slot.getAdUnitPath() === bid.adUnitCode || slot.getSlotElementId() === bid.adUnitCode) ` + +# Example of consuming BID_VIEWABLE event +``` + pbjs.onEvent('bidViewable', function(bid){ + console.log('got bid details in bidViewable event', bid); + }); + +``` + +# Example of using config +``` + pbjs.setConfig({ + bidViewability: { + enabled: true, + firePixels: true, + customMatchFunction: function(bid, slot){ + console.log('using custom match function....'); + return bid.adUnitCode === slot.getAdUnitPath(); + } + } + }); +``` + +# Please Note: +- Doesn't seems to work with Instream Video, https://docs.prebid.org/dev-docs/examples/instream-banner-mix.html as GPT's impressionViewable event is not triggered for instream-video-creative +- Works with Banner, Outsteam, Native creatives + diff --git a/modules/britepoolIdSystem.md b/modules/britepoolIdSystem.md index a15f601aee3..72edbe2324b 100644 --- a/modules/britepoolIdSystem.md +++ b/modules/britepoolIdSystem.md @@ -4,7 +4,7 @@ BritePool User ID Module. For assistance setting up your module please contact u ### Prebid Params -Individual params may be set for the BritePool User ID Submodule. At least one identifier must be set in the params. +Individual params may be set for the BritePool User ID Submodule. ``` pbjs.setConfig({ userSync: { diff --git a/modules/browsiRtdProvider.md b/modules/browsiRtdProvider.md index 0dd8c1d7609..9eed4b2b2d4 100644 --- a/modules/browsiRtdProvider.md +++ b/modules/browsiRtdProvider.md @@ -19,9 +19,9 @@ Configuration example for using RTD module with `browsi` provider "name": "browsi", "waitForIt": "true" "params": { - "url": "testUrl.com", - "siteKey": "testKey", - "pubKey": "testPub", + "url": "yield-manager.browsiprod.com", + "siteKey": "browsidemo", + "pubKey": "browsidemo" "keyName":"bv" } }] diff --git a/modules/cleanmedianetBidAdapter.js b/modules/cleanmedianetBidAdapter.js index a8f37450d68..80e4f13e7d4 100644 --- a/modules/cleanmedianetBidAdapter.js +++ b/modules/cleanmedianetBidAdapter.js @@ -3,6 +3,7 @@ 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'; export const helper = { getTopWindowDomain: function (url) { @@ -119,7 +120,7 @@ export const spec = { const hasFavoredMediaType = params.favoredMediaType && - this.supportedMediaTypes.includes(params.favoredMediaType); + includes(this.supportedMediaTypes, params.favoredMediaType); if (!mediaTypes || mediaTypes.banner) { if (!hasFavoredMediaType || params.favoredMediaType === BANNER) { diff --git a/modules/colossussspBidAdapter.js b/modules/colossussspBidAdapter.js index a3beb723528..1afb343566d 100644 --- a/modules/colossussspBidAdapter.js +++ b/modules/colossussspBidAdapter.js @@ -56,15 +56,8 @@ export const spec = { * @return ServerRequest Info describing the request to the server. */ buildRequests: (validBidRequests, bidderRequest) => { - let winTop = window; - let location; - try { - location = new URL(bidderRequest.refererInfo.referer) - winTop = window.top; - } catch (e) { - location = winTop.location; - utils.logMessage(e); - }; + const winTop = utils.getWindowTop(); + const location = winTop.location; let placements = []; let request = { 'deviceWidth': winTop.screen.width, @@ -94,15 +87,29 @@ export const spec = { bidId: bid.bidId, sizes: bid.mediaTypes[traff].sizes, traffic: traff, - eids: [] + eids: [], + floor: {} }; + if (typeof bid.getFloor === 'function') { + let tmpFloor = {}; + for (let size of placement.sizes) { + tmpFloor = bid.getFloor({ + currency: 'USD', + mediaType: traff, + size: size + }); + if (tmpFloor) { + placement.floor[`${size[0]}x${size[1]}`] = tmpFloor.floor; + } + } + } if (bid.schain) { placement.schain = bid.schain; } if (bid.userId) { getUserId(placement.eids, bid.userId.britepoolid, 'britepool.com'); getUserId(placement.eids, bid.userId.idl_env, 'identityLink'); - getUserId(placement.eids, utils.deepAccess(bid, 'userId.id5id.uid'), 'id5-sync.com', utils.deepAccess(bid, 'userId.id5id.ext')); + getUserId(placement.eids, bid.userId.id5id, 'id5-sync.com') getUserId(placement.eids, bid.userId.tdid, 'adserver.org', { rtiPartner: 'TDID' }); diff --git a/modules/consentManagement.js b/modules/consentManagement.js index 1060fdb5cc5..67af2baf959 100644 --- a/modules/consentManagement.js +++ b/modules/consentManagement.js @@ -199,29 +199,51 @@ function lookupIabConsent(cmpSuccess, cmpError, hookConfig) { function callCmpWhileInIframe(commandName, cmpFrame, moduleCallback) { let apiName = (cmpVersion === 2) ? '__tcfapi' : '__cmp'; + let callId = Math.random() + ''; + let callName = `${apiName}Call`; + /* Setup up a __cmp function to do the postMessage and stash the callback. This function behaves (from the caller's perspective identicially to the in-frame __cmp call */ - window[apiName] = function (cmd, arg, callback) { - let callId = Math.random() + ''; - let callName = `${apiName}Call`; - let msg = { - [callName]: { - command: cmd, - parameter: arg, - callId: callId - } - }; - if (cmpVersion !== 1) msg[callName].version = cmpVersion; + if (cmpVersion === 2) { + window[apiName] = function (cmd, cmpVersion, callback, arg) { + let msg = { + [callName]: { + command: cmd, + version: cmpVersion, + parameter: arg, + callId: callId + } + }; - cmpCallbacks[callId] = callback; - cmpFrame.postMessage(msg, '*'); - } + cmpCallbacks[callId] = callback; + cmpFrame.postMessage(msg, '*'); + } - /** when we get the return message, call the stashed callback */ - window.addEventListener('message', readPostMessageResponse, false); + /** when we get the return message, call the stashed callback */ + window.addEventListener('message', readPostMessageResponse, false); - // call CMP - window[apiName](commandName, undefined, moduleCallback); + // call CMP + window[apiName](commandName, cmpVersion, moduleCallback); + } else { + window[apiName] = function (cmd, arg, callback) { + let msg = { + [callName]: { + command: cmd, + parameter: arg, + callId: callId + } + }; + + cmpCallbacks[callId] = callback; + cmpFrame.postMessage(msg, '*'); + } + + /** when we get the return message, call the stashed callback */ + window.addEventListener('message', readPostMessageResponse, false); + + // call CMP + window[apiName](commandName, undefined, moduleCallback); + } function readPostMessageResponse(event) { let cmpDataPkgName = `${apiName}Return`; diff --git a/modules/consentManagementUsp.js b/modules/consentManagementUsp.js index 3edacb41549..cba9c2758d0 100644 --- a/modules/consentManagementUsp.js +++ b/modules/consentManagementUsp.js @@ -44,6 +44,35 @@ function lookupStaticConsentData(cmpSuccess, cmpError, hookConfig) { * @param {object} hookConfig contains module related variables (see comment in requestBidsHook function) */ function lookupUspConsent(uspSuccess, uspError, hookConfig) { + function findUsp() { + let f = window; + let uspapiFrame; + let uspapiFunction; + + while (!uspapiFrame) { + try { + if (typeof f.__uspapi === 'function') { + uspapiFunction = f.__uspapi; + uspapiFrame = f; + break; + } + } catch (e) {} + + try { + if (f.frames['__uspapiLocator']) { + uspapiFrame = f; + break; + } + } catch (e) {} + if (f === window.top) break; + f = f.parent; + } + return { + uspapiFrame, + uspapiFunction, + }; + } + function handleUspApiResponseCallbacks() { const uspResponse = {}; @@ -61,41 +90,43 @@ function lookupUspConsent(uspSuccess, uspError, hookConfig) { uspResponse.usPrivacy = consentResponse.uspString; } afterEach(); - } + }, }; } let callbackHandler = handleUspApiResponseCallbacks(); let uspapiCallbacks = {}; + let { uspapiFrame, uspapiFunction } = findUsp(); + + if (!uspapiFrame) { + return uspError('USP CMP not found.', hookConfig); + } + // to collect the consent information from the user, we perform a call to USPAPI // to collect the user's consent choices represented as a string (via getUSPData) // the following code also determines where the USPAPI is located and uses the proper workflow to communicate with it: - // - use the USPAPI locator code to see if USP's located in the current window or an ancestor window. This works in friendly or cross domain iframes + // - use the USPAPI locator code to see if USP's located in the current window or an ancestor window. + // - else assume prebid is in an iframe, and use the locator to see if the CMP is located in a higher parent window. This works in cross domain iframes. // - if USPAPI is not found, the iframe function will call the uspError exit callback to abort the rest of the USPAPI workflow - // - try to call the __uspapi() function directly, otherwise use the postMessage() api - // find the CMP frame/window - - try { - // try to call __uspapi directly - window.__uspapi('getUSPData', USPAPI_VERSION, callbackHandler.consentDataCallback); - } catch (e) { - // must not have been accessible, try using postMessage() api - let f = window; - let uspapiFrame; - while (!uspapiFrame) { - try { - if (f.frames['__uspapiLocator']) uspapiFrame = f; - } catch (e) { } - if (f === window.top) break; - f = f.parent; - } - if (!uspapiFrame) { - return uspError('USP CMP not found.', hookConfig); - } - callUspApiWhileInIframe('getUSPData', uspapiFrame, callbackHandler.consentDataCallback); + if (utils.isFn(uspapiFunction)) { + utils.logInfo('Detected USP CMP is directly accessible, calling it now...'); + uspapiFunction( + 'getUSPData', + USPAPI_VERSION, + callbackHandler.consentDataCallback + ); + } else { + utils.logInfo( + 'Detected USP CMP is outside the current iframe where Prebid.js is located, calling it now...' + ); + callUspApiWhileInIframe( + 'getUSPData', + uspapiFrame, + callbackHandler.consentDataCallback + ); } function callUspApiWhileInIframe(commandName, uspapiFrame, moduleCallback) { @@ -107,19 +138,19 @@ function lookupUspConsent(uspSuccess, uspError, hookConfig) { __uspapiCall: { command: cmd, version: ver, - callId: callId - } + callId: callId, + }, }; uspapiCallbacks[callId] = callback; uspapiFrame.postMessage(msg, '*'); - } + }; /** when we get the return message, call the stashed callback */ window.addEventListener('message', readPostMessageResponse, false); // call uspapi - window.__uspapi(commandName, USPAPI_VERSION, uspapiCallback); + window.__uspapi(commandName, USPAPI_VERSION, moduleCallback); function readPostMessageResponse(event) { const res = event && event.data && event.data.__uspapiReturn; @@ -130,11 +161,6 @@ function lookupUspConsent(uspSuccess, uspError, hookConfig) { } } } - - function uspapiCallback(consentObject, success) { - window.removeEventListener('message', readPostMessageResponse, false); - moduleCallback(consentObject, success); - } } } diff --git a/modules/craftBidAdapter.js b/modules/craftBidAdapter.js index 3838f5dee59..0124f96a107 100644 --- a/modules/craftBidAdapter.js +++ b/modules/craftBidAdapter.js @@ -7,7 +7,7 @@ import includes from 'core-js-pure/features/array/includes.js'; import { getStorageManager } from '../src/storageManager.js'; const BIDDER_CODE = 'craft'; -const URL = 'https://gacraft.jp/prebid-v3'; +const URL_BASE = 'https://gacraft.jp/prebid-v3'; const TTL = 360; const storage = getStorageManager(); @@ -143,10 +143,11 @@ function formatRequest(payload, bidderRequest) { withCredentials: false }; } + const payloadString = JSON.stringify(payload); return { method: 'POST', - url: URL, + url: `${URL_BASE}/${payload.tags[0].sitekey}`, data: payloadString, bidderRequest, options diff --git a/modules/criteoBidAdapter.js b/modules/criteoBidAdapter.js index 3dabe911884..e3a6b9eaa12 100644 --- a/modules/criteoBidAdapter.js +++ b/modules/criteoBidAdapter.js @@ -8,7 +8,7 @@ import { verify } from 'criteo-direct-rsa-validate/build/verify.js'; import { getStorageManager } from '../src/storageManager.js'; const GVLID = 91; -export const ADAPTER_VERSION = 32; +export const ADAPTER_VERSION = 33; const BIDDER_CODE = 'criteo'; const CDB_ENDPOINT = 'https://bidder.criteo.com/cdb'; const PROFILE_ID_INLINE = 207; @@ -79,10 +79,6 @@ export const spec = { if (publisherTagAvailable()) { // eslint-disable-next-line no-undef const adapter = new Criteo.PubTag.Adapters.Prebid(PROFILE_ID_PUBLISHERTAG, ADAPTER_VERSION, bidRequests, bidderRequest, '$prebid.version$'); - const enableSendAllBids = config.getConfig('enableSendAllBids'); - if (adapter.setEnableSendAllBids && typeof adapter.setEnableSendAllBids === 'function' && typeof enableSendAllBids === 'boolean') { - adapter.setEnableSendAllBids(enableSendAllBids); - } url = adapter.buildCdbUrl(); data = adapter.buildCdbRequest(); } else { @@ -133,8 +129,6 @@ export const spec = { if (slot.native) { if (bidRequest.params.nativeCallback) { bid.ad = createNativeAd(bidId, slot.native, bidRequest.params.nativeCallback); - } else if (config.getConfig('enableSendAllBids') === true) { - return; } else { bid.native = createPrebidNativeAd(slot.native); bid.mediaType = NATIVE; @@ -253,12 +247,14 @@ function buildCdbUrl(context) { function checkNativeSendId(bidRequest) { return !(bidRequest.nativeParams && - ((bidRequest.nativeParams.image && bidRequest.nativeParams.image.sendId !== true) || - (bidRequest.nativeParams.icon && bidRequest.nativeParams.icon.sendId !== true) || - (bidRequest.nativeParams.clickUrl && bidRequest.nativeParams.clickUrl.sendId !== true) || - (bidRequest.nativeParams.displayUrl && bidRequest.nativeParams.displayUrl.sendId !== true) || - (bidRequest.nativeParams.privacyLink && bidRequest.nativeParams.privacyLink.sendId !== true) || - (bidRequest.nativeParams.privacyIcon && bidRequest.nativeParams.privacyIcon.sendId !== true))); + ( + (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))) + )); } /** @@ -416,6 +412,7 @@ function hasValidVideoMediaType(bidRequest) { */ function createPrebidNativeAd(payload) { return { + sendTargetingKeys: false, // no key is added to KV by default title: payload.products[0].title, body: payload.products[0].description, sponsoredBy: payload.advertiser.description, diff --git a/modules/dfpAdServerVideo.js b/modules/dfpAdServerVideo.js index 554c44aa708..22c904b604c 100644 --- a/modules/dfpAdServerVideo.js +++ b/modules/dfpAdServerVideo.js @@ -8,6 +8,7 @@ import { deepAccess, isEmpty, logError, parseSizesInput, formatQS, parseUrl, bui import { config } from '../src/config.js'; import { getHook, submodule } from '../src/hook.js'; import { auctionManager } from '../src/auctionManager.js'; +import { gdprDataHandler, uspDataHandler } from '../src/adapterManager.js'; import events from '../src/events.js'; import CONSTANTS from '../src/constants.json'; @@ -100,6 +101,16 @@ export function buildDfpVideoUrl(options) { const descriptionUrl = getDescriptionUrl(bid, options, 'params'); if (descriptionUrl) { queryParams.description_url = descriptionUrl; } + const gdprConsent = gdprDataHandler.getConsentData(); + if (gdprConsent) { + if (typeof gdprConsent.gdprApplies === 'boolean') { queryParams.gdpr = Number(gdprConsent.gdprApplies); } + if (gdprConsent.consentString) { queryParams.gdpr_consent = gdprConsent.consentString; } + if (gdprConsent.addtlConsent) { queryParams.addtl_consent = gdprConsent.addtlConsent; } + } + + const uspConsent = uspDataHandler.getConsentData(); + if (uspConsent) { queryParams.us_privacy = uspConsent; } + return buildUrl({ protocol: 'https', host: 'securepubads.g.doubleclick.net', @@ -183,6 +194,16 @@ export function buildAdpodVideoUrl({code, params, callback} = {}) { { cust_params: encodedCustomParams } ); + const gdprConsent = gdprDataHandler.getConsentData(); + if (gdprConsent) { + if (typeof gdprConsent.gdprApplies === 'boolean') { queryParams.gdpr = Number(gdprConsent.gdprApplies); } + if (gdprConsent.consentString) { queryParams.gdpr_consent = gdprConsent.consentString; } + if (gdprConsent.addtlConsent) { queryParams.addtl_consent = gdprConsent.addtlConsent; } + } + + const uspConsent = uspDataHandler.getConsentData(); + if (uspConsent) { queryParams.us_privacy = uspConsent; } + const masterTag = buildUrl({ protocol: 'https', host: 'securepubads.g.doubleclick.net', diff --git a/modules/districtmDMXBidAdapter.js b/modules/districtmDMXBidAdapter.js index 60f6b9b64b1..ec0a0a2f2e6 100644 --- a/modules/districtmDMXBidAdapter.js +++ b/modules/districtmDMXBidAdapter.js @@ -7,6 +7,7 @@ const BIDDER_CODE = 'districtmDMX'; const DMXURI = 'https://dmx.districtm.io/b/v1'; +const GVLID = 144; const VIDEO_MAPPING = { playback_method: { 'auto_play_sound_on': 1, @@ -19,10 +20,12 @@ const VIDEO_MAPPING = { }; export const spec = { code: BIDDER_CODE, + gvlid: GVLID, + aliases: ['dmx'], supportedFormat: [BANNER, VIDEO], supportedMediaTypes: [VIDEO, BANNER], isBidRequestValid(bid) { - return !!(bid.params.dmxid && bid.params.memberid); + return !!(bid.params.memberid); }, interpretResponse(response, bidRequest) { response = response.body || {}; @@ -39,9 +42,9 @@ export const spec = { nBid.requestId = nBid.impid; nBid.width = nBid.w || width; nBid.height = nBid.h || height; - nBid.ttl = 360; + nBid.ttl = 300; nBid.mediaType = bid.mediaTypes && bid.mediaTypes.video ? 'video' : 'banner'; - if (nBid.mediaType) { + if (nBid.mediaType === 'video') { nBid.vastXml = cleanVast(nBid.adm, nBid.nurl); nBid.ttl = 3600; } @@ -152,7 +155,7 @@ export const spec = { let tosendtags = bidRequest.map(dmx => { var obj = {}; obj.id = dmx.bidId; - obj.tagid = String(dmx.params.dmxid); + obj.tagid = String(dmx.params.dmxid || dmx.adUnitCode); obj.secure = 1; obj.bidfloor = getFloor(dmx); if (dmx.mediaTypes && dmx.mediaTypes.video) { diff --git a/modules/emx_digitalBidAdapter.js b/modules/emx_digitalBidAdapter.js index 72da18d5691..41a5af6d703 100644 --- a/modules/emx_digitalBidAdapter.js +++ b/modules/emx_digitalBidAdapter.js @@ -158,11 +158,11 @@ export const emxAdapter = { return emxData; }, - getSupplyChain: (bidRequests, emxData) => { - if (bidRequests.schain) { + getSupplyChain: (bidderRequest, emxData) => { + if (bidderRequest.bids[0] && bidderRequest.bids[0].schain) { emxData.source = { ext: { - schain: bidRequests.schain + schain: bidderRequest.bids[0].schain } }; } diff --git a/modules/engageyaBidAdapter.js b/modules/engageyaBidAdapter.js new file mode 100644 index 00000000000..321b3287c2b --- /dev/null +++ b/modules/engageyaBidAdapter.js @@ -0,0 +1,133 @@ +import { + BANNER, + NATIVE +} from '../src/mediaTypes.js'; + +const { + registerBidder +} = require('../src/adapters/bidderFactory.js'); +const BIDDER_CODE = 'engageya'; +const ENDPOINT_URL = 'https://recs.engageya.com/rec-api/getrecs.json'; +const ENDPOINT_METHOD = 'GET'; + +function getPageUrl() { + var pUrl = window.location.href; + if (isInIframe()) { + pUrl = document.referrer ? document.referrer : pUrl; + } + pUrl = encodeURIComponent(pUrl); + return pUrl; +} + +function isInIframe() { + try { + var isInIframe = (window.self !== window.top); + } catch (e) { + isInIframe = true; + } + return isInIframe; +} + +export const spec = { + code: BIDDER_CODE, + supportedMediaTypes: [BANNER, NATIVE], + isBidRequestValid: function(bid) { + return bid && bid.params && bid.params.hasOwnProperty('widgetId') && bid.params.hasOwnProperty('websiteId') && !isNaN(bid.params.widgetId) && !isNaN(bid.params.websiteId); + }, + + buildRequests: function(validBidRequests, bidderRequest) { + var bidRequests = []; + if (validBidRequests && validBidRequests.length > 0) { + validBidRequests.forEach(function(bidRequest) { + if (bidRequest.params) { + var mediaType = bidRequest.hasOwnProperty('nativeParams') ? 1 : 2; + var imageWidth = -1; + var imageHeight = -1; + if (bidRequest.sizes && bidRequest.sizes.length > 0) { + imageWidth = bidRequest.sizes[0][0]; + imageHeight = bidRequest.sizes[0][1]; + } else if (bidRequest.nativeParams && bidRequest.nativeParams.image && bidRequest.nativeParams.image.sizes) { + imageWidth = bidRequest.nativeParams.image.sizes[0]; + imageHeight = bidRequest.nativeParams.image.sizes[1]; + } + + var widgetId = bidRequest.params.widgetId; + var websiteId = bidRequest.params.websiteId; + var pageUrl = (bidRequest.params.pageUrl && bidRequest.params.pageUrl != '[PAGE_URL]') ? bidRequest.params.pageUrl : ''; + if (!pageUrl) { + pageUrl = (bidderRequest && bidderRequest.refererInfo && bidderRequest.refererInfo.referer) ? bidderRequest.refererInfo.referer : getPageUrl(); + } + var bidId = bidRequest.bidId; + var finalUrl = ENDPOINT_URL + '?pubid=0&webid=' + websiteId + '&wid=' + widgetId + '&url=' + pageUrl + '&ireqid=' + bidId + '&pbtpid=' + mediaType + '&imw=' + imageWidth + '&imh=' + imageHeight; + if (bidderRequest && bidderRequest.gdprConsent && bidderRequest.gdprApplies && bidderRequest.consentString) { + finalUrl += '&is_gdpr=1&gdpr_consent=' + bidderRequest.consentString; + } + bidRequests.push({ + url: finalUrl, + method: ENDPOINT_METHOD, + data: '' + }); + } + }); + } + + return bidRequests; + }, + + interpretResponse: function(serverResponse, bidRequest) { + const bidResponses = []; + if (serverResponse.body && serverResponse.body.recs && serverResponse.body.recs.length > 0) { + var response = serverResponse.body; + var isNative = response.pbtypeId == 1; + response.recs.forEach(function(rec) { + var imageSrc = rec.thumbnail_path.indexOf('http') == -1 ? 'https:' + rec.thumbnail_path : rec.thumbnail_path; + if (isNative) { + bidResponses.push({ + requestId: response.ireqId, + cpm: rec.ecpm, + width: response.imageWidth, + height: response.imageHeight, + creativeId: rec.postId, + currency: 'USD', + netRevenue: false, + ttl: 360, + native: { + title: rec.title, + body: '', + image: { + url: imageSrc, + width: response.imageWidth, + height: response.imageHeight + }, + privacyLink: '', + clickUrl: rec.clickUrl, + displayUrl: rec.url, + cta: '', + sponsoredBy: rec.displayName, + impressionTrackers: [], + }, + }); + } else { + // var htmlTag = ""; + var htmlTag = '
'; + var tag = rec.tag ? rec.tag : htmlTag; + bidResponses.push({ + requestId: response.ireqId, + cpm: rec.ecpm, + width: response.imageWidth, + height: response.imageHeight, + creativeId: rec.postId, + currency: 'USD', + netRevenue: false, + ttl: 360, + ad: tag, + }); + } + }); + } + + return bidResponses; + } +}; + +registerBidder(spec); diff --git a/modules/engageyaBidAdapter.md b/modules/engageyaBidAdapter.md new file mode 100644 index 00000000000..541ba548eeb --- /dev/null +++ b/modules/engageyaBidAdapter.md @@ -0,0 +1,68 @@ +# Overview + +``` +Module Name: Engageya's Bidder Adapter +Module Type: Bidder Adapter +Maintainer: reem@engageya.com +``` + +# Description + +Module that connects to Engageya's demand sources + +# Test Parameters +``` + var adUnits = [ + { + code: 'test-div', + mediaTypes: { + banner: { + sizes: [[300, 250]], // a display size + } + }, + bids: [ + { + bidder: "engageya", + params: { + widgetId: '', + websiteId: '', + pageUrl:'[PAGE_URL]' + } + } + ] + },{ + code: 'test-div', + mediaTypes: { + native: { + image: { + required: true, + sizes: [236, 202] + }, + title: { + required: true, + len: 80 + }, + sponsoredBy: { + required: true + }, + clickUrl: { + required: true + }, + body: { + required: true + } + } + }, + bids: [ + { + bidder: "engageya", + params: { + widgetId: '', + websiteId: '', + pageUrl:'[PAGE_URL]' + } + } + ] + } + ]; +``` \ No newline at end of file diff --git a/modules/eplanningBidAdapter.js b/modules/eplanningBidAdapter.js index ac5ba659ad7..f85423ff05c 100644 --- a/modules/eplanningBidAdapter.js +++ b/modules/eplanningBidAdapter.js @@ -8,7 +8,7 @@ const BIDDER_CODE = 'eplanning'; const rnd = Math.random(); const DEFAULT_SV = 'ads.us.e-planning.net'; const DEFAULT_ISV = 'i.e-planning.net'; -const PARAMS = ['ci', 'sv', 't', 'ml']; +const PARAMS = ['ci', 'sv', 't', 'ml', 'sn']; const DOLLARS = 'USD'; const NET_REVENUE = true; const TTL = 120; @@ -16,6 +16,9 @@ const NULL_SIZE = '1x1'; const FILE = 'file'; const STORAGE_RENDER_PREFIX = 'pbsr_'; const STORAGE_VIEW_PREFIX = 'pbvi_'; +const mobileUserAgent = isMobileUserAgent(); +const PRIORITY_ORDER_FOR_MOBILE_SIZES_ASC = ['1x1', '300x50', '320x50', '300x250']; +const PRIORITY_ORDER_FOR_DESKTOP_SIZES_ASC = ['1x1', '970x90', '970x250', '160x600', '300x600', '728x90', '300x250']; export const spec = { code: BIDDER_CODE, @@ -140,6 +143,18 @@ export const spec = { }, } +function getUserAgent() { + return window.navigator.userAgent; +} +function getInnerWidth() { + return utils.getWindowSelf().innerWidth; +} +function isMobileUserAgent() { + return getUserAgent().match(/(mobile)|(ip(hone|ad))|(android)|(blackberry)|(nokia)|(phone)|(opera\smini)/i); +} +function isMobileDevice() { + return (getInnerWidth() <= 1024) || window.orientation || mobileUserAgent; +} function getUrlConfig(bidRequests) { if (isTestRequest(bidRequests)) { return getTestConfig(bidRequests.filter(br => br.params.t)); @@ -173,8 +188,32 @@ function getTestConfig(bidRequests) { }; } +function compareSizesByPriority(size1, size2) { + var priorityOrderForSizesAsc = isMobileDevice() ? PRIORITY_ORDER_FOR_MOBILE_SIZES_ASC : PRIORITY_ORDER_FOR_DESKTOP_SIZES_ASC; + var index1 = priorityOrderForSizesAsc.indexOf(size1); + var index2 = priorityOrderForSizesAsc.indexOf(size2); + if (index1 > -1) { + if (index2 > -1) { + return (index1 < index2) ? 1 : -1; + } else { + return -1; + } + } else { + return (index2 > -1) ? 1 : 0; + } +} + +function getSizesSortedByPriority(sizes) { + return utils.parseSizesInput(sizes).sort(compareSizesByPriority); +} + function getSize(bid, first) { - return bid.sizes && bid.sizes.length ? utils.parseSizesInput(first ? bid.sizes[0] : bid.sizes).join(',') : NULL_SIZE; + var arraySizes = bid.sizes && bid.sizes.length ? getSizesSortedByPriority(bid.sizes) : []; + if (arraySizes.length) { + return first ? arraySizes[0] : arraySizes.join(','); + } else { + return NULL_SIZE; + } } function getSpacesStruct(bids) { @@ -197,7 +236,14 @@ function getSpaces(bidRequests, ml) { let es = {str: '', vs: '', map: {}}; es.str = Object.keys(spacesStruct).map(size => spacesStruct[size].map((bid, i) => { es.vs += getVs(bid); - let name = ml ? cleanName(bid.adUnitCode) : getSize(bid, true) + '_' + i; + + let name; + if (ml) { + name = cleanName(bid.adUnitCode); + } else { + name = (bid.params && bid.params.sn) || (getSize(bid, true) + '_' + i); + } + es.map[name] = bid.bidId; return name + ':' + getSize(bid); }).join('+')).join('+'); diff --git a/modules/fabrickIdSystem.js b/modules/fabrickIdSystem.js index e61b377eefa..bb838788f07 100644 --- a/modules/fabrickIdSystem.js +++ b/modules/fabrickIdSystem.js @@ -46,7 +46,7 @@ export const fabrickIdSubmodule = { if (window.fabrickMod1) { window.fabrickMod1(configParams, consentData, cacheIdObj); } - if (!configParams || typeof configParams.apiKey !== 'string') { + if (!configParams || !configParams.apiKey || typeof configParams.apiKey !== 'string') { utils.logError('fabrick submodule requires an apiKey.'); return; } @@ -55,28 +55,34 @@ export const fabrickIdSubmodule = { let keysArr = Object.keys(configParams); for (let i in keysArr) { let k = keysArr[i]; - if (k === 'url' || k === 'refererInfo') { + if (k === 'url' || k === 'refererInfo' || (k.length > 3 && k.substring(0, 3) === 'max')) { continue; } let v = configParams[k]; if (Array.isArray(v)) { for (let j in v) { - url += `${k}=${v[j]}&`; + if (typeof v[j] === 'string' || typeof v[j] === 'number') { + url += `${k}=${v[j]}&`; + } } - } else { + } else if (typeof v === 'string' || typeof v === 'number') { url += `${k}=${v}&`; } } // pull off the trailing & url = url.slice(0, -1) const referer = _getRefererInfo(configParams); - const urls = new Set(); - url = truncateAndAppend(urls, url, 'r', referer.referer); + const refs = new Map(); + _setReferrer(refs, referer.referer); if (referer.stack && referer.stack[0]) { - url = truncateAndAppend(urls, url, 'r', referer.stack[0]); + _setReferrer(refs, referer.stack[0]); } - url = truncateAndAppend(urls, url, 'r', referer.canonicalUrl); - url = truncateAndAppend(urls, url, 'r', window.location.href); + _setReferrer(refs, referer.canonicalUrl); + _setReferrer(refs, window.location.href); + + refs.forEach(v => { + url = appendUrl(url, 'r', v, configParams); + }); const resp = function (callback) { const callbacks = { @@ -130,18 +136,48 @@ function _getBaseUrl(configParams) { } } -function truncateAndAppend(urls, url, paramName, s) { - if (s && url.length < 2000) { - if (s.length > 200) { - s = s.substring(0, 200); +function _setReferrer(refs, s) { + if (s) { + // store the longest one for the same URI + const url = s.split('?')[0]; + // OR store the longest one for the same domain + // const url = s.split('?')[0].replace('http://','').replace('https://', '').split('/')[0]; + if (refs.has(url)) { + const prevRef = refs.get(url); + if (s.length > prevRef.length) { + refs.set(url, s); + } + } else { + refs.set(url, s); + } + } +} + +export function appendUrl(url, paramName, s, configParams) { + const maxUrlLen = (configParams && configParams.maxUrlLen) || 2000; + const maxRefLen = (configParams && configParams.maxRefLen) || 1000; + const maxSpaceAvailable = (configParams && configParams.maxSpaceAvailable) || 50; + // make sure we have enough space left to make it worthwhile + if (s && url.length < (maxUrlLen - maxSpaceAvailable)) { + let thisMaxRefLen = maxUrlLen - url.length; + if (thisMaxRefLen > maxRefLen) { + thisMaxRefLen = maxRefLen; } - // Don't send the same url in multiple params - if (!urls.has(s)) { - urls.add(s); - return `${url}&${paramName}=${s}` + + s = `&${paramName}=${encodeURIComponent(s)}`; + + if (s.length >= thisMaxRefLen) { + s = s.substring(0, thisMaxRefLen); + if (s.charAt(s.length - 1) === '%') { + s = s.substring(0, thisMaxRefLen - 1); + } else if (s.charAt(s.length - 2) === '%') { + s = s.substring(0, thisMaxRefLen - 2); + } } + return `${url}${s}` + } else { + return url; } - return url; } submodule('userId', fabrickIdSubmodule); diff --git a/modules/fidelityBidAdapter.js b/modules/fidelityBidAdapter.js index baf5384fbfe..fac273721ff 100644 --- a/modules/fidelityBidAdapter.js +++ b/modules/fidelityBidAdapter.js @@ -6,7 +6,6 @@ const BIDDER_SERVER = 'x.fidelity-media.com'; const FIDELITY_VENDOR_ID = 408; export const spec = { code: BIDDER_CODE, - aliases: ['kubient'], gvlid: 408, isBidRequestValid: function isBidRequestValid(bid) { return !!(bid && bid.params && bid.params.zoneid); diff --git a/modules/freewheel-sspBidAdapter.js b/modules/freewheel-sspBidAdapter.js index 53f490a0a3c..1c9cd75f76f 100644 --- a/modules/freewheel-sspBidAdapter.js +++ b/modules/freewheel-sspBidAdapter.js @@ -102,22 +102,49 @@ function getCreativeId(xmlNode) { return creaId; } -function getDealId(xmlNode) { - var dealId = ''; +function getValueFromKeyInImpressionNode(xmlNode, key) { + var value = ''; var impNodes = xmlNode.querySelectorAll('Impression'); // Nodelist.forEach is not supported in IE and Edge - // Workaround given here https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/10638731/ - + var isRootViewKeyPresent = false; + var isAdsDisplayStartedPresent = false; Array.prototype.forEach.call(impNodes, function (el) { - var queries = el.textContent.substring(el.textContent.indexOf('?') + 1).split('&'); + if (isRootViewKeyPresent && isAdsDisplayStartedPresent) { + return value; + } + isRootViewKeyPresent = false; + isAdsDisplayStartedPresent = false; + var text = el.textContent; + var queries = text.substring(el.textContent.indexOf('?') + 1).split('&'); + var tempValue = ''; Array.prototype.forEach.call(queries, function (item) { var split = item.split('='); - if (split[0] == 'dealId') { - dealId = split[1]; + if (split[0] == key) { + tempValue = split[1]; + } + if (split[0] == 'reqType' && split[1] == 'AdsDisplayStarted') { + isAdsDisplayStartedPresent = true; + } + if (split[0] == 'rootViewKey') { + isRootViewKeyPresent = true; } }); + if (isAdsDisplayStartedPresent) { + value = tempValue; + } }); + return value; +} - return dealId; +function getDealId(xmlNode) { + return getValueFromKeyInImpressionNode(xmlNode, 'dealId'); +} + +function getBannerId(xmlNode) { + return getValueFromKeyInImpressionNode(xmlNode, 'adId'); +} + +function getCampaignId(xmlNode) { + return getValueFromKeyInImpressionNode(xmlNode, 'campaignId'); } /** @@ -373,7 +400,8 @@ export const spec = { const princingData = getPricing(xmlDoc); const creativeId = getCreativeId(xmlDoc); const dealId = getDealId(xmlDoc); - + const campaignId = getCampaignId(xmlDoc); + const bannerId = getBannerId(xmlDoc); const topWin = getTopMostWindow(); if (!topWin.freewheelssp_cache) { topWin.freewheelssp_cache = {}; @@ -392,7 +420,9 @@ export const spec = { currency: princingData.currency, netRevenue: true, ttl: 360, - dealId: dealId + dealId: dealId, + campaignId: campaignId, + bannerId: bannerId }; if (bidrequest.mediaTypes.video) { @@ -426,6 +456,6 @@ export const spec = { return []; } }, +}; -} registerBidder(spec); diff --git a/modules/gamoshiBidAdapter.js b/modules/gamoshiBidAdapter.js index 48a142a66a6..f8d2b70981c 100644 --- a/modules/gamoshiBidAdapter.js +++ b/modules/gamoshiBidAdapter.js @@ -3,6 +3,7 @@ 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'; const ENDPOINTS = { 'gamoshi': 'https://rtb.gamoshi.io' @@ -111,7 +112,7 @@ export const spec = { }; const hasFavoredMediaType = - params.favoredMediaType && this.supportedMediaTypes.includes(params.favoredMediaType); + params.favoredMediaType && includes(this.supportedMediaTypes, params.favoredMediaType); if (!mediaTypes || mediaTypes.banner) { if (!hasFavoredMediaType || params.favoredMediaType === BANNER) { diff --git a/modules/gjirafaBidAdapter.js b/modules/gjirafaBidAdapter.js index 48496b52c05..77589cd9071 100644 --- a/modules/gjirafaBidAdapter.js +++ b/modules/gjirafaBidAdapter.js @@ -31,6 +31,7 @@ export const spec = { let bidderRequestId = ''; let url = ''; let contents = []; + let data = {}; let placements = validBidRequests.map(bidRequest => { if (!propertyId) { propertyId = bidRequest.params.propertyId; } @@ -38,7 +39,8 @@ export const spec = { if (!storageId && bidRequest.params) { storageId = bidRequest.params.storageId || ''; } if (!bidderRequestId) { bidderRequestId = bidRequest.bidderRequestId; } if (!url && bidderRequest) { url = bidderRequest.refererInfo.referer; } - if (!contents.length && bidRequest.params.contents && bidRequest.params.contents.length) { contents = bidRequest.params.contents } + 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; } let adUnitId = bidRequest.adUnitCode; let placementId = bidRequest.params.placementId; @@ -49,6 +51,8 @@ export const spec = { adUnitId: adUnitId, placementId: placementId, bidid: bidRequest.bidId, + count: bidRequest.params.count, + skipTime: bidRequest.params.skipTime }; }); @@ -59,7 +63,8 @@ export const spec = { url: url, requestid: bidderRequestId, placements: placements, - contents: contents + contents: contents, + data: data } return [{ diff --git a/modules/gjirafaBidAdapter.md b/modules/gjirafaBidAdapter.md index deb06e74a27..fb4960d61f6 100644 --- a/modules/gjirafaBidAdapter.md +++ b/modules/gjirafaBidAdapter.md @@ -1,45 +1,67 @@ # Overview -Module Name: Gjirafa Bidder Adapter Module -Type: Bidder Adapter -Maintainer: drilon@gjirafa.com +Module Name: Gjirafa Bidder Adapter Module + +Type: Bidder Adapter + +Maintainer: arditb@gjirafa.com # Description Gjirafa Bidder Adapter for Prebid.js. # Test Parameters +```js var adUnits = [ { code: 'test-div', mediaTypes: { banner: { - sizes: [[728, 90]] + sizes: [ + [728, 90] + ] } }, - bids: [ - { - bidder: 'gjirafa', - params: { - propertyId: '105227', - placementId: '846841' + bids: [{ + bidder: 'gjirafa', + params: { + propertyId: '105227', //Required + placementId: '846841', //Required + data: { //Optional + catalogs: [{ + catalogId: 9, + items: ["193", "4", "1"] + }], + inventory: { + category: ["tech"], + query: ["iphone 12"] + } } } - ] + }] }, { code: 'test-div', mediaTypes: { - video: { + video: { context: 'instream' - } + } }, - bids: [ - { - bidder: 'gjirafa', - params: { - propertyId: '105227', - placementId: '846836' + bids: [{ + bidder: 'gjirafa', + params: { + propertyId: '105227', //Required + placementId: '846836', //Required + data: { //Optional + catalogs: [{ + catalogId: 9, + items: ["193", "4", "1"] + }], + inventory: { + category: ["tech"], + query: ["iphone 12"] + } } } - ] + }] } ]; +``` diff --git a/modules/glomexBidAdapter.js b/modules/glomexBidAdapter.js new file mode 100644 index 00000000000..8de3d3724ce --- /dev/null +++ b/modules/glomexBidAdapter.js @@ -0,0 +1,86 @@ +import { registerBidder } from '../src/adapters/bidderFactory.js' +import find from 'core-js-pure/features/array/find.js' +import { BANNER } from '../src/mediaTypes.js' + +const ENDPOINT = 'https://prebid.mes.glomex.cloud/request-bid' +const BIDDER_CODE = 'glomex' + +export const spec = { + code: BIDDER_CODE, + supportedMediaTypes: [BANNER], + + isBidRequestValid: function (bid) { + if (bid && bid.params && bid.params.integrationId) { + return true + } + return false + }, + + buildRequests: function (validBidRequests, bidderRequest = {}) { + const refererInfo = bidderRequest.refererInfo || {}; + const gdprConsent = bidderRequest.gdprConsent || {}; + + return { + method: 'POST', + url: `${ENDPOINT}`, + data: { + auctionId: bidderRequest.auctionId, + refererInfo: { + isAmp: refererInfo.isAmp, + numIframes: refererInfo.numIframes, + reachedTop: refererInfo.reachedTop, + referer: refererInfo.referer + }, + gdprConsent: { + consentString: gdprConsent.consentString, + gdprApplies: gdprConsent.gdprApplies + }, + bidRequests: validBidRequests.map(({ params, sizes, bidId, adUnitCode }) => ({ + bidId, + adUnitCode, + params, + sizes + })) + }, + options: { + withCredentials: false, + contentType: 'application/json' + }, + validBidRequests: validBidRequests, + } + }, + + interpretResponse: function (serverResponse, originalBidRequest) { + const bidResponses = [] + + originalBidRequest.validBidRequests.forEach(function (bidRequest) { + if (!serverResponse.body) { + return + } + + const matchedBid = find(serverResponse.body.bids, function (bid) { + return String(bidRequest.bidId) === String(bid.id) + }) + + if (matchedBid) { + const bidResponse = { + requestId: bidRequest.bidId, + cpm: matchedBid.cpm, + width: matchedBid.width, + height: matchedBid.height, + creativeId: matchedBid.creativeId, + dealId: matchedBid.dealId, + currency: matchedBid.currency, + netRevenue: matchedBid.netRevenue, + ttl: matchedBid.ttl, + ad: matchedBid.ad + } + + bidResponses.push(bidResponse) + } + }) + return bidResponses + } +}; + +registerBidder(spec) diff --git a/modules/glomexBidAdapter.md b/modules/glomexBidAdapter.md new file mode 100644 index 00000000000..d52ed88ff16 --- /dev/null +++ b/modules/glomexBidAdapter.md @@ -0,0 +1,32 @@ +# Overview + +``` +Module Name: Glomex Bidder Adapter +Module Type: Bidder Adapter +Maintainer: integration-squad@services.glomex.com +``` + +# Description + +Module to use the Glomex Player with prebid.js + +# Test Parameters +``` + var adUnits = [ + { + code: "banner", + mediaTypes: { + banner: { + sizes: [[640, 360]] + } + }, + bids: [{ + bidder: "glomex", + params: { + integrationId: '4059a11hkdzuf65i', + playlistId: 'v-bdui4dz7vjq9' + } + }] + } + ]; +``` diff --git a/modules/gmosspBidAdapter.js b/modules/gmosspBidAdapter.js index d9dc8f7641a..5eac4069b21 100644 --- a/modules/gmosspBidAdapter.js +++ b/modules/gmosspBidAdapter.js @@ -29,7 +29,7 @@ export const spec = { buildRequests: function (validBidRequests, bidderRequest) { const bidRequests = []; - const url = bidderRequest.refererInfo.referer; + const urlInfo = getUrlInfo(bidderRequest.refererInfo); const cur = getCurrencyType(); const dnt = utils.getDNT() ? '1' : '0'; @@ -46,7 +46,8 @@ export const spec = { queryString = utils.tryAppendQueryString(queryString, 'bid', bid); queryString = utils.tryAppendQueryString(queryString, 'ver', ver); queryString = utils.tryAppendQueryString(queryString, 'sid', sid); - queryString = utils.tryAppendQueryString(queryString, 'url', url); + queryString = utils.tryAppendQueryString(queryString, 'url', urlInfo.url); + queryString = utils.tryAppendQueryString(queryString, 'ref', urlInfo.ref); queryString = utils.tryAppendQueryString(queryString, 'cur', cur); queryString = utils.tryAppendQueryString(queryString, 'dnt', dnt); @@ -131,4 +132,31 @@ function getCurrencyType() { return 'JPY'; } +function getUrlInfo(refererInfo) { + return { + url: getUrl(refererInfo), + ref: getReferrer(), + }; +} + +function getUrl(refererInfo) { + if (refererInfo && refererInfo.referer) { + return refererInfo.referer; + } + + try { + return window.top.location.href; + } catch (e) { + return window.location.href; + } +} + +function getReferrer() { + try { + return window.top.document.referrer; + } catch (e) { + return document.referrer; + } +} + registerBidder(spec); diff --git a/modules/gothamadsBidAdapter.js b/modules/gothamadsBidAdapter.js new file mode 100644 index 00000000000..f2d2a9f5261 --- /dev/null +++ b/modules/gothamadsBidAdapter.js @@ -0,0 +1,307 @@ +import { registerBidder } from '../src/adapters/bidderFactory.js'; +import { BANNER, NATIVE, VIDEO } from '../src/mediaTypes.js'; +import * as utils from '../src/utils.js'; +import {config} from '../src/config.js'; + +const BIDDER_CODE = 'gothamads'; +const ACCOUNTID_MACROS = '[account_id]'; +const URL_ENDPOINT = `https://us-e-node1.gothamads.com/bid?pass=${ACCOUNTID_MACROS}&integration=prebidjs`; +const NATIVE_ASSET_IDS = { 0: 'title', 2: 'icon', 3: 'image', 5: 'sponsoredBy', 4: 'body', 1: 'cta' }; +const NATIVE_PARAMS = { + title: { + id: 0, + name: 'title' + }, + icon: { + id: 2, + type: 1, + name: 'img' + }, + image: { + id: 3, + type: 3, + name: 'img' + }, + sponsoredBy: { + id: 5, + name: 'data', + type: 1 + }, + body: { + id: 4, + name: 'data', + type: 2 + }, + cta: { + id: 1, + type: 12, + name: 'data' + } +}; +const NATIVE_VERSION = '1.2'; + +export const spec = { + code: BIDDER_CODE, + supportedMediaTypes: [BANNER, VIDEO, NATIVE], + + /** + * Determines whether or not the given bid request is valid. + * + * @param {object} bid The bid to validate. + * @return boolean True if this is a valid bid, and false otherwise. + */ + isBidRequestValid: (bid) => { + return Boolean(bid.params.accountId) && Boolean(bid.params.placementId) + }, + + /** + * Make a server request from the list of BidRequests. + * + * @param {BidRequest[]} validBidRequests A non-empty list of valid bid requests that should be sent to the Server. + * @return ServerRequest Info describing the request to the server. + */ + buildRequests: (validBidRequests, bidderRequest) => { + if (validBidRequests && validBidRequests.length === 0) return [] + let accuontId = validBidRequests[0].params.accountId; + const endpointURL = URL_ENDPOINT.replace(ACCOUNTID_MACROS, accuontId); + + let winTop = window; + let location; + try { + location = new URL(bidderRequest.refererInfo.referer) + winTop = window.top; + } catch (e) { + location = winTop.location; + utils.logMessage(e); + }; + + let bids = []; + for (let bidRequest of validBidRequests) { + let impObject = prepareImpObject(bidRequest); + let data = { + id: bidRequest.bidId, + test: config.getConfig('debug') ? 1 : 0, + cur: ['USD'], + device: { + w: winTop.screen.width, + h: winTop.screen.height, + language: (navigator && navigator.language) ? navigator.language.indexOf('-') != -1 ? navigator.language.split('-')[0] : navigator.language : '', + }, + site: { + page: location.pathname, + host: location.host + }, + source: { + tid: bidRequest.transactionId + }, + tmax: bidRequest.timeout, + imp: [impObject], + }; + bids.push(data) + } + return { + method: 'POST', + url: endpointURL, + data: bids + }; + }, + + /** + * Unpack the response from the server into a list of bids. + * + * @param {*} serverResponse A successful response from the server. + * @return {Bid[]} An array of bids which were nested inside the server. + */ + interpretResponse: (serverResponse) => { + if (!serverResponse || !serverResponse.body) return [] + let GothamAdskResponse = serverResponse.body; + + let bids = []; + for (let response of GothamAdskResponse) { + let mediaType = response.seatbid[0].bid[0].ext && response.seatbid[0].bid[0].ext.mediaType ? response.seatbid[0].bid[0].ext.mediaType : BANNER; + + let bid = { + requestId: response.id, + cpm: response.seatbid[0].bid[0].price, + width: response.seatbid[0].bid[0].w, + height: response.seatbid[0].bid[0].h, + ttl: response.ttl || 1200, + currency: response.cur || 'USD', + netRevenue: true, + creativeId: response.seatbid[0].bid[0].crid, + dealId: response.seatbid[0].bid[0].dealid, + mediaType: mediaType + }; + + switch (mediaType) { + case VIDEO: + bid.vastXml = response.seatbid[0].bid[0].adm + bid.vastUrl = response.seatbid[0].bid[0].ext.vastUrl + break + case NATIVE: + bid.native = parseNative(response.seatbid[0].bid[0].adm) + break + default: + bid.ad = response.seatbid[0].bid[0].adm + } + + bids.push(bid); + } + + return bids; + }, +}; + +/** + * Determine type of request + * + * @param bidRequest + * @param type + * @returns {boolean} + */ +const checkRequestType = (bidRequest, type) => { + return (typeof utils.deepAccess(bidRequest, `mediaTypes.${type}`) !== 'undefined'); +} + +const parseNative = admObject => { + const { assets, link, imptrackers, jstracker } = admObject.native; + const result = { + clickUrl: link.url, + clickTrackers: link.clicktrackers || undefined, + impressionTrackers: imptrackers || undefined, + javascriptTrackers: jstracker ? [ jstracker ] : undefined + }; + assets.forEach(asset => { + const kind = NATIVE_ASSET_IDS[asset.id]; + const content = kind && asset[NATIVE_PARAMS[kind].name]; + if (content) { + result[kind] = content.text || content.value || { url: content.url, width: content.w, height: content.h }; + } + }); + + return result; +} + +const prepareImpObject = (bidRequest) => { + let impObject = { + id: bidRequest.transactionId, + secure: 1, + ext: { + placementId: bidRequest.params.placementId + } + }; + if (checkRequestType(bidRequest, BANNER)) { + impObject.banner = addBannerParameters(bidRequest); + } + if (checkRequestType(bidRequest, VIDEO)) { + impObject.video = addVideoParameters(bidRequest); + } + if (checkRequestType(bidRequest, NATIVE)) { + impObject.native = { + ver: NATIVE_VERSION, + request: addNativeParameters(bidRequest) + }; + } + return impObject +}; + +const addNativeParameters = bidRequest => { + let impObject = { + id: bidRequest.transactionId, + ver: NATIVE_VERSION, + }; + + const assets = utils._map(bidRequest.mediaTypes.native, (bidParams, key) => { + const props = NATIVE_PARAMS[key]; + const asset = { + required: bidParams.required & 1, + }; + if (props) { + asset.id = props.id; + let wmin, hmin; + let aRatios = bidParams.aspect_ratios; + + if (aRatios && aRatios[0]) { + aRatios = aRatios[0]; + wmin = aRatios.min_width || 0; + hmin = aRatios.ratio_height * wmin / aRatios.ratio_width | 0; + } + + if (bidParams.sizes) { + const sizes = flatten(bidParams.sizes); + wmin = sizes[0]; + hmin = sizes[1]; + } + + asset[props.name] = {} + + if (bidParams.len) asset[props.name]['len'] = bidParams.len; + if (props.type) asset[props.name]['type'] = props.type; + if (wmin) asset[props.name]['wmin'] = wmin; + if (hmin) asset[props.name]['hmin'] = hmin; + + return asset; + } + }).filter(Boolean); + + impObject.assets = assets; + return impObject +} + +const addBannerParameters = (bidRequest) => { + let bannerObject = {}; + const size = parseSizes(bidRequest, 'banner'); + bannerObject.w = size[0]; + bannerObject.h = size[1]; + return bannerObject; +}; + +const parseSizes = (bid, mediaType) => { + let mediaTypes = bid.mediaTypes; + if (mediaType === 'video') { + let size = []; + if (mediaTypes.video && mediaTypes.video.w && mediaTypes.video.h) { + size = [ + mediaTypes.video.w, + mediaTypes.video.h + ]; + } else if (Array.isArray(utils.deepAccess(bid, 'mediaTypes.video.playerSize')) && bid.mediaTypes.video.playerSize.length === 1) { + size = bid.mediaTypes.video.playerSize[0]; + } else if (Array.isArray(bid.sizes) && bid.sizes.length > 0 && Array.isArray(bid.sizes[0]) && bid.sizes[0].length > 1) { + size = bid.sizes[0]; + } + return size; + } + let sizes = []; + if (Array.isArray(mediaTypes.banner.sizes)) { + sizes = mediaTypes.banner.sizes[0]; + } else if (Array.isArray(bid.sizes) && bid.sizes.length > 0) { + sizes = bid.sizes + } else { + utils.logWarn('no sizes are setup or found'); + } + + return sizes +} + +const addVideoParameters = (bidRequest) => { + let videoObj = {}; + let supportParamsList = ['mimes', 'minduration', 'maxduration', 'protocols', 'startdelay', 'placement', 'skip', 'skipafter', 'minbitrate', 'maxbitrate', 'delivery', 'playbackmethod', 'api', 'linearity'] + + for (let param of supportParamsList) { + if (bidRequest.mediaTypes.video[param] !== undefined) { + videoObj[param] = bidRequest.mediaTypes.video[param]; + } + } + + const size = parseSizes(bidRequest, 'video'); + videoObj.w = size[0]; + videoObj.h = size[1]; + return videoObj; +} + +const flatten = arr => { + return [].concat(...arr); +} + +registerBidder(spec); diff --git a/modules/gothamadsBidAdapter.md b/modules/gothamadsBidAdapter.md new file mode 100644 index 00000000000..3105dff6c6c --- /dev/null +++ b/modules/gothamadsBidAdapter.md @@ -0,0 +1,104 @@ +# Overview + +``` +Module Name: GothamAds SSP Bidder Adapter +Module Type: Bidder Adapter +Maintainer: support@gothamads.com +``` + +# Description + +Module that connects to GothamAds SSP demand sources + +# Test Parameters +``` + var adUnits = [{ + code: 'placementId', + mediaTypes: { + banner: { + sizes: [[300, 250], [300,600]] + } + }, + bids: [{ + bidder: 'gothamads', + params: { + placementId: 'hash', + accountId: 'accountId' + } + }] + }, + { + code: 'native_example', + // sizes: [[1, 1]], + mediaTypes: { + native: { + title: { + required: true, + len: 800 + }, + image: { + required: true, + len: 80 + }, + sponsoredBy: { + required: true + }, + clickUrl: { + required: true + }, + privacyLink: { + required: false + }, + body: { + required: true + }, + icon: { + required: true, + sizes: [50, 50] + } + } + + }, + bids: [ { + bidder: 'gothamads', + params: { + placementId: 'hash', + accountId: 'accountId' + } + }] + }, + { + code: 'video1', + sizes: [640,480], + mediaTypes: { video: { + minduration:0, + maxduration:999, + boxingallowed:1, + skip:0, + mimes:[ + 'application/javascript', + 'video/mp4' + ], + w:1920, + h:1080, + protocols:[ + 2 + ], + linearity:1, + api:[ + 1, + 2 + ] + } }, + bids: [ + { + bidder: 'gothamads', + params: { + placementId: 'hash', + accountId: 'accountId' + } + } + ] + } + ]; +``` \ No newline at end of file diff --git a/modules/gridBidAdapter.js b/modules/gridBidAdapter.js index 3f9d4fba31d..8ab2e2b8a95 100644 --- a/modules/gridBidAdapter.js +++ b/modules/gridBidAdapter.js @@ -51,6 +51,7 @@ export const spec = { let content = null; let schain = null; let userId = null; + let userIdAsEids = null; let user = null; let userExt = null; let {bidderRequestId, auctionId, gdprConsent, uspConsent, timeout, refererInfo} = bidderRequest || {}; @@ -72,6 +73,9 @@ export const spec = { if (!userId) { userId = bid.userId; } + if (!userIdAsEids) { + userIdAsEids = bid.userIdAsEids; + } const {params: {uid, keywords}, mediaTypes, bidId, adUnitCode, rtd} = bid; bidsMap[bidId] = bid; if (!pageKeywords && !utils.isEmpty(keywords)) { @@ -161,66 +165,9 @@ export const spec = { userExt = {consent: gdprConsent.consentString}; } - if (userId) { - if (userId.tdid) { - userExt = userExt || {}; - userExt.eids = userExt.eids || []; - userExt.eids.push({ - source: 'adserver.org', // Unified ID - uids: [{ - id: userId.tdid, - ext: { - rtiPartner: 'TDID' - } - }] - }); - } - if (userId.id5id && userId.id5id.uid) { - userExt = userExt || {}; - userExt.eids = userExt.eids || []; - userExt.eids.push({ - source: 'id5-sync.com', - uids: [{ - id: userId.id5id.uid - }], - ext: userId.id5id.ext - }); - } - if (userId.lipb && userId.lipb.lipbid) { - userExt = userExt || {}; - userExt.eids = userExt.eids || []; - userExt.eids.push({ - source: 'liveintent.com', - uids: [{ - id: userId.lipb.lipbid - }] - }); - } - if (userId.idl_env) { - userExt = userExt || {}; - userExt.eids = userExt.eids || []; - userExt.eids.push({ - source: 'identityLink', - uids: [{ - id: userId.idl_env - }] - }); - } - if (userId.criteoId) { - userExt = userExt || {}; - userExt.eids = userExt.eids || []; - userExt.eids.push({ - source: 'criteo.com', - uids: [{ - id: userId.criteoId - }] - }); - } - - if (userId.digitrustid && userId.digitrustid.data && userId.digitrustid.data.id) { - userExt = userExt || {}; - userExt.digitrust = Object.assign({}, userId.digitrustid.data); - } + if (userIdAsEids && userIdAsEids.length) { + userExt = userExt || {}; + userExt.eids = [...userIdAsEids]; } if (userExt && Object.keys(userExt).length) { @@ -382,7 +329,6 @@ function _addBidResponse(serverBid, bidRequest, bidResponses) { if (bid) { const bidResponse = { requestId: bid.bidId, // bid.bidderRequestId, - bidderCode: spec.code, cpm: serverBid.price, width: serverBid.w, height: serverBid.h, @@ -418,7 +364,7 @@ function _addBidResponse(serverBid, bidRequest, bidResponses) { } function createVideoRequest(bid, mediaType) { - const {playerSize, mimes, durationRangeSec} = mediaType; + const {playerSize, mimes, durationRangeSec, protocols} = mediaType; const size = (playerSize || bid.sizes || [])[0]; if (!size) return; @@ -433,6 +379,10 @@ function createVideoRequest(bid, mediaType) { result.maxduration = durationRangeSec[1]; } + if (protocols && protocols.length) { + result.protocols = protocols; + } + return result; } diff --git a/modules/gumgumBidAdapter.js b/modules/gumgumBidAdapter.js index 3206b7e1727..2b0f7e03d22 100644 --- a/modules/gumgumBidAdapter.js +++ b/modules/gumgumBidAdapter.js @@ -273,8 +273,13 @@ function buildRequests (validBidRequests, bidderRequest) { data.iriscat = params.iriscat; } - if (params.zone) { - data.t = params.zone; + if (params.irisid && typeof params.irisid === 'string') { + data.irisid = params.irisid; + } + + if (params.zone || params.pubId) { + params.zone ? (data.t = params.zone) : (data.pubId = params.pubId); + data.pi = 2; // inscreen // override pi if the following is found if (params.slot) { @@ -285,11 +290,8 @@ function buildRequests (validBidRequests, bidderRequest) { data.ni = parseInt(params.native, 10); data.pi = 5; } else if (mediaTypes.video) { - data.pi = mediaTypes.video.linearity === 1 ? 7 : 6; // video : invideo + data.pi = mediaTypes.video.linearity === 2 ? 6 : 7; // invideo : video } - } else if (params.pubId) { - data.pubId = params.pubId - data.pi = mediaTypes.video ? 7 : 2; // video : inscreen } else { // legacy params data = { ...data, ...handleLegacyParams(params, sizes) } } @@ -403,6 +405,7 @@ function interpretResponse (serverResponse, bidRequest) { } = Object.assign(defaultResponse, serverResponseBody) let data = bidRequest.data || {} let product = data.pi + let mediaType = (product === 6 || product === 7) ? VIDEO : BANNER let isTestUnit = (product === 3 && data.si === 9) let sizes = utils.parseSizesInput(bidRequest.sizes) let [width, height] = sizes[0].split('x') @@ -424,9 +427,9 @@ function interpretResponse (serverResponse, bidRequest) { bidResponses.push({ // dealId: DEAL_ID, // referrer: REFERER, - ...(product === 7 && { vastXml: markup, mediaType: VIDEO }), ad: wrapper ? getWrapperCode(wrapper, Object.assign({}, serverResponseBody, { bidRequest })) : markup, - ...(product === 6 && {ad: markup}), + ...(mediaType === VIDEO && {ad: markup, vastXml: markup}), + mediaType, cpm: isTestUnit ? 0.1 : cpm, creativeId, currency: cur || 'USD', diff --git a/modules/gumgumBidAdapter.md b/modules/gumgumBidAdapter.md index f47666e9628..57d56235d1c 100644 --- a/modules/gumgumBidAdapter.md +++ b/modules/gumgumBidAdapter.md @@ -8,14 +8,87 @@ Maintainer: engineering@gumgum.com # Description -GumGum adapter for Prebid.js 1.0 +GumGum adapter for Prebid.js +Please note that both video and in-video products require a mediaType of video. +In-screen and slot products should have a mediaType of banner. # Test Parameters ``` +var adUnits = [ + { + code: 'slot-placement', + sizes: [[300, 250]], + mediaTypes: { + banner: { + sizes: [[300, 250]], + } + }, + bids: [ + { + bidder: 'gumgum', + params: { + zone: 'dc9d6be1', // GumGum Zone ID given to the client + slot: '15901', // GumGum Slot ID given to the client, + bidfloor: 0.03 // CPM bid floor + } + } + ] + },{ + code: 'inscreen-placement', + sizes: [[300, 50]], + mediaTypes: { + banner: { + sizes: [[1, 1]], + } + }, + bids: [ + { + bidder: 'gumgum', + params: { + zone: 'dc9d6be1', // GumGum Zone ID given to the client + bidfloor: 0.03 // CPM bid floor + } + } + ] + },{ + code: 'video-placement', + sizes: [[300, 50]], + mediaTypes: { + video: { + context: 'instream', + playerSize: [640, 480], + minduration: 1, + maxduration: 2, + linearity: 1, + startdelay: 1, + placement: 1, + protocols: [1, 2] + } + }, + bids: [ + { + bidder: 'gumgum', + params: { + zone: 'ggumtest', // GumGum Zone ID given to the client + bidfloor: 0.03 // CPM bid floor + } + } + ] + } +]; +``` + +# Legacy Test Parameters +``` var adUnits = [ { code: 'test-div', sizes: [[300, 250]], + mediaTypes: { + banner: { + sizes: [[300, 250]], + } + }, bids: [ { bidder: 'gumgum', @@ -28,6 +101,11 @@ var adUnits = [ },{ code: 'test-div', sizes: [[300, 50]], + mediaTypes: { + banner: { + sizes: [[1, 1]], + } + }, bids: [ { bidder: 'gumgum', @@ -40,6 +118,18 @@ var adUnits = [ },{ code: 'test-div', sizes: [[300, 50]], + mediaTypes: { + video: { + context: 'instream', + playerSize: [640, 480], + minduration: 1, + maxduration: 2, + linearity: 2, + startdelay: 1, + placement: 1, + protocols: [1, 2] + } + } bids: [ { bidder: 'gumgum', diff --git a/modules/id5IdSystem.js b/modules/id5IdSystem.js index 7033a71d015..6dade7d01f0 100644 --- a/modules/id5IdSystem.js +++ b/modules/id5IdSystem.js @@ -16,7 +16,9 @@ const MODULE_NAME = 'id5Id'; const GVLID = 131; const NB_EXP_DAYS = 30; export const ID5_STORAGE_NAME = 'id5id'; +export const ID5_PRIVACY_STORAGE_NAME = `${ID5_STORAGE_NAME}_privacy`; const LOCAL_STORAGE = 'html5'; +const ABTEST_RESOLUTION = 10000; // order the legacy cookie names in reverse priority order so the last // cookie in the array is the most preferred to use @@ -42,27 +44,49 @@ export const id5IdSubmodule = { * decode the stored id value for passing to bid requests * @function decode * @param {(Object|string)} value + * @param {SubmoduleConfig|undefined} config * @returns {(Object|undefined)} */ - decode(value) { - let uid; + decode(value, config) { + let universalUid; let linkType = 0; if (value && typeof value.universal_uid === 'string') { - uid = value.universal_uid; + universalUid = value.universal_uid; linkType = value.link_type || linkType; } else { return undefined; } - return { - 'id5id': { - 'uid': uid, - 'ext': { - 'linkType': linkType + // check for A/B testing configuration and hide ID if in Control Group + const abConfig = getAbTestingConfig(config); + const controlGroup = isInControlGroup(universalUid, abConfig.controlGroupPct); + if (abConfig.enabled === true && typeof controlGroup === 'undefined') { + // A/B Testing is enabled, but configured improperly, so skip A/B testing + utils.logError('User ID - ID5 submodule: A/B Testing controlGroupPct must be a number >= 0 and <= 1! Skipping A/B Testing'); + } else if (abConfig.enabled === true && controlGroup === true) { + // A/B Testing is enabled and user is in the Control Group, so do not share the ID5 ID + utils.logInfo('User ID - ID5 submodule: A/B Testing Enabled - user is in the Control Group, so the ID5 ID is NOT exposed'); + universalUid = linkType = 0; + } else if (abConfig.enabled === true) { + // A/B Testing is enabled but user is not in the Control Group, so ID5 ID is shared + utils.logInfo('User ID - ID5 submodule: A/B Testing Enabled - user is NOT in the Control Group, so the ID5 ID is exposed'); + } + + let responseObj = { + id5id: { + uid: universalUid, + ext: { + linkType: linkType } } }; + + if (abConfig.enabled === true) { + utils.deepSetValue(responseObj, 'id5id.ext.abTestingControlGroup', (typeof controlGroup === 'undefined' ? false : controlGroup)); + } + + return responseObj; }, /** @@ -98,6 +122,10 @@ export const id5IdSubmodule = { 'v': '$prebid.version$' }; + if (getAbTestingConfig(config).enabled === true) { + utils.deepSetValue(data, 'features.ab', 1); + } + const resp = function (callback) { const callbacks = { success: response => { @@ -106,6 +134,9 @@ export const id5IdSubmodule = { try { responseObj = JSON.parse(response); resetNb(config.params.partner); + if (responseObj.privacy) { + storeInLocalStorage(ID5_PRIVACY_STORAGE_NAME, JSON.stringify(responseObj.privacy), NB_EXP_DAYS); + } // TODO: remove after requiring publishers to use localstorage and // all publishers have upgraded @@ -135,10 +166,11 @@ export const id5IdSubmodule = { * It's permissible to return neither, one, or both fields. * @function extendId * @param {SubmoduleConfig} config + * @param {ConsentData|undefined} consentData * @param {Object} cacheIdObj - existing id, if any * @return {(IdResponse|function(callback:function))} A response object that contains id and/or callback. */ - extendId(config, cacheIdObj) { + extendId(config, consentData, cacheIdObj) { const partnerId = (config && config.params && config.params.partner) || 0; incrementNb(partnerId); return cacheIdObj; @@ -247,4 +279,41 @@ export function storeInLocalStorage(key, value, expDays) { storage.setDataInLocalStorage(`${key}`, value); } +/** + * gets the existing abTesting config or generates a default config with abTesting off + * + * @param {SubmoduleConfig|undefined} config + * @returns {(Object|undefined)} + */ +function getAbTestingConfig(config) { + return (config && config.params && config.params.abTesting) || { enabled: false }; +} + +/** + * Return a consistant random number between 0 and ABTEST_RESOLUTION-1 for this user + * Falls back to plain random if no user provided + * @param {string} userId + * @returns {number} + */ +function abTestBucket(userId) { + if (userId) { + return ((utils.cyrb53Hash(userId) % ABTEST_RESOLUTION) + ABTEST_RESOLUTION) % ABTEST_RESOLUTION; + } else { + return Math.floor(Math.random() * ABTEST_RESOLUTION); + } +} + +/** + * Return a consistant boolean if this user is within the control group ratio provided + * @param {string} userId + * @param {number} controlGroupPct - Ratio [0,1] of users expected to be in the control group + * @returns {boolean} + */ +export function isInControlGroup(userId, controlGroupPct) { + if (!utils.isNumber(controlGroupPct) || controlGroupPct < 0 || controlGroupPct > 1) { + return undefined; + } + return abTestBucket(userId) < controlGroupPct * ABTEST_RESOLUTION; +} + submodule('userId', id5IdSubmodule); diff --git a/modules/id5IdSystem.md b/modules/id5IdSystem.md index e5e3969c19c..6a662361492 100644 --- a/modules/id5IdSystem.md +++ b/modules/id5IdSystem.md @@ -1,6 +1,6 @@ # ID5 Universal ID -The ID5 Universal ID is a shared, neutral identifier that publishers and ad tech platforms can use to recognise users even in environments where 3rd party cookies are not available. The ID5 Universal ID is designed to respect users' privacy choices and publishers’ preferences throughout the advertising value chain. For more information about the ID5 Universal ID and detailed integration docs, please visit [our documentation](https://console.id5.io/docs/public/prebid). We also recommend that you sign up for our [release notes](https://id5.io/universal-id/release-notes) to stay up-to-date with any changes to the implementation of the ID5 Universal ID in Prebid. +The ID5 Universal ID is a shared, neutral identifier that publishers and ad tech platforms can use to recognise users even in environments where 3rd party cookies are not available. The ID5 Universal ID is designed to respect users' privacy choices and publishers’ preferences throughout the advertising value chain. For more information about the ID5 Universal ID and detailed integration docs, please visit [our documentation](https://wiki.id5.io/x/BIAZ). We also recommend that you sign up for our [release notes](https://id5.io/universal-id/release-notes) to stay up-to-date with any changes to the implementation of the ID5 Universal ID in Prebid. ## ID5 Universal ID Registration @@ -22,14 +22,18 @@ The following configuration parameters are available: pbjs.setConfig({ userSync: { userIds: [{ - name: "id5Id", + name: 'id5Id', params: { partner: 173, // change to the Partner Number you received from ID5 - pd: "MT1iNTBjY..." // optional, see table below for a link to how to generate this + pd: 'MT1iNTBjY...', // optional, see table below for a link to how to generate this + abTesting: { // optional + enabled: true, // false by default + controlGroupPct: 0.1 // valid values are 0.0 - 1.0 (inclusive) + } }, storage: { - type: "html5", // "html5" is the required storage type - name: "id5id", // "id5id" is the required storage name + type: 'html5', // "html5" is the required storage type + name: 'id5id', // "id5id" is the required storage name expires: 90, // storage lasts for 90 days refreshInSeconds: 8*3600 // refresh ID every 8 hours to ensure it's fresh } @@ -46,6 +50,9 @@ pbjs.setConfig({ | params.partner | Required | Number | This is the ID5 Partner Number obtained from registering with ID5. | `173` | | params.pd | Optional | String | Publisher-supplied data used for linking ID5 IDs across domains. See [our documentation](https://wiki.id5.io/x/BIAZ) for details on generating the string. Omit the parameter or leave as an empty string if no data to supply | `"MT1iNTBjY..."` | | params.provider | Optional | String | An identifier provided by ID5 to technology partners who manage Prebid setups on behalf of publishers. Reach out to [ID5](mailto:prebid@id5.io) if you have questions about this parameter | `pubmatic-identity-hub` | +| params.abTesting | Optional | Object | Allows publishers to easily run an A/B Test. If enabled and the user is in the Control Group, the ID5 ID will NOT be exposed to bid adapters for that request | Disabled by default | +| params.abTesting.enabled | Optional | Boolean | Set this to `true` to turn on this feature | `true` or `false` | +| params.abTesting.controlGroupPct | Optional | Number | Must be a number between `0.0` and `1.0` (inclusive) and is used to determine the percentage of requests that fall into the control group (and thus not exposing the ID5 ID). For example, a value of `0.20` will result in 20% of requests without an ID5 ID and 80% with an ID. | `0.1` | | storage | Required | Object | Storage settings for how the User ID module will cache the ID5 ID locally | | | storage.type | Required | String | This is where the results of the user ID will be stored. ID5 **requires** `"html5"`. | `"html5"` | | storage.name | Required | String | The name of the local storage where the user ID will be stored. ID5 **requires** `"id5id"`. | `"id5id"` | @@ -53,3 +60,9 @@ pbjs.setConfig({ | storage.refreshInSeconds | Optional | Integer | How many seconds until the ID5 ID will be refreshed. ID5 strongly recommends 8 hours between refreshes | `8*3600` | **ATTENTION:** As of Prebid.js v4.14.0, ID5 requires `storage.type` to be `"html5"` and `storage.name` to be `"id5id"`. Using other values will display a warning today, but in an upcoming release, it will prevent the ID5 module from loading. This change is to ensure the ID5 module in Prebid.js interoperates properly with the [ID5 API](https://github.com/id5io/id5-api.js) and to reduce the size of publishers' first-party cookies that are sent to their web servers. If you have any questions, please reach out to us at [prebid@id5.io](mailto:prebid@id5.io). + +### A Note on A/B Testing + +Publishers may want to test the value of the ID5 ID with their downstream partners. While there are various ways to do this, A/B testing is a standard approach. Instead of publishers manually enabling or disabling the ID5 User ID Module based on their control group settings (which leads to fewer calls to ID5, reducing our ability to recognize the user), we have baked this in to our module directly. + +To turn on A/B Testing, simply edit the configuration (see above table) to enable it and set what percentage of users you would like to set for the control group. The control group is the set of user where an ID5 ID will not be exposed in to bid adapters or in the various user id functions available on the `pbjs` global. An additional value of `ext.abTestingControlGroup` will be set to `true` or `false` that can be used to inform reporting systems that the user was in the control group or not. It's important to note that the control group is user based, and not request based. In other words, from one page view to another, a user will always be in or out of the control group. diff --git a/modules/idLibrary.js b/modules/idImportLibrary.js similarity index 96% rename from modules/idLibrary.js rename to modules/idImportLibrary.js index ba3cc0b5efb..2a3a86cf270 100644 --- a/modules/idLibrary.js +++ b/modules/idImportLibrary.js @@ -8,7 +8,7 @@ let email; let conf; const LOG_PRE_FIX = 'ID-Library: '; const CONF_DEFAULT_OBSERVER_DEBOUNCE_MS = 250; -const CONF_DEFAULT_FULL_BODY_SCAN = true; +const CONF_DEFAULT_FULL_BODY_SCAN = false; const OBSERVER_CONFIG = { subtree: true, attributes: true, @@ -38,7 +38,7 @@ function getEmail(value) { if (!matched) { return null; } - logInfo('Email found' + matched[0]); + logInfo('Email found: ' + matched[0]); return matched[0]; } @@ -200,10 +200,10 @@ function postData() { logInfo('No user ids'); return; } - logInfo('Users' + JSON.stringify(userIds)); + logInfo('Users' + userIds); const syncPayload = {}; syncPayload.hid = MD5(email).toString(); - syncPayload.uids = JSON.stringify(userIds); + syncPayload.uids = userIds; const payloadString = JSON.stringify(syncPayload); logInfo(payloadString); ajax(conf.url, syncCallback(), payloadString, {method: 'POST', withCredentials: true}); @@ -240,4 +240,4 @@ export function setConfig(config) { associateIds(); } -config.getConfig('idLibrary', config => setConfig(config.idLibrary)); +config.getConfig('idImportLibrary', config => setConfig(config.idImportLibrary)); diff --git a/modules/idImportLibrary.md b/modules/idImportLibrary.md new file mode 100644 index 00000000000..3dd78ee25d8 --- /dev/null +++ b/modules/idImportLibrary.md @@ -0,0 +1,22 @@ +# ID Import Library + +## Configuration Options + +| Parameter | Required | Type | Default | Description | +| :--------- | :------- | :------ | :------ | :---------- | +| `target` | Yes | String | N/A | ID attribute of the element from which the email can be read. | +| `url` | Yes | String | N/A | URL endpoint used to post the hashed email and user IDs. | +| `debounce` | No | Number | 250 | Time in milliseconds before the email and IDs are fetched. | +| `fullscan` | No | Boolean | false | Enable/disable a full page body scan to get email. | + +## Example + +```javascript +pbjs.setConfig({ + idImportLibrary: { + target: 'username', + url: 'https://example.com', + debounce: 250, + fullscan: false, + }, +}); diff --git a/modules/idLibrary.md b/modules/idLibrary.md deleted file mode 100644 index 69b63dc466b..00000000000 --- a/modules/idLibrary.md +++ /dev/null @@ -1,24 +0,0 @@ -## ID Library Configuration Example - - -|Param |Required |Description | -|----------------|-------------------------------|-----------------------------| -|url |Yes | The url endpoint is used to post the hashed email and user ids. | -|target |No |It should contain the element id from which the email can be read. | -|debounce |No | Time in milliseconds before the email and ids are fetched | -|fullscan |No | Option to enable/disable full body scan to get email. By default the full body scan is enabled. | - -### Example -``` - pbjs.setConfig({ - idLibrary:{ - url: , - debounce: 250, - target: 'username', - fullscan: false - }, - }); -``` - - -``` diff --git a/modules/identityLinkIdSystem.js b/modules/identityLinkIdSystem.js index 33dd74380ac..716929db70a 100644 --- a/modules/identityLinkIdSystem.js +++ b/modules/identityLinkIdSystem.js @@ -43,7 +43,7 @@ export const identityLinkSubmodule = { getId(config, consentData) { const configParams = (config && config.params) || {}; if (!configParams || typeof configParams.pid !== 'string') { - utils.logError('identityLink submodule requires partner id to be defined'); + utils.logError('identityLink: requires partner id to be defined'); return; } const hasGdpr = (consentData && typeof consentData.gdprApplies === 'boolean' && consentData.gdprApplies) ? 1 : 0; @@ -51,7 +51,7 @@ export const identityLinkSubmodule = { const tcfPolicyV2 = utils.deepAccess(consentData, 'vendorData.tcfPolicyVersion') === 2; // use protocol relative urls for http or https if (hasGdpr && (!gdprConsentString || gdprConsentString === '')) { - utils.logInfo('Consent string is required to call envelope API.'); + utils.logInfo('identityLink: Consent string is required to call envelope API.'); return; } const url = `https://api.rlcdn.com/api/identity/envelope?pid=${configParams.pid}${hasGdpr ? (tcfPolicyV2 ? '&ct=4&cv=' : '&ct=1&cv=') + gdprConsentString : ''}`; @@ -60,10 +60,11 @@ export const identityLinkSubmodule = { // Check ats during callback so it has a chance to initialise. // If ats library is available, use it to retrieve envelope. If not use standard third party endpoint if (window.ats) { - utils.logInfo('ATS exists!'); + utils.logInfo('identityLink: ATS exists!'); window.ats.retrieveEnvelope(function (envelope) { if (envelope) { - utils.logInfo('An envelope can be retrieved from ATS!'); + utils.logInfo('identityLink: An envelope can be retrieved from ATS!'); + setEnvelopeSource(true); callback(JSON.parse(envelope).envelope); } else { getEnvelope(url, callback); @@ -92,14 +93,15 @@ function getEnvelope(url, callback) { callback((responseObj && responseObj.envelope) ? responseObj.envelope : ''); }, error: error => { - utils.logInfo(`identityLink: ID fetch encountered an error`, error); + utils.logInfo(`identityLink: identityLink: ID fetch encountered an error`, error); callback(); } }; if (!storage.getCookie('_lr_retry_request')) { setRetryCookie(); - utils.logInfo('A 3P retrieval is attempted!'); + utils.logInfo('identityLink: A 3P retrieval is attempted!'); + setEnvelopeSource(false); ajax(url, callbacks, undefined, { method: 'GET', withCredentials: true }); } } @@ -110,4 +112,10 @@ function setRetryCookie() { storage.setCookie('_lr_retry_request', 'true', now.toUTCString()); } +function setEnvelopeSource(src) { + let now = new Date(); + now.setTime(now.getTime() + 2592000000); + storage.setCookie('_lr_env_src_ats', src, now.toUTCString()); +} + submodule('userId', identityLinkSubmodule); diff --git a/modules/improvedigitalBidAdapter.js b/modules/improvedigitalBidAdapter.js index 0e2d8f6f7dd..0432adf2b2d 100644 --- a/modules/improvedigitalBidAdapter.js +++ b/modules/improvedigitalBidAdapter.js @@ -4,12 +4,14 @@ import { config } from '../src/config.js'; import { BANNER, NATIVE, VIDEO } from '../src/mediaTypes.js'; import {Renderer} from '../src/Renderer.js'; import { createEidsArray } from './userId/eids.js'; +import includes from 'core-js-pure/features/array/includes.js'; const BIDDER_CODE = 'improvedigital'; const RENDERER_URL = 'https://acdn.adnxs.com/video/outstream/ANOutstreamVideo.js'; +const VIDEO_TARGETING = ['skip', 'skipmin', 'skipafter']; export const spec = { - version: '7.1.0', + version: '7.2.0', code: BIDDER_CODE, gvlid: 253, aliases: ['id'], @@ -202,6 +204,21 @@ function isOutstreamVideo(bid) { 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; +} + function outstreamRender(bid) { bid.renderer.push(() => { window.ANOutstreamVideo.renderAd({ @@ -255,6 +272,9 @@ function getNormalizedBidRequest(bid) { if (isInstreamVideo(bid)) { normalizedBidRequest.adTypes = [ VIDEO ]; } + if (isInstreamVideo(bid) || isOutstreamVideo(bid)) { + normalizedBidRequest.video = getVideoTargetingParams(bid); + } if (placementId) { normalizedBidRequest.placementId = placementId; } else { @@ -400,7 +420,7 @@ export function ImproveDigitalAdServerJSClient(endPoint) { AD_SERVER_BASE_URL: 'ice.360yield.com', END_POINT: endPoint || 'hb', AD_SERVER_URL_PARAM: 'jsonp=', - CLIENT_VERSION: 'JS-6.3.0', + CLIENT_VERSION: 'JS-6.4.0', MAX_URL_LENGTH: 2083, ERROR_CODES: { MISSING_PLACEMENT_PARAMS: 2, @@ -609,6 +629,21 @@ export function ImproveDigitalAdServerJSClient(endPoint) { if (placementObject.transactionId) { impressionObject.tid = placementObject.transactionId; } + if (!utils.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) { + utils.logWarn(`video.skip: invalid value '${video.skip}'. Expected 0 or 1`); + delete video.skip; + } + } + if (!utils.isEmpty(video)) { + impressionObject.video = video; + } + } if (placementObject.keyValues) { for (let key in placementObject.keyValues) { for (let valueCounter = 0; valueCounter < placementObject.keyValues[key].length; valueCounter++) { diff --git a/modules/inmarBidAdapter.js b/modules/inmarBidAdapter.js index b5ab72266fc..7583985b23c 100755 --- a/modules/inmarBidAdapter.js +++ b/modules/inmarBidAdapter.js @@ -17,7 +17,7 @@ export const spec = { * @returns {boolean} True if this is a valid bid, and false otherwise */ isBidRequestValid: function(bid) { - return !!(bid.params && bid.params.partnerId && bid.params.adnetId); + return !!(bid.params && bid.params.partnerId); }, /** @@ -49,9 +49,6 @@ export const spec = { return { method: 'POST', url: 'https://prebid.owneriq.net:8443/bidder/pb/bid', - options: { - withCredentials: false - }, data: payloadString, }; }, diff --git a/modules/inmarBidAdapter.md b/modules/inmarBidAdapter.md index 1bacb30f2dd..8ed6b998602 100644 --- a/modules/inmarBidAdapter.md +++ b/modules/inmarBidAdapter.md @@ -25,7 +25,6 @@ Please reach out to your account manager for more information. bidder: 'inmar', params: { partnerId: 12345, - adnetId: 'ADb1f40rmi', position: 1 } }] @@ -37,7 +36,6 @@ Please reach out to your account manager for more information. bidder: 'inmar', params: { partnerId: 12345, - adnetId: 'ADb1f40rmo', position: 0 } }] diff --git a/modules/inskinBidAdapter.js b/modules/inskinBidAdapter.js index 2a55b5280db..29040205818 100644 --- a/modules/inskinBidAdapter.js +++ b/modules/inskinBidAdapter.js @@ -4,9 +4,7 @@ import { registerBidder } from '../src/adapters/bidderFactory.js'; const BIDDER_CODE = 'inskin'; const CONFIG = { - 'inskin': { - 'BASE_URI': 'https://mfad.inskinad.com/api/v2' - } + BASE_URI: 'https://mfad.inskinad.com/api/v2' }; export const spec = { @@ -97,8 +95,7 @@ export const spec = { } validBidRequests.map(bid => { - let config = CONFIG[bid.bidder]; - ENDPOINT_URL = config.BASE_URI; + ENDPOINT_URL = CONFIG.BASE_URI; const placement = Object.assign({ divName: bid.bidId, @@ -108,8 +105,12 @@ export const spec = { placement.adTypes.push(5, 9, 163, 2163, 3006); + placement.properties = placement.properties || {}; + + placement.properties.screenWidth = screen.width; + placement.properties.screenHeight = screen.height; + if (restrictions.length) { - placement.properties = placement.properties || {}; placement.properties.restrictions = restrictions; } @@ -188,7 +189,7 @@ export const spec = { const id = 'ism_tag_' + Math.floor((Math.random() * 10e16)); window[id] = { - plr_AdSlot: e.source.frameElement, + plr_AdSlot: e.source && e.source.frameElement, bidId: e.data.bidId, bidPrice: bidsMap[e.data.bidId].price, serverResponse diff --git a/modules/ipromBidAdapter.js b/modules/ipromBidAdapter.js new file mode 100644 index 00000000000..e328cd1ec5d --- /dev/null +++ b/modules/ipromBidAdapter.js @@ -0,0 +1,72 @@ +import * as utils from '../src/utils.js'; +import { registerBidder } from '../src/adapters/bidderFactory.js'; + +const BIDDER_CODE = 'iprom'; +const ENDPOINT_URL = 'https://core.iprom.net/programmatic'; +const VERSION = 'v1.0.0'; +const DEFAULT_CURRENCY = 'EUR'; +const DEFAULT_NETREVENUE = true; +const DEFAULT_TTL = 360; + +export const spec = { + code: BIDDER_CODE, + isBidRequestValid: function ({ bidder, params = {} } = {}) { + // id parameter checks + if (!params.id) { + utils.logError(`${bidder}: Parameter 'id' missing`); + return false; + } else if (typeof params.id !== 'string') { + utils.logError(`${bidder}: Parameter 'id' needs to be a string`); + return false; + } + // dimension parameter checks + if (!params.dimension) { + utils.logError(`${bidder}: Required parameter 'dimension' missing`); + return false; + } else if (typeof params.dimension !== 'string') { + utils.logError(`${bidder}: Parameter 'dimension' needs to be a string`); + return false; + } + + return true; + }, + + buildRequests: function (validBidRequests, bidderRequest) { + const payload = { + bids: validBidRequests, + referer: bidderRequest.refererInfo, + version: VERSION + }; + const payloadString = JSON.stringify(payload); + + return { + method: 'POST', + url: ENDPOINT_URL, + data: payloadString + }; + }, + + interpretResponse: function (serverResponse, request) { + let bids = serverResponse.body; + + const bidResponses = []; + + bids.forEach(bid => { + bidResponses.push({ + ad: bid.ad, + requestId: bid.requestId, + cpm: bid.cpm, + width: bid.width, + height: bid.height, + creativeId: bid.creativeId, + currency: bid.currency || DEFAULT_CURRENCY, + netRevenue: bid.netRevenue || DEFAULT_NETREVENUE, + ttl: bid.ttl || DEFAULT_TTL, + }); + }); + + return bidResponses; + }, +} + +registerBidder(spec); diff --git a/modules/ipromBidAdapter.md b/modules/ipromBidAdapter.md new file mode 100644 index 00000000000..f7124e7c89c --- /dev/null +++ b/modules/ipromBidAdapter.md @@ -0,0 +1,34 @@ +# Overview + +``` +Module Name: Iprom PreBid Adapter +Module Type: Bidder Adapter +Maintainer: support@iprom.si +``` + +# Description + +Module that connects to Iprom's demand sources + +# Test Parameters +``` + var adUnits = [ + { + code: 'test-div', + mediaTypes: { + banner: { + sizes: [[300, 250]], // a display size + } + }, + bids: [ + { + bidder: "iprom", + params: { + id: '1234', + dimension: '300x250' + } + } + ] + } + ]; +``` diff --git a/modules/ironsourceBidAdapter.js b/modules/ironsourceBidAdapter.js index e23c0cb857d..ba510e86e7f 100644 --- a/modules/ironsourceBidAdapter.js +++ b/modules/ironsourceBidAdapter.js @@ -243,7 +243,7 @@ function generateParameters(bid, bidderRequest) { if (bidderRequest && bidderRequest.refererInfo) { requestParams.referrer = utils.deepAccess(bidderRequest, 'refererInfo.referer'); - requestParams.page_url = utils.deepAccess(bidderRequest, 'refererInfo.canonicalUrl') || config.getConfig('pageUrl') || utils.deepAccess(window, 'location.href'); + requestParams.page_url = config.getConfig('pageUrl') || utils.deepAccess(window, 'location.href'); } return requestParams; diff --git a/modules/ixBidAdapter.js b/modules/ixBidAdapter.js index cb37e4735c5..7ddad134451 100644 --- a/modules/ixBidAdapter.js +++ b/modules/ixBidAdapter.js @@ -44,12 +44,20 @@ function bidToBannerImp(bid) { */ function bidToVideoImp(bid) { const imp = bidToImp(bid); + const videoAdUnitRef = utils.deepAccess(bid, 'mediaTypes.video'); + const context = utils.deepAccess(bid, 'mediaTypes.video.context'); + const videoAdUnitWhitelist = [ + 'mimes', 'minduration', 'maxduration', 'protocols', 'protocol', + 'startdelay', 'placement', 'linearity', 'skip', 'skipmin', + 'skipafter', 'sequence', 'battr', 'maxextended', 'minbitrate', + 'maxbitrate', 'boxingallowed', 'playbackmethod', 'playbackend', + 'delivery', 'pos', 'companionad', 'api', 'companiontype', 'ext' + ]; imp.video = utils.deepClone(bid.params.video) imp.video.w = bid.params.size[0]; imp.video.h = bid.params.size[1]; - const context = utils.deepAccess(bid, 'mediaTypes.video.context'); if (context) { if (context === 'instream') { imp.video.placement = 1; @@ -60,6 +68,12 @@ function bidToVideoImp(bid) { } } + for (let adUnitProperty in videoAdUnitRef) { + if (videoAdUnitWhitelist.indexOf(adUnitProperty) !== -1 && !imp.video.hasOwnProperty(adUnitProperty)) { + imp.video[adUnitProperty] = videoAdUnitRef[adUnitProperty]; + } + } + return imp; } @@ -198,36 +212,35 @@ function getBidRequest(id, impressions) { } /** - * Adds a User ID module's response into user Eids array. - * - * @param {array} userEids An array of objects containing user ids, - * will be attached to bid request later. - * @param {object} seenIdPartners An object with Identity partners names already added, - * updated with new partner name. - * @param {*} id The id obtained from User ID module. - * @param {string} source The URL of the User ID module. - * @param {string} ixlPartnerName The name of the Identity Partner in IX Library. - * @param {string} rtiPartner The name of the User ID provider in Prebid. - * @return {boolean} True if successfully added the ID to the userEids, false otherwise. + * From the userIdAsEids array, filter for the ones our adserver can use, and modify them + * for our purposes, e.g. add rtiPartner + * @param {array} allEids userIdAsEids passed in by prebid + * @return {object} contains toSend (eids to send to the adserver) and seenSources (used to filter + * identity info from IX Library) */ -function addUserEids(userEids, seenIdPartners, id, source, ixlPartnerName, rtiPartner) { - if (id) { - // mark the partnername that IX RTI uses - seenIdPartners[ixlPartnerName] = 1; - userEids.push({ - source: source, - uids: [{ - id: id, - ext: { - rtiPartner: rtiPartner - } - }] - }); - return true; +function getEidInfo(allEids) { + // determines which eids we send and the rtiPartner field in ext + var sourceRTIMapping = { + 'liveramp.com': 'idl', + 'netid.de': 'NETID', + 'neustar.biz': 'fabrickId', + 'zeotap.com': 'zeotapIdPlus' + }; + var toSend = []; + var seenSources = {}; + if (utils.isArray(allEids)) { + for (var i = 0; i < allEids.length; i++) { + if (sourceRTIMapping[allEids[i].source] && utils.deepAccess(allEids[i], 'uids.0')) { + seenSources[allEids[i].source] = 1; + allEids[i].uids[0].ext = { + rtiPartner: sourceRTIMapping[allEids[i].source] + }; + delete allEids[i].uids[0].atype; + toSend.push(allEids[i]); + } + } } - - utils.logWarn('Tried to add a user ID from Prebid, the ID received was null'); - return false; + return { toSend: toSend, seenSources: seenSources }; } /** @@ -241,21 +254,12 @@ function addUserEids(userEids, seenIdPartners, id, source, ixlPartnerName, rtiPa * */ function buildRequest(validBidRequests, bidderRequest, impressions, version) { - const userEids = []; - // Always use secure HTTPS protocol. let baseUrl = SECURE_BID_URL; - // Dict for identity partners already populated from prebid - let seenIdPartners = {}; - // Get ids from Prebid User ID Modules - const userId = validBidRequests[0].userId; - if (userId && typeof userId === 'object') { - if (userId.idl_env) { - addUserEids(userEids, seenIdPartners, userId.idl_env, 'liveramp.com', 'LiveRampIp', 'idl'); - } - } + var eidInfo = getEidInfo(utils.deepAccess(validBidRequests, '0.userIdAsEids')); + var userEids = eidInfo.toSend; // RTI ids will be included in the bid request if the function getIdentityInfo() is loaded // and if the data for the partner exist @@ -264,12 +268,10 @@ function buildRequest(validBidRequests, bidderRequest, impressions, version) { if (identityInfo && typeof identityInfo === 'object') { for (const partnerName in identityInfo) { if (identityInfo.hasOwnProperty(partnerName)) { - // check if not already populated by prebid cache - if (!seenIdPartners.hasOwnProperty(partnerName)) { - let response = identityInfo[partnerName]; - if (!response.responsePending && response.data && typeof response.data === 'object' && Object.keys(response.data).length) { - userEids.push(response.data); - } + let response = identityInfo[partnerName]; + if (!response.responsePending && response.data && typeof response.data === 'object' && + Object.keys(response.data).length && !eidInfo.seenSources[response.data.source]) { + userEids.push(response.data); } } } @@ -285,6 +287,12 @@ function buildRequest(validBidRequests, bidderRequest, impressions, version) { r.ext.source = 'prebid'; r.ext.ixdiag = {}; + // getting ixdiags for adunits of the video, outstream & multi format (MF) style + let ixdiag = buildIXDiag(validBidRequests); + for (var key in ixdiag) { + r.ext.ixdiag[key] = ixdiag[key]; + } + // if an schain is provided, send it along if (validBidRequests[0].schain) { r.source = { @@ -321,6 +329,12 @@ function buildRequest(validBidRequests, bidderRequest, impressions, version) { r.user.ext = { consent: gdprConsent.consentString || '' }; + + if (gdprConsent.hasOwnProperty('addtlConsent') && gdprConsent.addtlConsent) { + r.user.ext.consented_providers_settings = { + consented_providers: gdprConsent.addtlConsent + } + } } } @@ -382,7 +396,7 @@ function buildRequest(validBidRequests, bidderRequest, impressions, version) { data: payload }; - const BASE_REQ_SIZE = new Blob([`${request.url}${utils.parseQueryStringParameters({...request.data, r: JSON.stringify(r)})}`]).size; + const BASE_REQ_SIZE = new Blob([`${request.url}${utils.parseQueryStringParameters({ ...request.data, r: JSON.stringify(r) })}`]).size; let currReqSize = BASE_REQ_SIZE; const MAX_REQ_SIZE = 8000; @@ -466,6 +480,67 @@ function buildRequest(validBidRequests, bidderRequest, impressions, version) { return requests; } + +/** + * Calculates IX diagnostics values and packages them into an object + * + * @param {array} validBidRequests The valid bid requests from prebid + * @return {Object} IX diag values for ad units + */ +function buildIXDiag(validBidRequests) { + var adUnitMap = validBidRequests + .map(bidRequest => bidRequest.transactionId) + .filter((value, index, arr) => arr.indexOf(value) === index) + + var ixdiag = { + mfu: 0, + bu: 0, + iu: 0, + nu: 0, + ou: 0, + allU: 0, + ren: false, + version: '$prebid.version$' + }; + + // create ad unit map and collect the required diag properties + for (let i = 0; i < adUnitMap.length; i++) { + var bid = validBidRequests.filter(bidRequest => bidRequest.transactionId === adUnitMap[i])[0]; + + if (utils.deepAccess(bid, 'mediaTypes')) { + if (Object.keys(bid.mediaTypes).length > 1) { + ixdiag.mfu++; + } + + if (utils.deepAccess(bid, 'mediaTypes.native')) { + ixdiag.nu++; + } + + if (utils.deepAccess(bid, 'mediaTypes.banner')) { + ixdiag.bu++; + } + + if (utils.deepAccess(bid, 'mediaTypes.video.context') === 'outstream') { + ixdiag.ou++; + // renderer only needed for outstream + + const hasRenderer = typeof (utils.deepAccess(bid, 'renderer') || utils.deepAccess(bid, 'mediaTypes.video.renderer')) === 'object'; + + // if any one ad unit is missing renderer, set ren status to false in diag + ixdiag.ren = ixdiag.ren && hasRenderer ? (utils.deepAccess(ixdiag, 'ren')) : hasRenderer; + } + + if (utils.deepAccess(bid, 'mediaTypes.video.context') === 'instream') { + ixdiag.iu++; + } + + ixdiag.allU++; + } + } + + return ixdiag; +} + /** * * @param {Object} impressions containing ixImps and possibly missingImps @@ -525,7 +600,8 @@ function updateMissingSizes(validBidRequest, missingBannerSizes, imp) { if (utils.deepAccess(validBidRequest, 'mediaTypes.banner.sizes')) { let sizeList = utils.deepClone(validBidRequest.mediaTypes.banner.sizes); removeFromSizes(sizeList, validBidRequest.params.size); - let newAdUnitEntry = { 'missingSizes': sizeList, + let newAdUnitEntry = { + 'missingSizes': sizeList, 'impression': imp }; missingBannerSizes[transactionID] = newAdUnitEntry; @@ -560,32 +636,57 @@ export const spec = { * @return {boolean} True if this is a valid bid, and false otherwise. */ isBidRequestValid: function (bid) { + const paramsVideoRef = utils.deepAccess(bid, 'params.video'); + const paramsSize = utils.deepAccess(bid, 'params.size'); + const mediaTypeBannerSizes = utils.deepAccess(bid, 'mediaTypes.banner.sizes'); + const mediaTypeVideoRef = utils.deepAccess(bid, 'mediaTypes.video'); + const mediaTypeVideoPlayerSize = utils.deepAccess(bid, 'mediaTypes.video.playerSize'); + const hasBidFloor = bid.params.hasOwnProperty('bidFloor'); + const hasBidFloorCur = bid.params.hasOwnProperty('bidFloorCur'); + if (!isValidSize(bid.params.size)) { utils.logError('ix bidder params: bid size has invalid format.'); return false; } - if (!includesSize(bid.sizes, bid.params.size)) { - utils.logError('ix bidder params: bid size is not included in ad unit sizes.'); + if (bid.hasOwnProperty('mediaType') && !(utils.contains(SUPPORTED_AD_TYPES, bid.mediaType))) { return false; } - if (bid.hasOwnProperty('mediaType') && !(utils.contains(SUPPORTED_AD_TYPES, bid.mediaType))) { + if (bid.hasOwnProperty('mediaTypes') && !(mediaTypeBannerSizes || mediaTypeVideoPlayerSize)) { return false; } - if (bid.hasOwnProperty('mediaTypes') && !(utils.deepAccess(bid, 'mediaTypes.banner.sizes') || utils.deepAccess(bid, 'mediaTypes.video.playerSize'))) { + if (!includesSize(bid.sizes, paramsSize) && !((mediaTypeVideoPlayerSize && includesSize(mediaTypeVideoPlayerSize, paramsSize)) || + (mediaTypeBannerSizes && includesSize(mediaTypeBannerSizes, paramsSize)))) { + utils.logError('ix bidder params: bid size is not included in ad unit sizes or player size.'); return false; } + if (mediaTypeVideoRef && paramsVideoRef) { + const requiredIXParams = ['mimes', 'minduration', 'maxduration', 'protocols']; + let isParamsLevelValid = true; + for (let property of requiredIXParams) { + if (!mediaTypeVideoRef.hasOwnProperty(property) && !paramsVideoRef.hasOwnProperty(property)) { + const isProtocolsValid = (property === 'protocols' && (mediaTypeVideoRef.hasOwnProperty('protocol') || paramsVideoRef.hasOwnProperty('protocol'))); + if (isProtocolsValid) { + continue; + } + utils.logError('ix bidder params: ' + property + ' is not included in either the adunit or params level'); + isParamsLevelValid = false; + } + } + + if (!isParamsLevelValid) { + return false; + } + } + if (typeof bid.params.siteId !== 'string' && typeof bid.params.siteId !== 'number') { utils.logError('ix bidder params: siteId must be string or number value.'); return false; } - const hasBidFloor = bid.params.hasOwnProperty('bidFloor'); - const hasBidFloorCur = bid.params.hasOwnProperty('bidFloorCur'); - if (hasBidFloor || hasBidFloorCur) { if (!(hasBidFloor && hasBidFloorCur && isValidBidFloorParams(bid.params.bidFloor, bid.params.bidFloorCur))) { utils.logError('ix bidder params: bidFloor / bidFloorCur parameter has invalid format.'); @@ -616,7 +717,7 @@ export const spec = { detectMissingSizes: true, }; - const ixConfig = {...DEFAULT_IX_CONFIG, ...config.getConfig('ix')}; + const ixConfig = { ...DEFAULT_IX_CONFIG, ...config.getConfig('ix') }; for (let i = 0; i < validBidRequests.length; i++) { validBidRequest = validBidRequests[i]; @@ -631,11 +732,10 @@ export const spec = { } videoImps[validBidRequest.transactionId].ixImps.push(bidToVideoImp(validBidRequest)); - } else { - utils.logError('Bid size is not included in video playerSize') } } - if (validBidRequest.mediaType === BANNER || utils.deepAccess(validBidRequest, 'mediaTypes.banner') || + if (validBidRequest.mediaType === BANNER || + (utils.deepAccess(validBidRequest, 'mediaTypes.banner') && includesSize(utils.deepAccess(validBidRequest, 'mediaTypes.banner.sizes'), validBidRequest.params.size)) || (!validBidRequest.mediaType && !validBidRequest.mediaTypes)) { let imp = bidToBannerImp(validBidRequest); @@ -726,7 +826,7 @@ export const spec = { * @param {Boolean} isOpenRtb boolean to check openrtb2 protocol * @return {Object} params bid params */ - transformBidParams: function(params, isOpenRtb) { + transformBidParams: function (params, isOpenRtb) { return utils.convertTypes({ 'siteID': 'number' }, params); diff --git a/modules/ixBidAdapter.md b/modules/ixBidAdapter.md index 5b9903c91d2..c358b19a0a2 100644 --- a/modules/ixBidAdapter.md +++ b/modules/ixBidAdapter.md @@ -87,7 +87,7 @@ object are detailed here. | --- | --- | --- | --- | siteId | Required | String | An IX-specific identifier that is associated with a specific size on this ad unit. This is similar to a placement ID or an ad unit ID that some other modules have. Examples: `'3723'`, `'6482'`, `'3639'` | size | Required | Number[] | The single size associated with the site ID. It should be one of the sizes listed in the ad unit under `adUnits[].sizes` or `adUnits[].mediaTypes.video.playerSize`. Examples: `[300, 250]`, `[300, 600]` -| video | Required | Hash | The video object will serve as the properties of the video ad. You can create any field under the video object that is mentioned in the `OpenRTB Spec v2.5`. Some fields like `mimes, protocols, minduration, maxduration` are required. +| video | Required | Hash | The video object will serve as the properties of the video ad. You can create any field under the video object that is mentioned in the `OpenRTB Spec v2.5`. Some fields like `mimes, protocols, minduration, maxduration` are required. Properties not defined at this level, will be pulled from the Adunit level. | video.mimes | Required | String[] | Array list of content MIME types supported. Popular MIME types include, but are not limited to, `"video/x-ms- wmv"` for Windows Media and `"video/x-flv"` for Flash Video. |video.minduration| Required | Integer | Minimum video ad duration in seconds. |video.maxduration| Required | Integer | Maximum video ad duration in seconds. diff --git a/modules/jixieBidAdapter.js b/modules/jixieBidAdapter.js index d3ae090964c..7c6e0027482 100644 --- a/modules/jixieBidAdapter.js +++ b/modules/jixieBidAdapter.js @@ -9,7 +9,7 @@ import { Renderer } from '../src/Renderer.js'; export const storage = getStorageManager(); const BIDDER_CODE = 'jixie'; -const EVENTS_URL = 'https://jxhbtrackers.azurewebsites.net/sync/evt?'; +const EVENTS_URL = 'https://hbtra.jixie.io/sync/hb?'; const JX_OUTSTREAM_RENDERER_URL = 'https://scripts.jixie.io/jxhboutstream.js'; const REQUESTS_URL = 'https://hb.jixie.io/v2/hbpost'; const sidTTLMins_ = 30; diff --git a/modules/jwplayerRtdProvider.js b/modules/jwplayerRtdProvider.js index 197c3c192c8..a29c4ce5fa7 100644 --- a/modules/jwplayerRtdProvider.js +++ b/modules/jwplayerRtdProvider.js @@ -146,6 +146,11 @@ function enrichBidRequest(bidReqConfig, onDone) { export function enrichAdUnits(adUnits) { const fpdFallback = config.getConfig('fpd.context.data.jwTargeting'); adUnits.forEach(adUnit => { + const jwTargeting = extractPublisherParams(adUnit, fpdFallback); + if (!jwTargeting || !Object.keys(jwTargeting).length) { + return; + } + const onVatResponse = function (vat) { if (!vat) { return; @@ -153,25 +158,29 @@ export function enrichAdUnits(adUnits) { const targeting = formatTargetingResponse(vat); addTargetingToBids(adUnit.bids, targeting); }; - - const jwTargeting = extractPublisherParams(adUnit, fpdFallback); loadVat(jwTargeting, onVatResponse); }); } +function supportsInstreamVideo(mediaTypes) { + const video = mediaTypes && mediaTypes.video; + return video && video.context === 'instream'; +} + export function extractPublisherParams(adUnit, fallback) { let adUnitTargeting; try { adUnitTargeting = adUnit.fpd.context.data.jwTargeting; } catch (e) {} - return Object.assign({}, fallback, adUnitTargeting); -} -function loadVat(params, onCompletion) { - if (!params || !Object.keys(params).length) { + if (!adUnitTargeting && !supportsInstreamVideo(adUnit.mediaTypes)) { return; } + return Object.assign({}, fallback, adUnitTargeting); +} + +function loadVat(params, onCompletion) { const { playerID, mediaID } = params; if (pendingRequests[mediaID] !== undefined) { loadVatForPendingRequest(playerID, mediaID, onCompletion); diff --git a/modules/kubientBidAdapter.js b/modules/kubientBidAdapter.js new file mode 100644 index 00000000000..8f6ea53ecce --- /dev/null +++ b/modules/kubientBidAdapter.js @@ -0,0 +1,111 @@ +import {registerBidder} from '../src/adapters/bidderFactory.js'; +import {BANNER} from '../src/mediaTypes.js'; +import * as utils from '../src/utils.js'; + +const BIDDER_CODE = 'kubient'; +const END_POINT = 'https://kssp.kbntx.ch/pbjs'; +const VERSION = '1.0'; +const VENDOR_ID = 794; +export const spec = { + code: BIDDER_CODE, + gvlid: VENDOR_ID, + supportedMediaTypes: [BANNER], + isBidRequestValid: function (bid) { + return !!(bid && bid.params); + }, + buildRequests: function (validBidRequests, bidderRequest) { + if (!validBidRequests || !bidderRequest) { + return; + } + const result = validBidRequests.map(function (bid) { + let data = { + v: VERSION, + requestId: bid.bidderRequestId, + adSlots: [{ + bidId: bid.bidId, + zoneId: bid.params.zoneid || '', + floor: bid.params.floor || 0.0, + sizes: bid.sizes || [], + schain: bid.schain || {}, + mediaTypes: bid.mediaTypes + }], + referer: (bidderRequest.refererInfo && bidderRequest.refererInfo.referer) ? bidderRequest.refererInfo.referer : null, + tmax: bidderRequest.timeout, + gdpr: (bidderRequest.gdprConsent && bidderRequest.gdprConsent.gdprApplies) ? 1 : 0, + consent: (bidderRequest.gdprConsent && bidderRequest.gdprConsent.consentString) ? bidderRequest.gdprConsent.consentString : null, + consentGiven: kubientGetConsentGiven(bidderRequest.gdprConsent), + uspConsent: bidderRequest.uspConsent + }; + return { + method: 'POST', + url: END_POINT, + data: JSON.stringify(data) + }; + }); + return result; + }, + interpretResponse: function interpretResponse(serverResponse, request) { + if (!serverResponse || !serverResponse.body || !serverResponse.body.seatbid) { + return []; + } + let bidResponses = []; + serverResponse.body.seatbid.forEach(seatbid => { + let bids = seatbid.bid || []; + bids.forEach(bid => { + bidResponses.push({ + requestId: bid.bidId, + cpm: bid.price, + currency: bid.cur, + width: bid.w, + height: bid.h, + creativeId: bid.creativeId, + netRevenue: bid.netRevenue, + ttl: bid.ttl, + ad: bid.adm + }); + }); + }); + return bidResponses; + }, + getUserSyncs: function (syncOptions, serverResponses, gdprConsent, uspConsent) { + const syncs = []; + let gdprParams = ''; + if (gdprConsent && typeof gdprConsent.consentString === 'string') { + gdprParams = `?consent_str=${gdprConsent.consentString}`; + if (typeof gdprConsent.gdprApplies === 'boolean') { + gdprParams = gdprParams + `&gdpr=${Number(gdprConsent.gdprApplies)}`; + } + 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 + }); + } + return syncs; + } +}; + +function kubientGetConsentGiven(gdprConsent) { + let consentGiven = 0; + if (typeof gdprConsent !== 'undefined') { + let apiVersion = utils.deepAccess(gdprConsent, `apiVersion`); + switch (apiVersion) { + case 1: + consentGiven = utils.deepAccess(gdprConsent, `vendorData.vendorConsents.${VENDOR_ID}`) ? 1 : 0; + break; + case 2: + consentGiven = utils.deepAccess(gdprConsent, `vendorData.vendor.consents.${VENDOR_ID}`) ? 1 : 0; + break; + } + } + return consentGiven; +} +registerBidder(spec); diff --git a/modules/kubientBidAdapter.md b/modules/kubientBidAdapter.md new file mode 100644 index 00000000000..9f3e1d5f52e --- /dev/null +++ b/modules/kubientBidAdapter.md @@ -0,0 +1,26 @@ +# Overview +​ +**Module Name**: Kubient Bidder Adapter +**Module Type**: Bidder Adapter +**Maintainer**: artem.aleksashkin@kubient.com +​ +# Description +​ +Connects to Kubient KSSP demand source to fetch bids. +​ +# Test Parameters +``` + var adUnits = [{ + code: 'banner-ad-div', + mediaTypes: { + banner: { + sizes: [[300, 250],[728, 90]], + } + }, + bids: [{ + "bidder": "kubient", + "params": { + "zoneid": "5fbb948f1e22b", + } + }] + }]; diff --git a/modules/lemmaBidAdapter.js b/modules/lemmaBidAdapter.js index 5941802f97d..926761e5ab2 100644 --- a/modules/lemmaBidAdapter.js +++ b/modules/lemmaBidAdapter.js @@ -62,18 +62,6 @@ export var spec = { }, getUserSyncs: (syncOptions, responses, gdprConsent, uspConsent) => { let syncurl = USER_SYNC + 'pid=' + pubId; - - // Attaching GDPR Consent Params in UserSync url - if (gdprConsent) { - syncurl += '&gdpr=' + (gdprConsent.gdprApplies ? 1 : 0); - syncurl += '&gdpr_consent=' + encodeURIComponent(gdprConsent.consentString || ''); - } - - // CCPA - if (uspConsent) { - syncurl += '&us_privacy=' + encodeURIComponent(uspConsent); - } - if (syncOptions.iframeEnabled) { return [{ type: 'iframe', @@ -122,9 +110,9 @@ function parseRTBResponse(request, response) { newBid.dealId = bid.dealid; } if (req.imp && req.imp.length > 0) { - newBid.mediaType = req.mediaType; req.imp.forEach(robj => { if (bid.impid === robj.id) { + _checkMediaType(bid.adm, newBid); switch (newBid.mediaType) { case BANNER: break; @@ -277,10 +265,6 @@ function _getDeviceObject(request) { function setOtherParams(request, ortbRequest) { var params = request && request.params ? request.params : null; - if (request && request.gdprConsent) { - ortbRequest.regs = { ext: { gdpr: request.gdprConsent.gdprApplies ? 1 : 0 } }; - ortbRequest.user = { ext: { consent: request.gdprConsent.consentString } }; - } if (params) { ortbRequest.tmax = params.tmax; ortbRequest.bcat = params.bcat; @@ -424,4 +408,13 @@ function parse(rawResp) { return null; } +function _checkMediaType(adm, newBid) { + // Create a regex here to check the strings + var videoRegex = new RegExp(/VAST.*version/); + if (videoRegex.test(adm)) { + newBid.mediaType = VIDEO; + } else { + newBid.mediaType = BANNER; + } +} registerBidder(spec); diff --git a/modules/liveIntentIdSystem.js b/modules/liveIntentIdSystem.js index 4f18c73ad2a..f87d67aae8e 100644 --- a/modules/liveIntentIdSystem.js +++ b/modules/liveIntentIdSystem.js @@ -8,9 +8,10 @@ import * as utils from '../src/utils.js'; import { triggerPixel } from '../src/utils.js'; import { ajaxBuilder } from '../src/ajax.js'; import { submodule } from '../src/hook.js'; -import { LiveConnect } from 'live-connect-js/cjs/live-connect.js'; +import { LiveConnect } from 'live-connect-js/esm/initializer.js'; import { gdprDataHandler, uspDataHandler } from '../src/adapterManager.js'; 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); @@ -42,26 +43,18 @@ export function reset() { if (window && window.liQ) { window.liQ = []; } + liveIntentIdSubmodule.setModuleMode(null) eventFired = false; liveConnect = null; } function parseLiveIntentCollectorConfig(collectConfig) { const config = {}; - if (collectConfig) { - if (collectConfig.appId) { - config.appId = collectConfig.appId; - } - if (collectConfig.fpiStorageStrategy) { - config.storageStrategy = collectConfig.fpiStorageStrategy; - } - if (collectConfig.fpiExpirationDays) { - config.expirationDays = collectConfig.fpiExpirationDays; - } - if (collectConfig.collectorUrl) { - config.collectorUrl = collectConfig.collectorUrl; - } - } + collectConfig = collectConfig || {} + collectConfig.appId && (config.appId = collectConfig.appId); + collectConfig.fpiStorageStrategy && (config.storageStrategy = collectConfig.fpiStorageStrategy); + collectConfig.fpiExpirationDays && (config.expirationDays = collectConfig.fpiExpirationDays); + collectConfig.collectorUrl && (config.collectorUrl = collectConfig.collectorUrl); return config; } @@ -90,9 +83,6 @@ function initializeLiveConnect(configParams) { liveConnectConfig.wrapperName = 'prebid'; liveConnectConfig.identityResolutionConfig = identityResolutionConfig; liveConnectConfig.identifiersToResolve = configParams.identifiersToResolve || []; - if (configParams.emailHash) { - liveConnectConfig.eventSource = { hash: configParams.emailHash } - } const usPrivacyString = uspDataHandler.getConsentData(); if (usPrivacyString) { liveConnectConfig.usPrivacyString = usPrivacyString; @@ -105,7 +95,10 @@ function initializeLiveConnect(configParams) { // The second param is the storage object, LS & Cookie manipulation uses PBJS utils. // The third param is the ajax and pixel object, the ajax and pixel use PBJS utils. - liveConnect = LiveConnect(liveConnectConfig, storage, calls); + liveConnect = liveIntentIdSubmodule.getInitializer()(liveConnectConfig, storage, calls); + if (configParams.emailHash) { + liveConnect.push({ hash: configParams.emailHash }) + } return liveConnect; } @@ -118,12 +111,20 @@ function tryFireEvent() { /** @type {Submodule} */ export const liveIntentIdSubmodule = { + moduleMode: process.env.LiveConnectMode, /** * used to link submodule with config * @type {string} */ name: MODULE_NAME, + setModuleMode(mode) { + this.moduleMode = mode + }, + getInitializer() { + return this.moduleMode === 'minimal' ? MinimalLiveConnect : LiveConnect + }, + /** * decode the stored id value for passing to bid requests. Note that lipb object is a wrapper for everything, and * internally it could contain more data other than `lipbid`(e.g. `segments`) depending on the `partner` and @@ -136,7 +137,7 @@ export const liveIntentIdSubmodule = { decode(value, config) { const configParams = (config && config.params) || {}; function composeIdObject(value) { - const base = { 'lipbid': value['unifiedId'] }; + const base = { 'lipbid': value.unifiedId }; delete value.unifiedId; return { 'lipb': { ...base, ...value } }; } diff --git a/modules/livewrappedBidAdapter.js b/modules/livewrappedBidAdapter.js index 512fc775d95..f3e9709cd11 100644 --- a/modules/livewrappedBidAdapter.js +++ b/modules/livewrappedBidAdapter.js @@ -14,6 +14,7 @@ const VERSION = '1.4'; export const spec = { code: BIDDER_CODE, supportedMediaTypes: [BANNER, NATIVE, VIDEO], + gvlid: 919, /** * Determines whether or not the given bid request is valid. diff --git a/modules/loganBidAdapter.js b/modules/loganBidAdapter.js new file mode 100644 index 00000000000..e55876de675 --- /dev/null +++ b/modules/loganBidAdapter.js @@ -0,0 +1,118 @@ +import { registerBidder } from '../src/adapters/bidderFactory.js'; +import { BANNER, NATIVE, VIDEO } from '../src/mediaTypes.js'; +import * as utils from '../src/utils.js'; + +const BIDDER_CODE = 'logan'; +const AD_URL = 'https://USeast2.logan.ai/?c=o&m=multi'; +const SYNC_URL = 'https://ssp-cookie.logan.ai/html?src=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; + } +} + +export const spec = { + code: BIDDER_CODE, + supportedMediaTypes: [BANNER, VIDEO, NATIVE], + + isBidRequestValid: (bid) => { + return Boolean(bid.bidId && bid.params && !isNaN(parseInt(bid.params.placementId))); + }, + + buildRequests: (validBidRequests = [], bidderRequest) => { + const winTop = utils.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 || {}, + }; + const mediaType = bid.mediaTypes + + 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]; + placement.traffic = VIDEO; + } else if (mediaType && mediaType[NATIVE]) { + placement.native = mediaType[NATIVE]; + placement.traffic = 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)) { + response.push(resItem); + } + } + return response; + }, + + getUserSyncs: (syncOptions, serverResponses, gdprConsent, uspConsent) => { + let syncUrl = SYNC_URL + 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}`; + } + return [{ + type: 'iframe', + url: syncUrl + }]; + } +}; + +registerBidder(spec); diff --git a/modules/loganBidAdapter.md b/modules/loganBidAdapter.md new file mode 100644 index 00000000000..3c628cdacc2 --- /dev/null +++ b/modules/loganBidAdapter.md @@ -0,0 +1,72 @@ +# Overview + +``` +Module Name: logan Bidder Adapter +Module Type: logan Bidder Adapter +Maintainer: support@logan.ai +``` + +# Description + +Module that connects to logan demand sources + +# Test Parameters +``` + var adUnits = [ + { + code:'1', + mediaTypes:{ + banner: { + sizes: [[300, 250]], + } + }, + bids:[ + { + bidder: 'logan', + params: { + placementId: 0 + } + } + ] + }, + { + code:'1', + mediaTypes:{ + video: { + playerSize: [640, 480], + context: 'instream' + } + }, + bids:[ + { + bidder: 'logan', + params: { + placementId: 0 + } + } + ] + }, + { + code:'1', + mediaTypes:{ + native: { + title: { + required: true + }, + icon: { + required: true, + size: [64, 64] + } + } + }, + bids:[ + { + bidder: 'logan', + params: { + placementId: 0 + } + } + ] + } + ]; +``` \ No newline at end of file diff --git a/modules/lotamePanoramaIdSystem.js b/modules/lotamePanoramaIdSystem.js index 5c3a9a16b3a..6d36687b4e8 100644 --- a/modules/lotamePanoramaIdSystem.js +++ b/modules/lotamePanoramaIdSystem.js @@ -16,8 +16,11 @@ const MODULE_NAME = 'lotamePanoramaId'; const NINE_MONTHS_MS = 23328000 * 1000; const DAYS_TO_CACHE = 7; const DAY_MS = 60 * 60 * 24 * 1000; +const MISSING_CORE_CONSENT = 111; +const GVLID = 95; -export const storage = getStorageManager(null, MODULE_NAME); +export const storage = getStorageManager(GVLID, MODULE_NAME); +let cookieDomain; /** * Set the Lotame First Party Profile ID in the first party namespace @@ -26,7 +29,14 @@ export const storage = getStorageManager(null, MODULE_NAME); function setProfileId(profileId) { if (storage.cookiesAreEnabled()) { let expirationDate = new Date(utils.timestamp() + NINE_MONTHS_MS).toUTCString(); - storage.setCookie(KEY_PROFILE, profileId, expirationDate, 'Lax', undefined, undefined); + storage.setCookie( + KEY_PROFILE, + profileId, + expirationDate, + 'Lax', + cookieDomain, + undefined + ); } if (storage.hasLocalStorage()) { storage.setDataInLocalStorage(KEY_PROFILE, profileId, undefined); @@ -88,7 +98,7 @@ function saveLotameCache( value, expirationDate, 'Lax', - undefined, + cookieDomain, undefined ); } @@ -115,7 +125,7 @@ function getLotameLocalCache() { try { const rawExpiry = getFromStorage(KEY_EXPIRY); if (utils.isStr(rawExpiry)) { - cache.expiryTimestampMs = parseInt(rawExpiry, 0); + cache.expiryTimestampMs = parseInt(rawExpiry, 10); } } catch (error) { utils.logError(error); @@ -132,7 +142,14 @@ function clearLotameCache(key) { if (key) { if (storage.cookiesAreEnabled()) { let expirationDate = new Date(0).toUTCString(); - storage.setCookie(key, '', expirationDate, 'Lax', undefined, undefined); + storage.setCookie( + key, + '', + expirationDate, + 'Lax', + cookieDomain, + undefined + ); } if (storage.hasLocalStorage()) { storage.removeDataFromLocalStorage(key, undefined); @@ -141,13 +158,18 @@ function clearLotameCache(key) { } /** @type {Submodule} */ export const lotamePanoramaIdSubmodule = { - /** * used to link submodule with config * @type {string} */ name: MODULE_NAME, + /** + * Vendor id of Lotame + * @type {Number} + */ + gvlid: GVLID, + /** * Decode the stored id value for passing to bid requests * @function decode @@ -156,7 +178,7 @@ export const lotamePanoramaIdSubmodule = { * @returns {(Object|undefined)} */ decode(value, config) { - return utils.isStr(value) ? { 'lotamePanoramaId': value } : undefined; + return utils.isStr(value) ? { lotamePanoramaId: value } : undefined; }, /** @@ -168,13 +190,14 @@ export const lotamePanoramaIdSubmodule = { * @returns {IdResponse|undefined} */ getId(config, consentData, cacheIdObj) { + cookieDomain = lotamePanoramaIdSubmodule.findRootDomain(); let localCache = getLotameLocalCache(); let refreshNeeded = Date.now() > localCache.expiryTimestampMs; if (!refreshNeeded) { return { - id: localCache.data + id: localCache.data, }; } @@ -183,14 +206,25 @@ export const lotamePanoramaIdSubmodule = { const resolveIdFunction = function (callback) { let queryParams = {}; if (storedUserId) { - queryParams.fp = storedUserId + queryParams.fp = storedUserId; } - if (consentData && utils.isBoolean(consentData.gdprApplies)) { - queryParams.gdpr_applies = consentData.gdprApplies; - if (consentData.gdprApplies) { - queryParams.gdpr_consent = consentData.consentString; + let consentString; + if (consentData) { + if (utils.isBoolean(consentData.gdprApplies)) { + queryParams.gdpr_applies = consentData.gdprApplies; } + consentString = consentData.consentString; + } + // If no consent string, try to read it from 1st party cookies + if (!consentString) { + consentString = getFromStorage('eupubconsent-v2'); + } + if (!consentString) { + consentString = getFromStorage('euconsent-v2'); + } + if (consentString) { + queryParams.gdpr_consent = consentString; } const url = utils.buildUrl({ protocol: 'https', @@ -205,10 +239,17 @@ export const lotamePanoramaIdSubmodule = { if (response) { try { let responseObj = JSON.parse(response); - saveLotameCache(KEY_EXPIRY, responseObj.expiry_ts); + const shouldUpdateProfileId = !( + utils.isArray(responseObj.errors) && + responseObj.errors.indexOf(MISSING_CORE_CONSENT) !== -1 + ); + + saveLotameCache(KEY_EXPIRY, responseObj.expiry_ts, responseObj.expiry_ts); if (utils.isStr(responseObj.profile_id)) { - setProfileId(responseObj.profile_id); + if (shouldUpdateProfileId) { + setProfileId(responseObj.profile_id); + } if (utils.isStr(responseObj.core_id)) { saveLotameCache( @@ -221,7 +262,9 @@ export const lotamePanoramaIdSubmodule = { clearLotameCache(KEY_ID); } } else { - clearLotameCache(KEY_PROFILE); + if (shouldUpdateProfileId) { + clearLotameCache(KEY_PROFILE); + } clearLotameCache(KEY_ID); } } catch (error) { @@ -233,7 +276,7 @@ export const lotamePanoramaIdSubmodule = { undefined, { method: 'GET', - withCredentials: true + withCredentials: true, } ); }; diff --git a/modules/malltvBidAdapter.js b/modules/malltvBidAdapter.js index 846935a5522..7deffe6c07a 100644 --- a/modules/malltvBidAdapter.js +++ b/modules/malltvBidAdapter.js @@ -31,6 +31,7 @@ export const spec = { let bidderRequestId = ''; let url = ''; let contents = []; + let data = {}; let placements = validBidRequests.map(bidRequest => { if (!propertyId) { propertyId = bidRequest.params.propertyId; } @@ -38,7 +39,8 @@ export const spec = { if (!storageId && bidRequest.params) { storageId = bidRequest.params.storageId || ''; } if (!bidderRequestId) { bidderRequestId = bidRequest.bidderRequestId; } if (!url && bidderRequest) { url = bidderRequest.refererInfo.referer; } - if (!contents.length && bidRequest.params.contents && bidRequest.params.contents.length) { contents = bidRequest.params.contents } + 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; } let adUnitId = bidRequest.adUnitCode; let placementId = bidRequest.params.placementId; @@ -49,6 +51,8 @@ export const spec = { adUnitId: adUnitId, placementId: placementId, bidid: bidRequest.bidId, + count: bidRequest.params.count, + skipTime: bidRequest.params.skipTime }; }); @@ -59,7 +63,8 @@ export const spec = { url: url, requestid: bidderRequestId, placements: placements, - contents: contents + contents: contents, + data: data } return [{ diff --git a/modules/malltvBidAdapter.md b/modules/malltvBidAdapter.md index 3d419fa0916..e32eb54f90f 100644 --- a/modules/malltvBidAdapter.md +++ b/modules/malltvBidAdapter.md @@ -1,45 +1,68 @@ # Overview -Module Name: MallTV Bidder Adapter Module -Type: Bidder Adapter -Maintainer: drilon@gjirafa.com +Module Name: MallTV Bidder Adapter Module + +Type: Bidder Adapter + +Maintainer: arditb@gjirafa.com # Description MallTV Bidder Adapter for Prebid.js. # Test Parameters +```js var adUnits = [ { code: 'test-div', mediaTypes: { banner: { - sizes: [[300, 250], [300, 300]] + sizes: [ + [300, 250], + [300, 300] + ] } }, - bids: [ - { - bidder: 'malltv', - params: { - propertyId: '105134', - placementId: '846832' + 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: { + video: { context: 'instream' - } + } }, - bids: [ - { - bidder: 'malltv', - params: { - propertyId: '105134', - placementId: '846841' + 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/mediaforceBidAdapter.js b/modules/mediaforceBidAdapter.js index 7e29bb519b5..e03423f36ac 100644 --- a/modules/mediaforceBidAdapter.js +++ b/modules/mediaforceBidAdapter.js @@ -128,7 +128,7 @@ export const spec = { let impObj = { id: bid.bidId, tagid: tagid, - secure: window.location.protocol === 'https' ? 1 : 0, + secure: window.location.protocol === 'https:' ? 1 : 0, bidfloor: bidfloor, ext: { mediaforce: { diff --git a/modules/medianetAnalyticsAdapter.js b/modules/medianetAnalyticsAdapter.js index 35c9273f951..cb65a6afc54 100644 --- a/modules/medianetAnalyticsAdapter.js +++ b/modules/medianetAnalyticsAdapter.js @@ -4,7 +4,8 @@ import CONSTANTS from '../src/constants.json'; import * as utils from '../src/utils.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 { AUCTION_COMPLETED, AUCTION_IN_PROGRESS, getPriceGranularity } from '../src/auction.js'; +import includes from 'core-js-pure/features/array/includes.js'; const analyticsType = 'endpoint'; const ENDPOINT = 'https://pb-logs.media.net/log?logid=kfk&evtid=prebid_analytics_events_client'; @@ -125,7 +126,7 @@ class Configure { this.setDataFromResponse(response); this.overrideDomainLevelData(response); this.overrideToDebug(this.mnetDebugConfig); - this.urlToConsume = VALID_URL_KEY.includes(response.urlKey) ? response.urlKey : this.urlToConsume; + this.urlToConsume = includes(VALID_URL_KEY, response.urlKey) ? response.urlKey : this.urlToConsume; this.ajaxState = CONFIG_PASS; } catch (e) { this.ajaxState = CONFIG_ERROR; diff --git a/modules/mediasquareBidAdapter.js b/modules/mediasquareBidAdapter.js index 87f768d3b77..cb9bf5285a1 100644 --- a/modules/mediasquareBidAdapter.js +++ b/modules/mediasquareBidAdapter.js @@ -59,7 +59,11 @@ export const spec = { } if (bidderRequest.uspConsent) { payload.uspConsent = bidderRequest.uspConsent; } if (bidderRequest.schain) { payload.schain = bidderRequest.schain; } - if (bidderRequest.userId) { payload.userId = bidderRequest.userId; } + if (bidderRequest.userId) { + payload.userId = bidderRequest.userId; + } else if (bidderRequest.hasOwnProperty('bids') && typeof bidderRequest.bids == 'object' && bidderRequest.bids.length > 0 && bidderRequest.bids[0].hasOwnProperty('userId')) { + payload.userId = bidderRequest.bids[0].userId; + } }; if (test) { payload.debug = true; } const payloadString = JSON.stringify(payload); diff --git a/modules/missenaBidAdapter.js b/modules/missenaBidAdapter.js new file mode 100644 index 00000000000..2b1d6bdb118 --- /dev/null +++ b/modules/missenaBidAdapter.js @@ -0,0 +1,94 @@ +import * as utils from '../src/utils.js'; +import { BANNER } from '../src/mediaTypes.js'; +import { registerBidder } from '../src/adapters/bidderFactory.js'; + +const BIDDER_CODE = 'missena'; +const ENDPOINT_URL = 'https://bid.missena.io/'; + +export const spec = { + aliases: [BIDDER_CODE], + code: BIDDER_CODE, + gvlid: 687, + supportedMediaTypes: [BANNER], + + /** + * Determines whether or not the given bid request is valid. + * + * @param {BidRequest} bid The bid params to validate. + * @return boolean True if this is a valid bid, and false otherwise. + */ + isBidRequestValid: function (bid) { + return typeof bid == 'object' && !!bid.params.apiKey; + }, + + /** + * 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) { + return validBidRequests.map((bidRequest) => { + const payload = { + request_id: bidRequest.bidId, + timeout: bidderRequest.timeout, + }; + + if (bidderRequest && bidderRequest.refererInfo) { + payload.referer = bidderRequest.refererInfo.referer; + payload.referer_canonical = bidderRequest.refererInfo.canonicalUrl; + } + + if (bidderRequest && bidderRequest.gdprConsent) { + payload.consent_string = bidderRequest.gdprConsent.consentString; + payload.consent_required = bidderRequest.gdprConsent.gdprApplies; + } + + return { + method: 'POST', + url: + ENDPOINT_URL + + '?' + + utils.formatQS({ + t: bidRequest.params.apiKey, + }), + data: JSON.stringify(payload), + }; + }); + }, + + /** + * Unpack the response from the server into a list of bids. + * + * @param {ServerResponse} serverResponse A successful response from the server. + * @return {Bid[]} An array of bids which were nested inside the server. + */ + interpretResponse: function (serverResponse, bidRequest) { + const bidResponses = []; + const response = serverResponse.body; + + if (response && !response.timeout && !!response.ad) { + bidResponses.push(response); + } + + return bidResponses; + }, + + /** + * Register bidder specific code, which will execute if bidder timed out after an auction + * @param {data} Containing timeout specific data + */ + onTimeout: function onTimeout(timeoutData) { + utils.logInfo('Missena - Timeout from adapter', timeoutData); + }, + + /** + * 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) { + utils.logInfo('Missena - Bid won', bid); + }, +}; + +registerBidder(spec); diff --git a/modules/missenaBidAdapter.md b/modules/missenaBidAdapter.md new file mode 100644 index 00000000000..d5fcacf04ab --- /dev/null +++ b/modules/missenaBidAdapter.md @@ -0,0 +1,70 @@ +# Overview + +``` +Module Name: Missena Bid Adapter +Module Type: Bidder Adapter +Maintainer: jney@missena.com +``` + +## Introduction + +Connects to Missena for bids. + +**Note:** this adapter doesn't support SafeFrame. + +Useful resources: + +- [README](../README.md#Build) +- [https://docs.prebid.org/dev-docs/bidder-adaptor.html](https://docs.prebid.org/dev-docs/bidder-adaptor.html) + +## Develop + +Setup the missena adapter in `integrationExamples/gpt/userId_example.html`. + +For example: + +```js +const AD_UNIT_CODE = "test-div"; +const PUBLISHER_MISSENA_TOKEN = "PA-34745704"; + +var adUnits = [ + { + code: AD_UNIT_CODE, + mediaTypes: { + banner: { + sizes: [1, 1], + }, + }, + bids: [ + { + bidder: "missena", + params: { + apiKey: PUBLISHER_MISSENA_TOKEN, + }, + }, + ], + }, +]; +``` + +Then start the demo app: + +```shell +gulp serve-fast --modules=missenaBidAdapter +``` + +And open [http://localhost:9999/integrationExamples/gpt/userId_example.html](http://localhost:9999/integrationExamples/gpt/userId_example.html) + +## Test + +```shell +gulp test --file test/spec/modules/missenaBidAdapter_spec.js +``` + +Add the `--watch` option to re-run unit tests whenever the source code changes. + +## Build + +```shell +gulp build --modules=missenaBidAdapter +``` diff --git a/modules/oneVideoBidAdapter.js b/modules/oneVideoBidAdapter.js index c287bc2f3b7..2f9275f969d 100644 --- a/modules/oneVideoBidAdapter.js +++ b/modules/oneVideoBidAdapter.js @@ -4,7 +4,7 @@ import {registerBidder} from '../src/adapters/bidderFactory.js'; const BIDDER_CODE = 'oneVideo'; export const spec = { code: 'oneVideo', - VERSION: '3.0.4', + VERSION: '3.0.5', ENDPOINT: 'https://ads.adaptv.advertising.com/rtb/openrtb?ext_id=', E2ETESTENDPOINT: 'https://ads-wc.v.ssp.yahoo.com/rtb/openrtb?ext_id=', SYNC_ENDPOINT1: 'https://pixel.advertising.com/ups/57304/sync?gdpr=&gdpr_consent=&_origin=0&redir=true', @@ -75,7 +75,7 @@ export const spec = { * @param {*} serverResponse A successful response from the server. * @return {Bid[]} An array of bids which were nested inside the server. */ - interpretResponse: function(response, { bidRequest }) { + interpretResponse: function(response, {bidRequest}) { let bid; let size; let bidResponse; @@ -99,7 +99,7 @@ export const spec = { width: size.width, height: size.height, currency: response.cur, - ttl: 100, + ttl: (bidRequest.params.video.ttl > 0 && bidRequest.params.video.ttl <= 3600) ? bidRequest.params.video.ttl : 300, netRevenue: true, adUnitCode: bidRequest.adUnitCode }; @@ -113,7 +113,6 @@ export const spec = { } else if (bid.adm) { bidResponse.vastXml = bid.adm; } - if (bidRequest.mediaTypes.video) { bidResponse.renderer = (bidRequest.mediaTypes.video.context === 'outstream') ? newRenderer(bidRequest, bidResponse) : undefined; } @@ -128,7 +127,10 @@ export const spec = { * @return {UserSync[]} The user syncs which should be dropped. */ getUserSyncs: function(syncOptions, responses, consentData = {}) { - let { gdprApplies, consentString = '' } = consentData; + let { + gdprApplies, + consentString = '' + } = consentData; if (syncOptions.pixelEnabled) { return [{ @@ -302,14 +304,33 @@ function getRequestData(bid, consentData, bidRequest) { bidData.site.ref = 'https://verizonmedia.com'; bidData.tmax = 1000; } - if (bid.params.video.custom && Object.prototype.toString.call(bid.params.video.custom) === '[object Object]') { + if (bid.params.video.custom && utils.isPlainObject(bid.params.video.custom)) { bidData.imp[0].ext.custom = {}; for (const key in bid.params.video.custom) { - if (typeof bid.params.video.custom[key] === 'string' || typeof bid.params.video.custom[key] === 'number') { + if (utils.isStr(bid.params.video.custom[key]) || utils.isNumber(bid.params.video.custom[key])) { bidData.imp[0].ext.custom[key] = bid.params.video.custom[key]; } } } + if (bid.params.video.content && utils.isPlainObject(bid.params.video.content)) { + bidData.imp[0].content = {}; + const contentStringKeys = ['id', 'title', 'series', 'season', 'genre', 'contentrating', 'language']; + const contentNumberkeys = ['episode', 'prodq', 'context', 'livestream', 'len']; + const contentArrayKeys = ['cat']; + const contentObjectKeys = ['ext']; + for (const contentKey in bid.params.video.content) { + if ( + (contentStringKeys.indexOf(contentKey) > -1 && utils.isStr(bid.params.video.content[contentKey])) || + (contentNumberkeys.indexOf(contentKey) > -1 && utils.isNumber(bid.params.video.content[contentKey])) || + (contentObjectKeys.indexOf(contentKey) > -1 && utils.isPlainObject(bid.params.video.content[contentKey])) || + (contentArrayKeys.indexOf(contentKey) > -1 && utils.isArray(bid.params.video.content[contentKey]) && + bid.params.video.content[contentKey].every(catStr => utils.isStr(catStr)))) { + bidData.imp[0].content[contentKey] = bid.params.video.content[contentKey]; + } else { + utils.logMessage('oneVideo bid adapter validation error: ', contentKey, ' is either not supported is OpenRTB V2.5 or value is undefined'); + } + } + } return bidData; } @@ -325,7 +346,7 @@ function newRenderer(bidRequest, bid) { bidRequest.renderer = {}; bidRequest.renderer.url = 'https://cdn.vidible.tv/prod/hb-outstream-renderer/renderer.js'; bidRequest.renderer.render = function(bid) { - setTimeout(function () { + setTimeout(function() { // eslint-disable-next-line no-undef o2PlayerRender(bid); }, 700) diff --git a/modules/oneVideoBidAdapter.md b/modules/oneVideoBidAdapter.md index 92958af9e83..97c4e91ca78 100644 --- a/modules/oneVideoBidAdapter.md +++ b/modules/oneVideoBidAdapter.md @@ -40,6 +40,7 @@ Connects to Verizon Media's Video SSP (AKA ONE Video / Adap.tv) demand source to inventoryid: 123, minduration: 10, maxduration: 30, + ttl: 300, custom: { key1: "value1", key2: 123345 @@ -89,6 +90,7 @@ Connects to Verizon Media's Video SSP (AKA ONE Video / Adap.tv) demand source to inventoryid: 123, minduration: 10, maxduration: 30, + ttl: 250 }, site: { id: 1, @@ -298,3 +300,89 @@ var adUnits = [ } ] ``` +# Content Object Support +The oneVideoBidAdapter supports passing of OpenRTB V2.5 Content Object. + +``` +const adUnits = [{ + code: 'video1', + mediaTypes: { + video: { + context: 'outstream', + playerSize: [640, 480] + } + }, + bids: [{ + bidder: 'oneVideo', + params: { + video: { + playerWidth: 640, + playerHeight: 480, + mimes: ['video/mp4', 'application/javascript'], + protocols: [2, 5], + api: [1, 2], + ttl: 300, + content: { + id: "1234", + title: "Title", + series: "Series", + season: "Season", + episode: 1 + cat: [ + "IAB1", + "IAB1-1", + "IAB1-2", + "IAB2", + "IAB2-1" + ], + genre: "Genre", + contentrating: "C-Rating", + language: "EN", + prodq: 1, + context: 1, + livestream: 0, + len: 360, + ext: { + network: "ext-network", + channel: "ext-channel" + } + } + }, + pubId: 'HBExchange' + } + } + }] + }] +``` + + +# TTL Support +The oneVideoBidAdapter supports passing of "Time To Live" (ttl) that indicates to prebid chache for how long to keep the chaced winning bid alive. +Value is Number in seconds +You can enter any number between 1 - 3600 (seconds) +``` +const adUnits = [{ + code: 'video1', + mediaTypes: { + video: { + context: 'outstream', + playerSize: [640, 480] + } + }, + bids: [{ + bidder: 'oneVideo', + params: { + video: { + playerWidth: 640, + playerHeight: 480, + mimes: ['video/mp4', 'application/javascript'], + protocols: [2, 5], + api: [1, 2], + ttl: 300 + }, + pubId: 'HBExchange' + } + }] + }] +``` + diff --git a/modules/onetagBidAdapter.js b/modules/onetagBidAdapter.js index 1a2df023b81..16b8096646f 100644 --- a/modules/onetagBidAdapter.js +++ b/modules/onetagBidAdapter.js @@ -6,6 +6,7 @@ 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'; const ENDPOINT = 'https://onetag-sys.com/prebid-request'; const USER_SYNC_ENDPOINT = 'https://onetag-sys.com/usync/'; @@ -63,8 +64,8 @@ function buildRequests(validBidRequests, bidderRequest) { if (bidderRequest && bidderRequest.uspConsent) { payload.usPrivacy = bidderRequest.uspConsent; } - if (bidderRequest && bidderRequest.userId) { - payload.userId = bidderRequest.userId; + if (validBidRequests && validBidRequests.length !== 0 && validBidRequests[0].userId) { + payload.userId = createEidsArray(validBidRequests[0].userId); } try { if (storage.hasLocalStorage()) { @@ -88,47 +89,34 @@ function interpretResponse(serverResponse, bidderRequest) { if (!body.bids || !Array.isArray(body.bids) || body.bids.length === 0) { return bids; } - body.bids.forEach(({ - requestId, - cpm, - width, - height, - creativeId, - dealId, - currency, - mediaType, - ttl, - rendererUrl, - ad, - vastUrl, - videoCacheKey - }) => { + body.bids.forEach(bid => { const responseBid = { - requestId, - cpm, - width, - height, - creativeId, - dealId: dealId == null ? dealId : '', - currency, - netRevenue: false, + requestId: bid.requestId, + cpm: bid.cpm, + width: bid.width, + height: bid.height, + creativeId: bid.creativeId, + dealId: bid.dealId == null ? bid.dealId : '', + currency: bid.currency, + netRevenue: bid.netRevenue || false, + mediaType: bid.mediaType, meta: { - mediaType + mediaType: bid.mediaType }, - ttl: ttl || 300 + ttl: bid.ttl || 300 }; - if (mediaType === BANNER) { - responseBid.ad = ad; - } else if (mediaType === VIDEO) { - const {context, adUnitCode} = find(requestData.bids, (item) => item.bidId === requestId); + if (bid.mediaType === BANNER) { + responseBid.ad = bid.ad; + } else if (bid.mediaType === VIDEO) { + const {context, adUnitCode} = find(requestData.bids, (item) => item.bidId === bid.requestId); if (context === INSTREAM) { - responseBid.vastUrl = vastUrl; - responseBid.videoCacheKey = videoCacheKey; + responseBid.vastUrl = bid.vastUrl; + responseBid.videoCacheKey = bid.videoCacheKey; } else if (context === OUTSTREAM) { - responseBid.vastXml = ad; - responseBid.vastUrl = vastUrl; - if (rendererUrl) { - responseBid.renderer = createRenderer({requestId, rendererUrl, adUnitCode}); + responseBid.vastXml = bid.ad; + responseBid.vastUrl = bid.vastUrl; + if (bid.rendererUrl) { + responseBid.renderer = createRenderer({ ...bid, adUnitCode }); } } } @@ -146,25 +134,24 @@ function createRenderer(bid, rendererOptions = {}) { loaded: false }); try { - renderer.setRender(onetagRenderer); + renderer.setRender(({renderer, width, height, vastXml, adUnitCode}) => { + renderer.push(() => { + window.onetag.Player.init({ + ...bid, + width, + height, + vastXml, + nodeId: adUnitCode, + config: renderer.getConfig() + }); + }); + }); } catch (e) { } return renderer; } -function onetagRenderer({renderer, width, height, vastXml, adUnitCode}) { - renderer.push(() => { - window.onetag.Player.init({ - width, - height, - vastXml, - nodeId: adUnitCode, - config: renderer.getConfig() - }); - }); -} - function getFrameNesting() { let topmostFrame = window; let parent = window.parent; diff --git a/modules/ozoneBidAdapter.js b/modules/ozoneBidAdapter.js index 4246a39bc69..8ada9d59ae2 100644 --- a/modules/ozoneBidAdapter.js +++ b/modules/ozoneBidAdapter.js @@ -5,108 +5,151 @@ import {config} from '../src/config.js'; import {getPriceBucketString} from '../src/cpmBucketManager.js'; import { Renderer } from '../src/Renderer.js'; const BIDDER_CODE = 'ozone'; -const ALLOWED_LOTAME_PARAMS = ['oz_lotameid', 'oz_lotamepid', 'oz_lotametpid']; // *** PROD *** -const OZONEURI = 'https://elb.the-ozone-project.com/openrtb2/auction'; -const OZONECOOKIESYNC = 'https://elb.the-ozone-project.com/static/load-cookie.html'; +const ORIGIN = 'https://elb.the-ozone-project.com' // applies only to auction & cookie +const AUCTIONURI = '/openrtb2/auction'; +const OZONECOOKIESYNC = '/static/load-cookie.html'; const OZONE_RENDERER_URL = 'https://prebid.the-ozone-project.com/ozone-renderer.js'; -const OZONEVERSION = '2.4.0'; +const OZONEVERSION = '2.5.0'; export const spec = { + gvlid: 524, + aliases: [{ code: 'lmc' }], + version: OZONEVERSION, code: BIDDER_CODE, supportedMediaTypes: [VIDEO, BANNER], cookieSyncBag: {'publisherId': null, 'siteId': null, 'userIdObject': {}}, // variables we want to make available to cookie sync - propertyBag: {'lotameWasOverridden': 0, 'pageId': null, 'buildRequestsStart': 0, 'buildRequestsEnd': 0}, /* allow us to store vars in instance scope - needs to be an object to be mutable */ - + propertyBag: {'pageId': null, 'buildRequestsStart': 0, 'buildRequestsEnd': 0}, /* allow us to store vars in instance scope - needs to be an object to be mutable */ + whitelabel_defaults: { + 'logId': 'OZONE', + 'bidder': 'ozone', + 'keyPrefix': 'oz', + 'auctionUrl': ORIGIN + AUCTIONURI, + 'cookieSyncUrl': ORIGIN + OZONECOOKIESYNC, + 'rendererUrl': OZONE_RENDERER_URL + }, + /** + * make sure that the whitelabel/default values are available in the propertyBag + * @param bid Object : the bid + */ + loadWhitelabelData(bid) { + if (this.propertyBag.whitelabel) { return; } + this.propertyBag.whitelabel = JSON.parse(JSON.stringify(this.whitelabel_defaults)); + let bidder = bid.bidder || 'ozone'; // eg. ozone + this.propertyBag.whitelabel.logId = bidder.toUpperCase(); + this.propertyBag.whitelabel.bidder = bidder; + let bidderConfig = config.getConfig(bidder) || {}; + if (bidderConfig.kvpPrefix) { + this.propertyBag.whitelabel.keyPrefix = bidderConfig.kvpPrefix; + } + if (bidderConfig.endpointOverride) { + if (bidderConfig.endpointOverride.origin) { + this.propertyBag.whitelabel.auctionUrl = bidderConfig.endpointOverride.origin + AUCTIONURI; + this.propertyBag.whitelabel.cookieSyncUrl = bidderConfig.endpointOverride.origin + OZONECOOKIESYNC; + } + if (bidderConfig.endpointOverride.rendererUrl) { + this.propertyBag.whitelabel.rendererUrl = bidderConfig.endpointOverride.rendererUrl; + } + } + this.logInfo('set propertyBag.whitelabel to', this.propertyBag.whitelabel); + }, + getAuctionUrl() { + return this.propertyBag.whitelabel.auctionUrl; + }, + getCookieSyncUrl() { + return this.propertyBag.whitelabel.cookieSyncUrl; + }, + getRendererUrl() { + return this.propertyBag.whitelabel.rendererUrl; + }, + /** + * wrappers for this.logInfo logWarn & logError, to add the proper prefix + */ + logInfo() { + if (!this.propertyBag.whitelabel) { return; } + let args = arguments; + args[0] = `${this.propertyBag.whitelabel.logId}: ${arguments[0]}`; + utils.logInfo.apply(this, args); + }, + logError() { + if (!this.propertyBag.whitelabel) { return; } + let args = arguments; + args[0] = `${this.propertyBag.whitelabel.logId}: ${arguments[0]}`; + utils.logError.apply(this, args); + }, + logWarn() { + if (!this.propertyBag.whitelabel) { return; } + let args = arguments; + args[0] = `${this.propertyBag.whitelabel.logId}: ${arguments[0]}`; + utils.logWarn.apply(this, args); + }, /** * Basic check to see whether required parameters are in the request. * @param bid * @returns {boolean} */ isBidRequestValid(bid) { - utils.logInfo('OZONE: isBidRequestValid : ', config.getConfig(), bid); + this.loadWhitelabelData(bid); + this.logInfo('isBidRequestValid : ', config.getConfig(), bid); let adUnitCode = bid.adUnitCode; // adunit[n].code if (!(bid.params.hasOwnProperty('placementId'))) { - utils.logError('OZONE: OZONE BID ADAPTER VALIDATION FAILED : missing placementId : siteId, placementId and publisherId are REQUIRED', adUnitCode); + this.logError('BID ADAPTER VALIDATION FAILED : missing placementId : siteId, placementId and publisherId are REQUIRED', adUnitCode); return false; } if (!this.isValidPlacementId(bid.params.placementId)) { - utils.logError('OZONE: OZONE BID ADAPTER VALIDATION FAILED : placementId must be exactly 10 numeric characters', adUnitCode); + this.logError('BID ADAPTER VALIDATION FAILED : placementId must be exactly 10 numeric characters', adUnitCode); return false; } if (!(bid.params.hasOwnProperty('publisherId'))) { - utils.logError('OZONE: OZONE BID ADAPTER VALIDATION FAILED : missing publisherId : siteId, placementId and publisherId are REQUIRED', adUnitCode); + this.logError('BID ADAPTER VALIDATION FAILED : missing publisherId : siteId, placementId and publisherId are REQUIRED', adUnitCode); return false; } if (!(bid.params.publisherId).toString().match(/^[a-zA-Z0-9\-]{12}$/)) { - utils.logError('OZONE: OZONE BID ADAPTER VALIDATION FAILED : publisherId must be exactly 12 alphanumieric characters including hyphens', adUnitCode); + this.logError('BID ADAPTER VALIDATION FAILED : publisherId must be exactly 12 alphanumieric characters including hyphens', adUnitCode); return false; } if (!(bid.params.hasOwnProperty('siteId'))) { - utils.logError('OZONE: OZONE BID ADAPTER VALIDATION FAILED : missing siteId : siteId, placementId and publisherId are REQUIRED', adUnitCode); + this.logError('BID ADAPTER VALIDATION FAILED : missing siteId : siteId, placementId and publisherId are REQUIRED', adUnitCode); return false; } if (!(bid.params.siteId).toString().match(/^[0-9]{10}$/)) { - utils.logError('OZONE: OZONE BID ADAPTER VALIDATION FAILED : siteId must be exactly 10 numeric characters', adUnitCode); + this.logError('BID ADAPTER VALIDATION FAILED : siteId must be exactly 10 numeric characters', adUnitCode); return false; } if (bid.params.hasOwnProperty('customParams')) { - utils.logError('OZONE: OZONE BID ADAPTER VALIDATION FAILED : customParams should be renamed to customData', adUnitCode); + this.logError('BID ADAPTER VALIDATION FAILED : customParams should be renamed to customData', adUnitCode); return false; } if (bid.params.hasOwnProperty('customData')) { if (!Array.isArray(bid.params.customData)) { - utils.logError('OZONE: OZONE BID ADAPTER VALIDATION FAILED : customData is not an Array', adUnitCode); + this.logError('BID ADAPTER VALIDATION FAILED : customData is not an Array', adUnitCode); return false; } if (bid.params.customData.length < 1) { - utils.logError('OZONE: OZONE BID ADAPTER VALIDATION FAILED : customData is an array but does not contain any elements', adUnitCode); + this.logError('BID ADAPTER VALIDATION FAILED : customData is an array but does not contain any elements', adUnitCode); return false; } if (!(bid.params.customData[0]).hasOwnProperty('targeting')) { - utils.logError('OZONE: OZONE BID ADAPTER VALIDATION FAILED : customData[0] does not contain "targeting"', adUnitCode); + this.logError('BID ADAPTER VALIDATION FAILED : customData[0] does not contain "targeting"', adUnitCode); return false; } if (typeof bid.params.customData[0]['targeting'] != 'object') { - utils.logError('OZONE: OZONE BID ADAPTER VALIDATION FAILED : customData[0] targeting is not an object', adUnitCode); - return false; - } - } - if (bid.params.hasOwnProperty('lotameData')) { - if (typeof bid.params.lotameData !== 'object') { - utils.logError('OZONE: OZONE BID ADAPTER VALIDATION FAILED : lotameData is not an object', adUnitCode); + this.logError('BID ADAPTER VALIDATION FAILED : customData[0] targeting is not an object', adUnitCode); return false; } } if (bid.hasOwnProperty('mediaTypes') && bid.mediaTypes.hasOwnProperty(VIDEO)) { if (!bid.mediaTypes[VIDEO].hasOwnProperty('context')) { - utils.logError('OZONE: No video context key/value in bid. Rejecting bid: ', bid); + this.logError('No video context key/value in bid. Rejecting bid: ', bid); return false; } if (bid.mediaTypes[VIDEO].context !== 'instream' && bid.mediaTypes[VIDEO].context !== 'outstream') { - utils.logError('OZONE: video.context is invalid. Only instream/outstream video is supported. Rejecting bid: ', bid); + this.logError('video.context is invalid. Only instream/outstream video is supported. Rejecting bid: ', bid); return false; } } - // guard against hacks in GET parameters that we might allow - const arrLotameOverride = this.getLotameOverrideParams(); - // lotame override, test params. All 3 must be present, or none. - let lotameKeys = Object.keys(arrLotameOverride); - if (lotameKeys.length === ALLOWED_LOTAME_PARAMS.length) { - utils.logInfo('OZONE: VALIDATION : arrLotameOverride', arrLotameOverride); - for (let i in lotameKeys) { - if (!arrLotameOverride[ALLOWED_LOTAME_PARAMS[i]].toString().match(/^[0-9a-zA-Z]+$/)) { - utils.logError('OZONE: Only letters & numbers allowed in lotame override: ' + i.toString() + ': ' + arrLotameOverride[ALLOWED_LOTAME_PARAMS[i]].toString() + '. Rejecting bid: ', bid); - return false; - } - } - } else if (lotameKeys.length > 0) { - utils.logInfo('OZONE: VALIDATION : arrLotameOverride', arrLotameOverride); - utils.logError('OZONE: lotame override params are incomplete. You must set all ' + ALLOWED_LOTAME_PARAMS.length + ': ' + JSON.stringify(ALLOWED_LOTAME_PARAMS) + ', . Rejecting bid: ', bid); - return false; - } return true; }, @@ -119,8 +162,11 @@ export const spec = { }, buildRequests(validBidRequests, bidderRequest) { + this.loadWhitelabelData(validBidRequests[0]); this.propertyBag.buildRequestsStart = new Date().getTime(); - utils.logInfo(`OZONE: buildRequests time: ${this.propertyBag.buildRequestsStart} ozone v ${OZONEVERSION} validBidRequests`, validBidRequests, 'bidderRequest', bidderRequest); + let whitelabelBidder = this.propertyBag.whitelabel.bidder; // by default = ozone + let whitelabelPrefix = this.propertyBag.whitelabel.keyPrefix; + this.logInfo(`buildRequests time: ${this.propertyBag.buildRequestsStart} v ${OZONEVERSION} validBidRequests`, JSON.parse(JSON.stringify(validBidRequests)), 'bidderRequest', JSON.parse(JSON.stringify(bidderRequest))); // First check - is there any config to block this request? if (this.blockTheRequest()) { return []; @@ -132,31 +178,31 @@ export const spec = { this.cookieSyncBag.publisherId = utils.deepAccess(validBidRequests[0], 'params.publisherId'); htmlParams = validBidRequests[0].params; } - utils.logInfo('OZONE: cookie sync bag', this.cookieSyncBag); - let singleRequest = config.getConfig('ozone.singleRequest'); + this.logInfo('cookie sync bag', this.cookieSyncBag); + let singleRequest = this.getWhitelabelConfigItem('ozone.singleRequest'); singleRequest = singleRequest !== false; // undefined & true will be true - utils.logInfo('OZONE: config ozone.singleRequest : ', singleRequest); + this.logInfo(`config ${whitelabelBidder}.singleRequest : `, singleRequest); let ozoneRequest = {}; // we only want to set specific properties on this, not validBidRequests[0].params delete ozoneRequest.test; // don't allow test to be set in the config - ONLY use $_GET['pbjs_debug'] if (bidderRequest && bidderRequest.gdprConsent) { - utils.logInfo('OZONE: ADDING GDPR info'); - let apiVersion = utils.deepAccess(bidderRequest.gdprConsent, 'apiVersion', '1'); + this.logInfo('ADDING GDPR info'); + let apiVersion = bidderRequest.gdprConsent.apiVersion || '1'; ozoneRequest.regs = {ext: {gdpr: bidderRequest.gdprConsent.gdprApplies ? 1 : 0, apiVersion: apiVersion}}; if (ozoneRequest.regs.ext.gdpr) { ozoneRequest.user = ozoneRequest.user || {}; ozoneRequest.user.ext = {'consent': bidderRequest.gdprConsent.consentString}; } else { - utils.logInfo('OZONE: **** Strange CMP info: bidderRequest.gdprConsent exists BUT bidderRequest.gdprConsent.gdprApplies is false. See bidderRequest logged above. ****'); + this.logInfo('**** Strange CMP info: bidderRequest.gdprConsent exists BUT bidderRequest.gdprConsent.gdprApplies is false. See bidderRequest logged above. ****'); } } else { - utils.logInfo('OZONE: WILL NOT ADD GDPR info; no bidderRequest.gdprConsent object was present.'); + this.logInfo('WILL NOT ADD GDPR info; no bidderRequest.gdprConsent object was present.'); } const getParams = this.getGetParametersAsObject(); - const ozTestMode = getParams.hasOwnProperty('oztestmode') ? getParams.oztestmode : null; // this can be any string, it's used for testing ads + const wlOztestmodeKey = whitelabelPrefix + 'testmode'; + const isTestMode = getParams[wlOztestmodeKey] || null; // this can be any string, it's used for testing ads ozoneRequest.device = {'w': window.innerWidth, 'h': window.innerHeight}; let placementIdOverrideFromGetParam = this.getPlacementIdOverrideFromGetParam(); // null or string - let lotameDataSingle = {}; // we will capture lotame data once & send it to the server as ext.ozone.lotameData // build the array of params to attach to `imp` let tosendtags = validBidRequests.map(ozoneBidRequest => { var obj = {}; @@ -168,18 +214,18 @@ export const spec = { let arrBannerSizes = []; if (!ozoneBidRequest.hasOwnProperty('mediaTypes')) { if (ozoneBidRequest.hasOwnProperty('sizes')) { - utils.logInfo('OZONE: no mediaTypes detected - will use the sizes array in the config root'); + this.logInfo('no mediaTypes detected - will use the sizes array in the config root'); arrBannerSizes = ozoneBidRequest.sizes; } else { - utils.logInfo('OZONE: no mediaTypes detected, no sizes array in the config root either. Cannot set sizes for banner type'); + this.logInfo('no mediaTypes detected, no sizes array in the config root either. Cannot set sizes for banner type'); } } else { if (ozoneBidRequest.mediaTypes.hasOwnProperty(BANNER)) { arrBannerSizes = ozoneBidRequest.mediaTypes[BANNER].sizes; /* Note - if there is a sizes element in the config root it will be pushed into here */ - utils.logInfo('OZONE: setting banner size from the mediaTypes.banner element for bidId ' + obj.id + ': ', arrBannerSizes); + this.logInfo('setting banner size from the mediaTypes.banner element for bidId ' + obj.id + ': ', arrBannerSizes); } if (ozoneBidRequest.mediaTypes.hasOwnProperty(VIDEO)) { - utils.logInfo('OZONE: openrtb 2.5 compliant video'); + this.logInfo('openrtb 2.5 compliant video'); // examine all the video attributes in the config, and either put them into obj.video if allowed by IAB2.5 or else in to obj.video.ext if (typeof ozoneBidRequest.mediaTypes[VIDEO] == 'object') { let childConfig = utils.deepAccess(ozoneBidRequest, 'params.video', {}); @@ -188,25 +234,25 @@ export const spec = { } // we need to duplicate some of the video values let wh = getWidthAndHeightFromVideoObject(obj.video); - utils.logInfo('OZONE: setting video object from the mediaTypes.video element: ' + obj.id + ':', obj.video, 'wh=', wh); + this.logInfo('setting video object from the mediaTypes.video element: ' + obj.id + ':', obj.video, 'wh=', wh); if (wh && typeof wh === 'object') { obj.video.w = wh['w']; obj.video.h = wh['h']; if (playerSizeIsNestedArray(obj.video)) { // this should never happen; it was in the original spec for this change though. - utils.logInfo('OZONE: setting obj.video.format to be an array of objects'); + this.logInfo('setting obj.video.format to be an array of objects'); obj.video.ext.format = [wh]; } else { - utils.logInfo('OZONE: setting obj.video.format to be an object'); + this.logInfo('setting obj.video.format to be an object'); obj.video.ext.format = wh; } } else { - utils.logWarn('OZONE: cannot set w, h & format values for video; the config is not right'); + this.logWarn('cannot set w, h & format values for video; the config is not right'); } } // Native integration is not complete yet if (ozoneBidRequest.mediaTypes.hasOwnProperty(NATIVE)) { obj.native = ozoneBidRequest.mediaTypes[NATIVE]; - utils.logInfo('OZONE: setting native object from the mediaTypes.native element: ' + obj.id + ':', obj.native); + this.logInfo('setting native object from the mediaTypes.native element: ' + obj.id + ':', obj.native); } } if (arrBannerSizes.length > 0) { @@ -223,52 +269,55 @@ export const spec = { // these 3 MUST exist - we check them in the validation method obj.placementId = placementId; // build the imp['ext'] object - obj.ext = {'prebid': {'storedrequest': {'id': placementId}}, 'ozone': {}}; - obj.ext.ozone.adUnitCode = ozoneBidRequest.adUnitCode; // eg. 'mpu' - obj.ext.ozone.transactionId = ozoneBidRequest.transactionId; // this is the transactionId PER adUnit, common across bidders for this unit + obj.ext = {'prebid': {'storedrequest': {'id': placementId}}}; + obj.ext[whitelabelBidder] = {}; + obj.ext[whitelabelBidder].adUnitCode = ozoneBidRequest.adUnitCode; // eg. 'mpu' + obj.ext[whitelabelBidder].transactionId = ozoneBidRequest.transactionId; // this is the transactionId PER adUnit, common across bidders for this unit if (ozoneBidRequest.params.hasOwnProperty('customData')) { - obj.ext.ozone.customData = ozoneBidRequest.params.customData; + obj.ext[whitelabelBidder].customData = ozoneBidRequest.params.customData; } - utils.logInfo('OZONE: obj.ext.ozone is ', obj.ext.ozone); - if (ozTestMode != null) { - utils.logInfo('OZONE: setting ozTestMode to ', ozTestMode); - if (obj.ext.ozone.hasOwnProperty('customData')) { - for (let i = 0; i < obj.ext.ozone.customData.length; i++) { - obj.ext.ozone.customData[i]['targeting']['oztestmode'] = ozTestMode; + this.logInfo(`obj.ext.${whitelabelBidder} is `, obj.ext[whitelabelBidder]); + if (isTestMode != null) { + this.logInfo('setting isTestMode to ', isTestMode); + if (obj.ext[whitelabelBidder].hasOwnProperty('customData')) { + for (let i = 0; i < obj.ext[whitelabelBidder].customData.length; i++) { + obj.ext[whitelabelBidder].customData[i]['targeting'][wlOztestmodeKey] = isTestMode; } } else { - obj.ext.ozone.customData = [{'settings': {}, 'targeting': {'oztestmode': ozTestMode}}]; + obj.ext[whitelabelBidder].customData = [{'settings': {}, 'targeting': {}}]; + obj.ext[whitelabelBidder].customData[0].targeting[wlOztestmodeKey] = isTestMode; } - } else { - utils.logInfo('OZONE: no ozTestMode '); - } - // now deal with lotame, including the optional override parameters - if (Object.keys(lotameDataSingle).length === 0) { // we've not yet found lotameData, see if we can get it from this bid request object - lotameDataSingle = this.tryGetLotameData(ozoneBidRequest); } return obj; }); // in v 2.0.0 we moved these outside of the individual ad slots - let extObj = {'ozone': {'oz_pb_v': OZONEVERSION, 'oz_rw': placementIdOverrideFromGetParam ? 1 : 0, 'oz_lot_rw': this.propertyBag.lotameWasOverridden}}; + let extObj = {}; + extObj[whitelabelBidder] = {}; + extObj[whitelabelBidder][whitelabelPrefix + '_pb_v'] = OZONEVERSION; + extObj[whitelabelBidder][whitelabelPrefix + '_rw'] = placementIdOverrideFromGetParam ? 1 : 0; if (validBidRequests.length > 0) { - let userIds = this.findAllUserIds(validBidRequests[0]); + let userIds = this.cookieSyncBag.userIdObject; // 2021-01-06 - slight optimisation - we've already found this info + // let userIds = this.findAllUserIds(validBidRequests[0]); if (userIds.hasOwnProperty('pubcid')) { - extObj.ozone.pubcid = userIds.pubcid; + extObj[whitelabelBidder].pubcid = userIds.pubcid; } } - extObj.ozone.pv = this.getPageId(); // attach the page ID that will be common to all auciton calls for this page if refresh() is called - extObj.ozone.lotameData = lotameDataSingle; // 2.4.0 moved lotameData out of bid objects into the single ext.ozone area to remove duplication - let ozOmpFloorDollars = config.getConfig('ozone.oz_omp_floor'); // valid only if a dollar value (typeof == 'number') - utils.logInfo('OZONE: oz_omp_floor dollar value = ', ozOmpFloorDollars); + extObj[whitelabelBidder].pv = this.getPageId(); // attach the page ID that will be common to all auciton calls for this page if refresh() is called + let ozOmpFloorDollars = this.getWhitelabelConfigItem('ozone.oz_omp_floor'); // valid only if a dollar value (typeof == 'number') + this.logInfo(`${whitelabelPrefix}_omp_floor dollar value = `, ozOmpFloorDollars); if (typeof ozOmpFloorDollars === 'number') { - extObj.ozone.oz_omp_floor = ozOmpFloorDollars; + extObj[whitelabelBidder][whitelabelPrefix + '_omp_floor'] = ozOmpFloorDollars; } else if (typeof ozOmpFloorDollars !== 'undefined') { - utils.logError('OZONE: oz_omp_floor is invalid - IF SET then this must be a number, representing dollar value eg. oz_omp_floor: 1.55. You have it set as a ' + (typeof ozOmpFloorDollars)); + this.logError(`${whitelabelPrefix}_omp_floor is invalid - IF SET then this must be a number, representing dollar value eg. ${whitelabelPrefix}_omp_floor: 1.55. You have it set as a ` + (typeof ozOmpFloorDollars)); } - let ozWhitelistAdserverKeys = config.getConfig('ozone.oz_whitelist_adserver_keys'); + let ozWhitelistAdserverKeys = this.getWhitelabelConfigItem('ozone.oz_whitelist_adserver_keys'); let useOzWhitelistAdserverKeys = utils.isArray(ozWhitelistAdserverKeys) && ozWhitelistAdserverKeys.length > 0; - extObj.ozone.oz_kvp_rw = useOzWhitelistAdserverKeys ? 1 : 0; + extObj[whitelabelBidder][whitelabelPrefix + '_kvp_rw'] = useOzWhitelistAdserverKeys ? 1 : 0; + if (whitelabelBidder != 'ozone') { + this.logInfo('setting aliases object'); + extObj.prebid = {aliases: {'ozone': whitelabelBidder}}; + } var userExtEids = this.generateEids(validBidRequests); // generate the UserIDs in the correct format for UserId module @@ -277,7 +326,7 @@ export const spec = { 'page': document.location.href, 'id': htmlParams.siteId }; - ozoneRequest.test = (getParams.hasOwnProperty('pbjs_debug') && getParams['pbjs_debug'] == 'true') ? 1 : 0; + ozoneRequest.test = (getParams.hasOwnProperty('pbjs_debug') && getParams['pbjs_debug'] === 'true') ? 1 : 0; // this is for 2.2.1 // coppa compliance @@ -287,7 +336,7 @@ export const spec = { // return the single request object OR the array: if (singleRequest) { - utils.logInfo('OZONE: buildRequests starting to generate response for a single request'); + this.logInfo('buildRequests starting to generate response for a single request'); ozoneRequest.id = bidderRequest.auctionId; // Unique ID of the bid request, provided by the exchange. ozoneRequest.auctionId = bidderRequest.auctionId; // not sure if this should be here? ozoneRequest.imp = tosendtags; @@ -296,36 +345,36 @@ export const spec = { utils.deepSetValue(ozoneRequest, 'user.ext.eids', userExtEids); var ret = { method: 'POST', - url: OZONEURI, + url: this.getAuctionUrl(), data: JSON.stringify(ozoneRequest), bidderRequest: bidderRequest }; - utils.logInfo('OZONE: buildRequests ozoneRequest for single = ', ozoneRequest); + this.logInfo('buildRequests request data for single = ', ozoneRequest); this.propertyBag.buildRequestsEnd = new Date().getTime(); - utils.logInfo(`OZONE: buildRequests going to return for single at time ${this.propertyBag.buildRequestsEnd} (took ${this.propertyBag.buildRequestsEnd - this.propertyBag.buildRequestsStart}ms): `, ret); + this.logInfo(`buildRequests going to return for single at time ${this.propertyBag.buildRequestsEnd} (took ${this.propertyBag.buildRequestsEnd - this.propertyBag.buildRequestsStart}ms): `, ret); return ret; } // not single request - pull apart the tosendtags array & return an array of objects each containing one element in the imp array. let arrRet = tosendtags.map(imp => { - utils.logInfo('OZONE: buildRequests starting to generate non-single response, working on imp : ', imp); + this.logInfo('buildRequests starting to generate non-single response, working on imp : ', imp); let ozoneRequestSingle = Object.assign({}, ozoneRequest); - imp.ext.ozone.pageAuctionId = bidderRequest['auctionId']; // make a note in the ext object of what the original auctionId was, in the bidderRequest object - ozoneRequestSingle.id = imp.ext.ozone.transactionId; // Unique ID of the bid request, provided by the exchange. - ozoneRequestSingle.auctionId = imp.ext.ozone.transactionId; // not sure if this should be here? + imp.ext[whitelabelBidder].pageAuctionId = bidderRequest['auctionId']; // make a note in the ext object of what the original auctionId was, in the bidderRequest object + ozoneRequestSingle.id = imp.ext[whitelabelBidder].transactionId; // Unique ID of the bid request, provided by the exchange. + ozoneRequestSingle.auctionId = imp.ext[whitelabelBidder].transactionId; // not sure if this should be here? ozoneRequestSingle.imp = [imp]; ozoneRequestSingle.ext = extObj; - ozoneRequestSingle.source = {'tid': imp.ext.ozone.transactionId}; + ozoneRequestSingle.source = {'tid': imp.ext[whitelabelBidder].transactionId}; utils.deepSetValue(ozoneRequestSingle, 'user.ext.eids', userExtEids); - utils.logInfo('OZONE: buildRequests ozoneRequestSingle (for non-single) = ', ozoneRequestSingle); + this.logInfo('buildRequests RequestSingle (for non-single) = ', ozoneRequestSingle); return { method: 'POST', - url: OZONEURI, + url: this.getAuctionUrl(), data: JSON.stringify(ozoneRequestSingle), bidderRequest: bidderRequest }; }); this.propertyBag.buildRequestsEnd = new Date().getTime(); - utils.logInfo(`OZONE: buildRequests going to return for non-single at time ${this.propertyBag.buildRequestsEnd} (took ${this.propertyBag.buildRequestsEnd - this.propertyBag.buildRequestsStart}ms): `, arrRet); + this.logInfo(`buildRequests going to return for non-single at time ${this.propertyBag.buildRequestsEnd} (took ${this.propertyBag.buildRequestsEnd - this.propertyBag.buildRequestsStart}ms): `, arrRet); return arrRet; }, /** @@ -339,9 +388,12 @@ export const spec = { * @returns {*} */ interpretResponse(serverResponse, request) { + if (request && request.bidderRequest && request.bidderRequest.bids) { this.loadWhitelabelData(request.bidderRequest.bids[0]); } let startTime = new Date().getTime(); - utils.logInfo(`OZONE: interpretResponse time: ${startTime} . Time between buildRequests done and interpretResponse start was ${startTime - this.propertyBag.buildRequestsEnd}ms`); - utils.logInfo(`OZONE: serverResponse, request`, serverResponse, request); + let whitelabelBidder = this.propertyBag.whitelabel.bidder; // by default = ozone + let whitelabelPrefix = this.propertyBag.whitelabel.keyPrefix; + this.logInfo(`interpretResponse time: ${startTime} . Time between buildRequests done and interpretResponse start was ${startTime - this.propertyBag.buildRequestsEnd}ms`); + this.logInfo(`serverResponse, request`, JSON.parse(JSON.stringify(serverResponse)), JSON.parse(JSON.stringify(request))); serverResponse = serverResponse.body || {}; // note that serverResponse.id value is the auction_id we might want to use for reporting reasons. if (!serverResponse.hasOwnProperty('seatbid')) { @@ -351,91 +403,91 @@ export const spec = { return []; } let arrAllBids = []; - let enhancedAdserverTargeting = config.getConfig('ozone.enhancedAdserverTargeting'); - utils.logInfo('OZONE: enhancedAdserverTargeting', enhancedAdserverTargeting); + let enhancedAdserverTargeting = this.getWhitelabelConfigItem('ozone.enhancedAdserverTargeting'); + this.logInfo('enhancedAdserverTargeting', enhancedAdserverTargeting); if (typeof enhancedAdserverTargeting == 'undefined') { enhancedAdserverTargeting = true; } - utils.logInfo('OZONE: enhancedAdserverTargeting', enhancedAdserverTargeting); + this.logInfo('enhancedAdserverTargeting', enhancedAdserverTargeting); serverResponse.seatbid = injectAdIdsIntoAllBidResponses(serverResponse.seatbid); // we now make sure that each bid in the bidresponse has a unique (within page) adId attribute. serverResponse.seatbid = this.removeSingleBidderMultipleBids(serverResponse.seatbid); - let ozOmpFloorDollars = config.getConfig('ozone.oz_omp_floor'); // valid only if a dollar value (typeof == 'number') + let ozOmpFloorDollars = this.getWhitelabelConfigItem('ozone.oz_omp_floor'); // valid only if a dollar value (typeof == 'number') let addOzOmpFloorDollars = typeof ozOmpFloorDollars === 'number'; - let ozWhitelistAdserverKeys = config.getConfig('ozone.oz_whitelist_adserver_keys'); + let ozWhitelistAdserverKeys = this.getWhitelabelConfigItem('ozone.oz_whitelist_adserver_keys'); let useOzWhitelistAdserverKeys = utils.isArray(ozWhitelistAdserverKeys) && ozWhitelistAdserverKeys.length > 0; for (let i = 0; i < serverResponse.seatbid.length; i++) { let sb = serverResponse.seatbid[i]; for (let j = 0; j < sb.bid.length; j++) { let thisRequestBid = this.getBidRequestForBidId(sb.bid[j].impid, request.bidderRequest.bids); - utils.logInfo(`OZONE seatbid:${i}, bid:${j} Going to set default w h for seatbid/bidRequest`, sb.bid[j], thisRequestBid); + this.logInfo(`seatbid:${i}, bid:${j} Going to set default w h for seatbid/bidRequest`, sb.bid[j], thisRequestBid); const {defaultWidth, defaultHeight} = defaultSize(thisRequestBid); let thisBid = ozoneAddStandardProperties(sb.bid[j], defaultWidth, defaultHeight); let videoContext = null; let isVideo = false; let bidType = utils.deepAccess(thisBid, 'ext.prebid.type'); - utils.logInfo(`OZONE: this bid type is : ${bidType}`, j); + this.logInfo(`this bid type is : ${bidType}`, j); if (bidType === VIDEO) { isVideo = true; videoContext = this.getVideoContextForBidId(thisBid.bidId, request.bidderRequest.bids); // should be instream or outstream (or null if error) if (videoContext === 'outstream') { - utils.logInfo('OZONE: going to attach a renderer to OUTSTREAM video : ', j); + this.logInfo('going to attach a renderer to OUTSTREAM video : ', j); thisBid.renderer = newRenderer(thisBid.bidId); } else { - utils.logInfo('OZONE: bid is not an outstream video, will not attach a renderer: ', j); + this.logInfo('bid is not an outstream video, will not attach a renderer: ', j); } } let adserverTargeting = {}; if (enhancedAdserverTargeting) { let allBidsForThisBidid = ozoneGetAllBidsForBidId(thisBid.bidId, serverResponse.seatbid); // add all the winning & non-winning bids for this bidId: - utils.logInfo('OZONE: Going to iterate allBidsForThisBidId', allBidsForThisBidid); - Object.keys(allBidsForThisBidid).forEach(function (bidderName, index, ar2) { - utils.logInfo(`OZONE: adding adserverTargeting for ${bidderName} for bidId ${thisBid.bidId}`); + this.logInfo('Going to iterate allBidsForThisBidId', allBidsForThisBidid); + Object.keys(allBidsForThisBidid).forEach((bidderName, index, ar2) => { + this.logInfo(`adding adserverTargeting for ${bidderName} for bidId ${thisBid.bidId}`); // let bidderName = bidderNameWH.split('_')[0]; - adserverTargeting['oz_' + bidderName] = bidderName; - adserverTargeting['oz_' + bidderName + '_crid'] = String(allBidsForThisBidid[bidderName].crid); - adserverTargeting['oz_' + bidderName + '_adv'] = String(allBidsForThisBidid[bidderName].adomain); - adserverTargeting['oz_' + bidderName + '_adId'] = String(allBidsForThisBidid[bidderName].adId); - adserverTargeting['oz_' + bidderName + '_pb_r'] = getRoundedBid(allBidsForThisBidid[bidderName].price, allBidsForThisBidid[bidderName].ext.prebid.type); + adserverTargeting[whitelabelPrefix + '_' + bidderName] = bidderName; + adserverTargeting[whitelabelPrefix + '_' + bidderName + '_crid'] = String(allBidsForThisBidid[bidderName].crid); + adserverTargeting[whitelabelPrefix + '_' + bidderName + '_adv'] = String(allBidsForThisBidid[bidderName].adomain); + adserverTargeting[whitelabelPrefix + '_' + bidderName + '_adId'] = String(allBidsForThisBidid[bidderName].adId); + adserverTargeting[whitelabelPrefix + '_' + bidderName + '_pb_r'] = getRoundedBid(allBidsForThisBidid[bidderName].price, allBidsForThisBidid[bidderName].ext.prebid.type); if (allBidsForThisBidid[bidderName].hasOwnProperty('dealid')) { - adserverTargeting['oz_' + bidderName + '_dealid'] = String(allBidsForThisBidid[bidderName].dealid); + adserverTargeting[whitelabelPrefix + '_' + bidderName + '_dealid'] = String(allBidsForThisBidid[bidderName].dealid); } if (addOzOmpFloorDollars) { - adserverTargeting['oz_' + bidderName + '_omp'] = allBidsForThisBidid[bidderName].price >= ozOmpFloorDollars ? '1' : '0'; + adserverTargeting[whitelabelPrefix + '_' + bidderName + '_omp'] = allBidsForThisBidid[bidderName].price >= ozOmpFloorDollars ? '1' : '0'; } if (isVideo) { - adserverTargeting['oz_' + bidderName + '_vid'] = videoContext; // outstream or instream + adserverTargeting[whitelabelPrefix + '_' + bidderName + '_vid'] = videoContext; // outstream or instream } - let flr = utils.deepAccess(allBidsForThisBidid[bidderName], 'ext.bidder.ozone.floor', null); + let flr = utils.deepAccess(allBidsForThisBidid[bidderName], `ext.bidder.${whitelabelBidder}.floor`, null); if (flr != null) { - adserverTargeting['oz_' + bidderName + '_flr'] = flr; + adserverTargeting[whitelabelPrefix + '_' + bidderName + '_flr'] = flr; } - let rid = utils.deepAccess(allBidsForThisBidid[bidderName], 'ext.bidder.ozone.ruleId', null); + let rid = utils.deepAccess(allBidsForThisBidid[bidderName], `ext.bidder.${whitelabelBidder}.ruleId`, null); if (rid != null) { - adserverTargeting['oz_' + bidderName + '_rid'] = rid; + adserverTargeting[whitelabelPrefix + '_' + bidderName + '_rid'] = rid; } if (bidderName.match(/^ozappnexus/)) { - adserverTargeting['oz_' + bidderName + '_sid'] = String(allBidsForThisBidid[bidderName].cid); + adserverTargeting[whitelabelPrefix + '_' + bidderName + '_sid'] = String(allBidsForThisBidid[bidderName].cid); } }); } else { if (useOzWhitelistAdserverKeys) { - utils.logWarn('OZONE: You have set a whitelist of adserver keys but this will be ignored because ozone.enhancedAdserverTargeting is set to false. No per-bid keys will be sent to adserver.'); + this.logWarn(`You have set a whitelist of adserver keys but this will be ignored because ${whitelabelBidder}.enhancedAdserverTargeting is set to false. No per-bid keys will be sent to adserver.`); } else { - utils.logInfo('OZONE: ozone.enhancedAdserverTargeting is set to false, so no per-bid keys will be sent to adserver.'); + this.logInfo(`${whitelabelBidder}.enhancedAdserverTargeting is set to false, so no per-bid keys will be sent to adserver.`); } } // also add in the winning bid, to be sent to dfp let {seat: winningSeat, bid: winningBid} = ozoneGetWinnerForRequestBid(thisBid.bidId, serverResponse.seatbid); - adserverTargeting['oz_auc_id'] = String(request.bidderRequest.auctionId); - adserverTargeting['oz_winner'] = String(winningSeat); + adserverTargeting[whitelabelPrefix + '_auc_id'] = String(request.bidderRequest.auctionId); + adserverTargeting[whitelabelPrefix + '_winner'] = String(winningSeat); if (enhancedAdserverTargeting) { - adserverTargeting['oz_imp_id'] = String(winningBid.impid); - adserverTargeting['oz_pb_v'] = OZONEVERSION; + adserverTargeting[whitelabelPrefix + '_imp_id'] = String(winningBid.impid); + adserverTargeting[whitelabelPrefix + '_pb_v'] = OZONEVERSION; } if (useOzWhitelistAdserverKeys) { // delete any un-whitelisted keys - utils.logInfo('OZONE: Going to filter out adserver targeting keys not in the whitelist: ', ozWhitelistAdserverKeys); + this.logInfo('Going to filter out adserver targeting keys not in the whitelist: ', ozWhitelistAdserverKeys); Object.keys(adserverTargeting).forEach(function(key) { if (ozWhitelistAdserverKeys.indexOf(key) === -1) { delete adserverTargeting[key]; } }); } thisBid.adserverTargeting = adserverTargeting; @@ -443,9 +495,22 @@ export const spec = { } } let endTime = new Date().getTime(); - utils.logInfo(`OZONE: interpretResponse going to return at time ${endTime} (took ${endTime - startTime}ms) Time from buildRequests Start -> interpretRequests End = ${endTime - this.propertyBag.buildRequestsStart}ms`, arrAllBids); + this.logInfo(`interpretResponse going to return at time ${endTime} (took ${endTime - startTime}ms) Time from buildRequests Start -> interpretRequests End = ${endTime - this.propertyBag.buildRequestsStart}ms`, arrAllBids); return arrAllBids; }, + /** + * Use this to get all config values + * Now it's getting complicated with whitelabeling, this simplifies the code for getting config values. + * eg. to get ozone.oz_omp_floor you just send '_omp_floor' + * @param ozoneVersion string like 'ozone.oz_omp_floor' + * @return {string|object} + */ + getWhitelabelConfigItem(ozoneVersion) { + if (this.propertyBag.whitelabel.bidder == 'ozone') { return config.getConfig(ozoneVersion); } + let whitelabelledSearch = ozoneVersion.replace('ozone', this.propertyBag.whitelabel.bidder); + whitelabelledSearch = ozoneVersion.replace('oz_', this.propertyBag.whitelabel.keyPrefix + '_'); + return config.getConfig(whitelabelledSearch); + }, /** * If a bidder bids for > 1 size for an adslot, allow only the highest bid * @param seatbid object (serverResponse.seatbid) @@ -475,7 +540,7 @@ export const spec = { }, // see http://prebid.org/dev-docs/bidder-adaptor.html#registering-user-syncs getUserSyncs(optionsType, serverResponse, gdprConsent) { - utils.logInfo('OZONE: getUserSyncs optionsType, serverResponse, gdprConsent, cookieSyncBag', optionsType, serverResponse, gdprConsent, this.cookieSyncBag); + this.logInfo('getUserSyncs optionsType, serverResponse, gdprConsent, cookieSyncBag', optionsType, serverResponse, gdprConsent, this.cookieSyncBag); if (!serverResponse || serverResponse.length === 0) { return []; } @@ -494,15 +559,16 @@ export const spec = { arrQueryString.push('publisherId=' + this.cookieSyncBag.publisherId); arrQueryString.push('siteId=' + this.cookieSyncBag.siteId); arrQueryString.push('cb=' + Date.now()); + arrQueryString.push('bidder=' + this.propertyBag.whitelabel.bidder); var strQueryString = arrQueryString.join('&'); if (strQueryString.length > 0) { strQueryString = '?' + strQueryString; } - utils.logInfo('OZONE: getUserSyncs going to return cookie sync url : ' + OZONECOOKIESYNC + strQueryString); + this.logInfo('getUserSyncs going to return cookie sync url : ' + this.getCookieSyncUrl() + strQueryString); return [{ type: 'iframe', - url: OZONECOOKIESYNC + strQueryString + url: this.getCookieSyncUrl() + strQueryString }]; } }, @@ -535,26 +601,31 @@ export const spec = { }, /** * Look for pubcid & all the other IDs according to http://prebid.org/dev-docs/modules/userId.html + * NOTE that criteortus is deprecated & should be removed asap * @return map */ findAllUserIds(bidRequest) { var ret = {}; - let searchKeysSingle = ['pubcid', 'tdid', 'parrableId', 'idl_env', 'digitrustid', 'criteortus']; + // @todo - what is fabrick called & where to look for it? If it's a simple value then it will automatically be ok + let searchKeysSingle = ['pubcid', 'tdid', 'id5id', 'parrableId', 'idl_env', 'criteoId', 'criteortus', + 'sharedid', 'lotamePanoramaId', 'fabrickId']; if (bidRequest.hasOwnProperty('userId')) { for (let arrayId in searchKeysSingle) { let key = searchKeysSingle[arrayId]; if (bidRequest.userId.hasOwnProperty(key)) { - ret[key] = bidRequest.userId[key]; + if (typeof (bidRequest.userId[key]) == 'string') { + ret[key] = bidRequest.userId[key]; + } else if (typeof (bidRequest.userId[key]) == 'object') { + ret[key] = bidRequest.userId[key][Object.keys(bidRequest.userId[key])[0]]; // cannot use Object.values + } else { + this.logError(`failed to get string key value for userId : ${key}`); + } } } var lipbid = utils.deepAccess(bidRequest.userId, 'lipb.lipbid'); if (lipbid) { ret['lipb'] = {'lipbid': lipbid}; } - var id5id = utils.deepAccess(bidRequest.userId, 'id5id.uid'); - if (id5id) { - ret['id5id'] = id5id; - } } if (!ret.hasOwnProperty('pubcid')) { var pubcid = utils.deepAccess(bidRequest, 'crumbs.pubcid'); @@ -564,69 +635,6 @@ export const spec = { } return ret; }, - /** - * get all the lotame override keys/values from the querystring. - * @return object containing zero or more keys/values - */ - getLotameOverrideParams() { - const arrGet = this.getGetParametersAsObject(); - utils.logInfo('OZONE: getLotameOverrideParams - arrGet=', arrGet); - let arrRet = {}; - for (let i in ALLOWED_LOTAME_PARAMS) { - if (arrGet.hasOwnProperty(ALLOWED_LOTAME_PARAMS[i])) { - arrRet[ALLOWED_LOTAME_PARAMS[i]] = arrGet[ALLOWED_LOTAME_PARAMS[i]]; - } - } - return arrRet; - }, - /** - * Boolean function to check that this lotame data is valid (check Audience.id) - */ - isLotameDataValid(lotameObj) { - if (!lotameObj.hasOwnProperty('Profile')) return false; - let prof = lotameObj.Profile; - if (!prof.hasOwnProperty('tpid')) return false; - if (!prof.hasOwnProperty('pid')) return false; - let audiences = utils.deepAccess(prof, 'Audiences.Audience'); - if (typeof audiences != 'object') { - return false; - } - for (var i = 0; i < audiences.length; i++) { - let aud = audiences[i]; - if (!aud.hasOwnProperty('id')) { - return false; - } - } - return true; // All Audiences objects have an 'id' key - }, - /** - * Use the arrOverride keys/vals to update the arrExisting lotame object. - * Ideally we will only be using the oz_lotameid value to update the audiences id, but in the event of bad/missing - * pid & tpid we will also have to use substitute values for those too. - * - * @param objOverride object will contain all the ALLOWED_LOTAME_PARAMS parameters - * @param lotameData object might be {} or contain the lotame data - */ - makeLotameObjectFromOverride(objOverride, lotameData) { - if ((lotameData.hasOwnProperty('Profile') && Object.keys(lotameData.Profile).length < 3) || - (!lotameData.hasOwnProperty('Profile'))) { // bad or empty lotame object (should contain pid, tpid & Audiences object) - build a total replacement - utils.logInfo('OZONE: makeLotameObjectFromOverride will return a full default lotame object'); - return { - 'Profile': { - 'tpid': objOverride['oz_lotametpid'], - 'pid': objOverride['oz_lotamepid'], - 'Audiences': {'Audience': [{'id': objOverride['oz_lotameid'], 'abbr': objOverride['oz_lotameid']}]} - } - }; - } - if (utils.deepAccess(lotameData, 'Profile.Audiences.Audience')) { - utils.logInfo('OZONE: makeLotameObjectFromOverride will return the existing lotame object with updated Audience by oz_lotameid'); - lotameData.Profile.Audiences.Audience = [{'id': objOverride['oz_lotameid'], 'abbr': objOverride['oz_lotameid']}]; - return lotameData; - } - utils.logInfo('OZONE: makeLotameObjectFromOverride Weird error - failed to find Profile.Audiences.Audience in lotame object. Will return the object as-is'); - return lotameData; - }, /** * Convenient method to get the value we need for the placementId - ONLY from the bidRequest - NOT taking into account any GET override ID * @param bidRequest @@ -642,48 +650,30 @@ export const spec = { * @returns null|string */ getPlacementIdOverrideFromGetParam() { + let whitelabelPrefix = this.propertyBag.whitelabel.keyPrefix; let arr = this.getGetParametersAsObject(); - if (arr.hasOwnProperty('ozstoredrequest')) { - if (this.isValidPlacementId(arr.ozstoredrequest)) { - utils.logInfo('OZONE: using GET ozstoredrequest ' + arr.ozstoredrequest + ' to replace placementId'); - return arr.ozstoredrequest; + if (arr.hasOwnProperty(whitelabelPrefix + 'storedrequest')) { + if (this.isValidPlacementId(arr[whitelabelPrefix + 'storedrequest'])) { + this.logInfo(`using GET ${whitelabelPrefix}storedrequest ` + arr[whitelabelPrefix + 'storedrequest'] + ' to replace placementId'); + return arr[whitelabelPrefix + 'storedrequest']; } else { - utils.logError('OZONE: GET ozstoredrequest FAILED VALIDATION - will not use it'); + this.logError(`GET ${whitelabelPrefix}storedrequest FAILED VALIDATION - will not use it`); } } return null; }, - /** - * Produces external userid object - */ - addExternalUserId(eids, value, source, atype) { - if (utils.isStr(value)) { - eids.push({ - source, - uids: [{ - id: value, - atype - }] - }); - } - }, /** * Generate an object we can append to the auction request, containing user data formatted correctly for different ssps + * http://prebid.org/dev-docs/modules/userId.html * @param validBidRequests * @return {Array} */ generateEids(validBidRequests) { - let eids = []; - this.handleTTDId(eids, validBidRequests); + let eids; const bidRequest = validBidRequests[0]; if (bidRequest && bidRequest.userId) { - this.addExternalUserId(eids, utils.deepAccess(bidRequest, `userId.pubcid`), 'pubcid', 1); - this.addExternalUserId(eids, utils.deepAccess(bidRequest, `userId.pubcid`), 'pubcommon', 1); - this.addExternalUserId(eids, utils.deepAccess(bidRequest, `userId.id5id.uid`), 'id5-sync.com', 1); - this.addExternalUserId(eids, utils.deepAccess(bidRequest, `userId.criteortus.${BIDDER_CODE}.userid`), 'criteortus', 1); - this.addExternalUserId(eids, utils.deepAccess(bidRequest, `userId.idl_env`), 'liveramp.com', 1); - this.addExternalUserId(eids, utils.deepAccess(bidRequest, `userId.lipb.lipbid`), 'liveintent.com', 1); - this.addExternalUserId(eids, utils.deepAccess(bidRequest, `userId.parrableId.eid`), 'parrable.com', 1); + eids = bidRequest.userIdAsEids; + this.handleTTDId(eids, validBidRequests); } return eids; }, @@ -725,9 +715,9 @@ export const spec = { */ blockTheRequest() { // if there is an ozone.oz_request = false then quit now. - let ozRequest = config.getConfig('ozone.oz_request'); + let ozRequest = this.getWhitelabelConfigItem('ozone.oz_request'); if (typeof ozRequest == 'boolean' && !ozRequest) { - utils.logWarn('OZONE: Will not allow auction : ozone.oz_request is set to false'); + this.logWarn(`Will not allow auction : ${this.propertyBag.whitelabel.keyPrefix}one.${this.propertyBag.whitelabel.keyPrefix}_request is set to false`); return true; } return false; @@ -747,34 +737,6 @@ export const spec = { } return this.propertyBag.pageId; }, - /** - * handle the complexity of there possibly being lotameData override (may be valid/invalid) & there may or may not be lotameData present in the bidRequest - * NOTE THAT this will also set this.propertyBag.lotameWasOverridden=1 if we use lotame override - * @param ozoneBidRequest - * @return object representing the absolute lotameData we need to use. - */ - tryGetLotameData: function(ozoneBidRequest) { - const arrLotameOverride = this.getLotameOverrideParams(); - let ret = {}; - if (Object.keys(arrLotameOverride).length === ALLOWED_LOTAME_PARAMS.length) { - // all override params are present, override lotame object: - if (ozoneBidRequest.params.hasOwnProperty('lotameData')) { - ret = this.makeLotameObjectFromOverride(arrLotameOverride, ozoneBidRequest.params.lotameData); - } else { - ret = this.makeLotameObjectFromOverride(arrLotameOverride, {}); - } - this.propertyBag.lotameWasOverridden = 1; - } else if (ozoneBidRequest.params.hasOwnProperty('lotameData')) { - // no lotame override, use it as-is - if (this.isLotameDataValid(ozoneBidRequest.params.lotameData)) { - ret = ozoneBidRequest.params.lotameData; - } else { - utils.logError('OZONE: INVALID LOTAME DATA FOUND - WILL NOT USE THIS AT ALL ELSE IT MIGHT BREAK THE AUCTION CALL!', ozoneBidRequest.params.lotameData); - ret = {}; - } - } - return ret; - }, unpackVideoConfigIntoIABformat(videoConfig, childConfig) { let ret = {'ext': {}}; ret = this._unpackVideoConfigIntoIABformat(ret, videoConfig); @@ -853,7 +815,7 @@ export const spec = { * @returns seatbid object */ export function injectAdIdsIntoAllBidResponses(seatbid) { - utils.logInfo('OZONE: injectAdIdsIntoAllBidResponses', seatbid); + spec.logInfo('injectAdIdsIntoAllBidResponses', seatbid); for (let i = 0; i < seatbid.length; i++) { let sb = seatbid[i]; for (let j = 0; j < sb.bid.length; j++) { @@ -879,7 +841,7 @@ export function checkDeepArray(Arr) { export function defaultSize(thebidObj) { if (!thebidObj) { - utils.logInfo('OZONE: defaultSize received empty bid obj! going to return fixed default size'); + spec.logInfo('defaultSize received empty bid obj! going to return fixed default size'); return { 'defaultHeight': 250, 'defaultWidth': 300 @@ -932,7 +894,7 @@ export function ozoneGetAllBidsForBidId(matchBidId, serverResponseSeatBid) { for (let k = 0; k < theseBids.length; k++) { if (theseBids[k].impid === matchBidId) { if (objBids.hasOwnProperty(thisSeat)) { // > 1 bid for an adunit from a bidder - only use the one with the highest bid - // objBids[`${thisSeat}${theseBids[k].w}x${theseBids[k].h}`] = theseBids[k]; + // objBids[`${thisSeat}${theseBids[k].w}x${theseBids[k].h}`] = theseBids[k]; if (objBids[thisSeat]['price'] < theseBids[k].price) { objBids[thisSeat] = theseBids[k]; } @@ -957,14 +919,14 @@ export function getRoundedBid(price, mediaType) { let theConfigObject = getGranularityObject(mediaType, mediaTypeGranularity, strBuckets, objBuckets); let theConfigKey = getGranularityKeyName(mediaType, mediaTypeGranularity, strBuckets); - utils.logInfo('OZONE: getRoundedBid. price:', price, 'mediaType:', mediaType, 'configkey:', theConfigKey, 'configObject:', theConfigObject, 'mediaTypeGranularity:', mediaTypeGranularity, 'strBuckets:', strBuckets); + spec.logInfo('getRoundedBid. price:', price, 'mediaType:', mediaType, 'configkey:', theConfigKey, 'configObject:', theConfigObject, 'mediaTypeGranularity:', mediaTypeGranularity, 'strBuckets:', strBuckets); let priceStringsObj = getPriceBucketString( price, theConfigObject, config.getConfig('currency.granularityMultiplier') ); - utils.logInfo('OZONE: priceStringsObj', priceStringsObj); + spec.logInfo('priceStringsObj', priceStringsObj); // by default, without any custom granularity set, you get granularity name : 'medium' let granularityNamePriceStringsKeyMapping = { 'medium': 'med', @@ -975,7 +937,7 @@ export function getRoundedBid(price, mediaType) { }; if (granularityNamePriceStringsKeyMapping.hasOwnProperty(theConfigKey)) { let priceStringsKey = granularityNamePriceStringsKeyMapping[theConfigKey]; - utils.logInfo('OZONE: getRoundedBid: looking for priceStringsKey:', priceStringsKey); + spec.logInfo('getRoundedBid: looking for priceStringsKey:', priceStringsKey); return priceStringsObj[priceStringsKey]; } return priceStringsObj['auto']; @@ -1044,15 +1006,15 @@ export function getWidthAndHeightFromVideoObject(objVideo) { return null; } if (playerSize[0] && typeof playerSize[0] === 'object') { - utils.logInfo('OZONE: getWidthAndHeightFromVideoObject found nested array inside playerSize.', playerSize[0]); + spec.logInfo('getWidthAndHeightFromVideoObject found nested array inside playerSize.', playerSize[0]); playerSize = playerSize[0]; if (typeof playerSize[0] !== 'number' && typeof playerSize[0] !== 'string') { - utils.logInfo('OZONE: getWidthAndHeightFromVideoObject found non-number/string type inside the INNER array in playerSize. This is totally wrong - cannot continue.', playerSize[0]); + spec.logInfo('getWidthAndHeightFromVideoObject found non-number/string type inside the INNER array in playerSize. This is totally wrong - cannot continue.', playerSize[0]); return null; } } if (playerSize.length !== 2) { - utils.logInfo('OZONE: getWidthAndHeightFromVideoObject found playerSize with length of ' + playerSize.length + '. This is totally wrong - cannot continue.'); + spec.logInfo('getWidthAndHeightFromVideoObject found playerSize with length of ' + playerSize.length + '. This is totally wrong - cannot continue.'); return null; } return ({'w': playerSize[0], 'h': playerSize[1]}); @@ -1079,17 +1041,17 @@ export function playerSizeIsNestedArray(objVideo) { * @returns {*} */ function getPlayerSizeFromObject(objVideo) { - utils.logInfo('OZONE: getPlayerSizeFromObject received object', objVideo); + spec.logInfo('getPlayerSizeFromObject received object', objVideo); let playerSize = utils.deepAccess(objVideo, 'playerSize'); if (!playerSize) { playerSize = utils.deepAccess(objVideo, 'ext.playerSize'); } if (!playerSize) { - utils.logError('OZONE: getPlayerSizeFromObject FAILED: no playerSize in video object or ext', objVideo); + spec.logError('getPlayerSizeFromObject FAILED: no playerSize in video object or ext', objVideo); return null; } if (typeof playerSize !== 'object') { - utils.logError('OZONE: getPlayerSizeFromObject FAILED: playerSize is not an object/array', objVideo); + spec.logError('getPlayerSizeFromObject FAILED: playerSize is not an object/array', objVideo); return null; } return playerSize; @@ -1099,21 +1061,23 @@ function getPlayerSizeFromObject(objVideo) { The renderer function will not assume that the renderer script is loaded - it will push() the ultimate render function call */ function newRenderer(adUnitCode, rendererOptions = {}) { + let isLoaded = window.ozoneVideo; + spec.logInfo(`newRenderer going to set loaded to ${isLoaded ? 'true' : 'false'}`); const renderer = Renderer.install({ - url: OZONE_RENDERER_URL, + url: spec.getRendererUrl(), config: rendererOptions, - loaded: false, + loaded: isLoaded, adUnitCode }); try { renderer.setRender(outstreamRender); } catch (err) { - utils.logWarn('OZONE Prebid Error calling setRender on renderer', err); + spec.logError('Prebid Error when calling setRender on renderer', JSON.parse(JSON.stringify(renderer)), err); } return renderer; } function outstreamRender(bid) { - utils.logInfo('OZONE: outstreamRender called. Going to push the call to window.ozoneVideo.outstreamRender(bid) bid =', bid); + spec.logInfo('outstreamRender called. Going to push the call to window.ozoneVideo.outstreamRender(bid) bid =', JSON.parse(JSON.stringify(bid))); // push to render queue because ozoneVideo may not be loaded yet bid.renderer.push(() => { window.ozoneVideo.outstreamRender(bid); @@ -1121,4 +1085,4 @@ function outstreamRender(bid) { } registerBidder(spec); -utils.logInfo('OZONE: ozoneBidAdapter was loaded'); +utils.logInfo(`*BidAdapter ${OZONEVERSION} was loaded`); diff --git a/modules/ozoneBidAdapter.md b/modules/ozoneBidAdapter.md index bc8cb6a6102..ca18c962219 100644 --- a/modules/ozoneBidAdapter.md +++ b/modules/ozoneBidAdapter.md @@ -37,7 +37,6 @@ adUnits = [{ siteId: '4204204201', /* An ID used to identify a site within a publisher account - required */ placementId: '0420420421', /* an ID used to identify the piece of inventory - required - for appnexus test use 13144370. */ customData: [{"settings": {}, "targeting": {"key": "value", "key2": ["value1", "value2"]}}],/* optional array with 'targeting' placeholder for passing publisher specific key-values for targeting. */ - lotameData: {"Profile": {"tpid":"value","pid":"value","Audiences": {"Audience":[{"id":"value"},{"id":"value2"}]}}}, /* optional JSON placeholder for passing Lotame DMP data */ } }] }]; @@ -52,7 +51,7 @@ adUnits = [{ code: 'id-of-your-video-div', mediaTypes: { video: { - playerSize: [640, 480], + playerSize: [640, 360], mimes: ['video/mp4'], context: 'outstream', } @@ -64,7 +63,6 @@ adUnits = [{ siteId: '4204204201', /* An ID used to identify a site within a publisher account - required */ customData: [{"settings": {}, "targeting": { "key": "value", "key2": ["value1", "value2"]}}] placementId: '0440440442', /* an ID used to identify the piece of inventory - required - for unruly test use 0440440442. */ - lotameData: {"Profile": {"tpid":"value","pid":"value","Audiences": {"Audience":[{"id":"value"},{"id":"value2"}]}}}, /* optional JSON placeholder for passing Lotame DMP data */ video: { skippable: true, /* optional */ playback_method: ['auto_play_sound_off'], /* optional */ diff --git a/modules/parrableIdSystem.js b/modules/parrableIdSystem.js index 7587962c62b..826dd9a933a 100644 --- a/modules/parrableIdSystem.js +++ b/modules/parrableIdSystem.js @@ -6,6 +6,7 @@ */ import * as utils 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'; @@ -14,12 +15,13 @@ import { getStorageManager } from '../src/storageManager.js'; const PARRABLE_URL = 'https://h.parrable.com/prebid'; const PARRABLE_COOKIE_NAME = '_parrable_id'; +const PARRABLE_GVLID = 928; const LEGACY_ID_COOKIE_NAME = '_parrable_eid'; const LEGACY_OPTOUT_COOKIE_NAME = '_parrable_optout'; const ONE_YEAR_MS = 364 * 24 * 60 * 60 * 1000; const EXPIRE_COOKIE_DATE = 'Thu, 01 Jan 1970 00:00:00 GMT'; -const storage = getStorageManager(); +const storage = getStorageManager(PARRABLE_GVLID); function getExpirationDate() { const oneYearFromNow = new Date(utils.timestamp() + ONE_YEAR_MS); @@ -60,7 +62,7 @@ function isValidConfig(configParams) { utils.logError('User ID - parrableId submodule requires configParams'); return false; } - if (!configParams.partner) { + if (!configParams.partners && !configParams.partner) { utils.logError('User ID - parrableId submodule requires partner list'); return false; } @@ -70,6 +72,15 @@ function isValidConfig(configParams) { return true; } +function encodeBase64UrlSafe(base64) { + const ENC = { + '+': '-', + '/': '_', + '=': '.' + }; + return base64.replace(/[+/=]/g, (m) => ENC[m]); +} + function readCookie() { const parrableIdStr = storage.getCookie(PARRABLE_COOKIE_NAME); if (parrableIdStr) { @@ -127,12 +138,18 @@ function shouldFilterImpression(configParams, parrableId) { const offset = (new Date()).getTimezoneOffset() / 60; const zone = Intl.DateTimeFormat().resolvedOptions().timeZone; + function isZoneListed(list, zone) { + // IE does not provide a timeZone in IANA format so zone will be empty + const zoneLowercase = zone && zone.toLowerCase(); + return !!(list && zone && find(list, zn => zn.toLowerCase() === zoneLowercase)); + } + function isAllowed() { if (utils.isEmpty(config.allowedZones) && utils.isEmpty(config.allowedOffsets)) { return true; } - if (utils.contains(config.allowedZones, zone)) { + if (isZoneListed(config.allowedZones, zone)) { return true; } if (utils.contains(config.allowedOffsets, offset)) { @@ -146,7 +163,7 @@ function shouldFilterImpression(configParams, parrableId) { utils.isEmpty(config.blockedOffsets)) { return false; } - if (utils.contains(config.blockedZones, zone)) { + if (isZoneListed(config.blockedZones, zone)) { return true; } if (utils.contains(config.blockedOffsets, offset)) { @@ -155,10 +172,10 @@ function shouldFilterImpression(configParams, parrableId) { return false; } - return !isAllowed() || isBlocked(); + return isBlocked() || !isAllowed(); } -function fetchId(configParams) { +function fetchId(configParams, gdprConsentData) { if (!isValidConfig(configParams)) return; let parrableId = readCookie(); @@ -174,15 +191,24 @@ function fetchId(configParams) { const eid = (parrableId) ? parrableId.eid : null; const refererInfo = getRefererInfo(); const uspString = uspDataHandler.getConsentData(); + const gdprApplies = (gdprConsentData && typeof gdprConsentData.gdprApplies === 'boolean' && gdprConsentData.gdprApplies); + const gdprConsentString = (gdprConsentData && gdprApplies && gdprConsentData.consentString) || ''; + const partners = configParams.partners || configParams.partner + const trackers = typeof partners === 'string' + ? partners.split(',') + : partners; const data = { eid, - trackers: configParams.partner.split(','), - url: refererInfo.referer + trackers, + url: refererInfo.referer, + prebidVersion: '$prebid.version$', + isIframe: utils.inIframe() }; const searchParams = { - data: btoa(JSON.stringify(data)), + data: encodeBase64UrlSafe(btoa(JSON.stringify(data))), + gdpr: gdprApplies ? 1 : 0, _rand: Math.random() }; @@ -190,6 +216,10 @@ function fetchId(configParams) { searchParams.us_privacy = uspString; } + if (gdprApplies) { + searchParams.gdpr_consent = gdprConsentString; + } + const options = { method: 'GET', withCredentials: true @@ -236,7 +266,7 @@ function fetchId(configParams) { callback, id: parrableId }; -}; +} /** @type {Submodule} */ export const parrableIdSubmodule = { @@ -245,6 +275,12 @@ export const parrableIdSubmodule = { * @type {string} */ name: 'parrableId', + /** + * Global Vendor List ID + * @type {number} + */ + gvlid: PARRABLE_GVLID, + /** * decode the stored id value for passing to bid requests * @function @@ -267,7 +303,7 @@ export const parrableIdSubmodule = { */ getId(config, gdprConsentData, currentStoredId) { const configParams = (config && config.params) || {}; - return fetchId(configParams); + return fetchId(configParams, gdprConsentData); } }; diff --git a/modules/permutiveRtdProvider.js b/modules/permutiveRtdProvider.js new file mode 100644 index 00000000000..8ec215d3cca --- /dev/null +++ b/modules/permutiveRtdProvider.js @@ -0,0 +1,171 @@ +/** + * This module adds permutive provider to the real time data module + * The {@link module:modules/realTimeData} module is required + * The module will add custom segment targeting to ad units of specific bidders + * @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 } from '../src/utils.js' +import includes from 'core-js-pure/features/array/includes.js' + +export const storage = getStorageManager() + +function init (config, userConsent) { + return true +} + +/** +* Set segment targeting from cache and then try to wait for Permutive +* to initialise to get realtime segment targeting +*/ +export function initSegments (reqBidsConfigObj, callback, customConfig) { + const permutiveOnPage = isPermutiveOnPage() + const config = mergeDeep({ + waitForIt: false, + params: { + maxSegs: 500, + acBidders: [], + overwrites: {} + } + }, customConfig) + + setSegments(reqBidsConfigObj, config) + + if (config.waitForIt && permutiveOnPage) { + window.permutive.ready(function () { + setSegments(reqBidsConfigObj, config) + callback() + }, 'realtime') + } else { + callback() + } +} + +function setSegments (reqBidsConfigObj, config) { + const adUnits = reqBidsConfigObj.adUnits || getGlobal().adUnits + const data = getSegments(config.params.maxSegs) + const utils = { deepSetValue, deepAccess, isFn, mergeDeep } + + adUnits.forEach(adUnit => { + adUnit.bids.forEach(bid => { + const { bidder } = bid + const acEnabled = isAcEnabled(config, bidder) + const customFn = getCustomBidderFn(config, bidder) + const defaultFn = getDefaultBidderFn(bidder) + + if (customFn) { + customFn(bid, data, acEnabled, utils, defaultFn) + } else if (defaultFn) { + defaultFn(bid, data, acEnabled) + } else { + + } + }) + }) +} + +function getCustomBidderFn (config, bidder) { + const overwriteFn = deepAccess(config, `params.overwrites.${bidder}`) + + if (overwriteFn && isFn(overwriteFn)) { + return overwriteFn + } else { + return null + } +} + +/** +* Returns a function that receives a `bid` object, a `data` object and a `acEnabled` boolean +* and which will set the right segment targeting keys for `bid` based on `data` and `acEnabled` +* @param {string} bidder +* @param {object} data +*/ +function getDefaultBidderFn (bidder) { + const bidderMapper = { + appnexus: function (bid, data, acEnabled) { + if (acEnabled && data.ac && data.ac.length) { + deepSetValue(bid, 'params.keywords.p_standard', data.ac) + } + if (data.appnexus && data.appnexus.length) { + deepSetValue(bid, 'params.keywords.permutive', data.appnexus) + } + + return bid + }, + rubicon: function (bid, data, acEnabled) { + if (acEnabled && data.ac && data.ac.length) { + deepSetValue(bid, 'params.visitor.p_standard', data.ac) + } + if (data.rubicon && data.rubicon.length) { + deepSetValue(bid, 'params.visitor.permutive', data.rubicon) + } + + return bid + }, + ozone: function (bid, data, acEnabled) { + if (acEnabled && data.ac && data.ac.length) { + deepSetValue(bid, 'params.customData.0.targeting.p_standard', data.ac) + } + + return bid + } + } + + return bidderMapper[bidder] +} + +export function isAcEnabled (config, bidder) { + const acBidders = deepAccess(config, 'params.acBidders') || [] + return includes(acBidders, bidder) +} + +export function isPermutiveOnPage () { + return typeof window.permutive !== 'undefined' && typeof window.permutive.ready === 'function' +} + +/** +* Returns all relevant segment IDs in an object +*/ +export function getSegments (maxSegs) { + const legacySegs = readSegments('_psegs').map(Number).filter(seg => seg >= 1000000).map(String) + const _ppam = readSegments('_ppam') + const _pcrprs = readSegments('_pcrprs') + + const segments = { + ac: [..._pcrprs, ..._ppam, ...legacySegs], + rubicon: readSegments('_prubicons'), + appnexus: readSegments('_papns'), + gam: readSegments('_pdfps') + } + + for (const type in segments) { + segments[type] = segments[type].slice(0, maxSegs) + } + + return segments +} + +/** + * Gets an array of segment IDs from LocalStorage + * or returns an empty array + * @param {string} key + */ +function readSegments (key) { + try { + return JSON.parse(storage.getDataFromLocalStorage(key) || '[]') + } catch (e) { + return [] + } +} + +/** @type {RtdSubmodule} */ +export const permutiveSubmodule = { + name: 'permutive', + getBidRequestData: initSegments, + init: init +} + +submodule('realTimeData', permutiveSubmodule) diff --git a/modules/permutiveRtdProvider.md b/modules/permutiveRtdProvider.md new file mode 100644 index 00000000000..55bdf6420cf --- /dev/null +++ b/modules/permutiveRtdProvider.md @@ -0,0 +1,99 @@ +# Permutive Real-time Data Submodule +This submodule reads segments 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=permutiveRtdProvider +``` +You then need to enable the Permutive RTD in your Prebid configuration, using the below format: + +```javascript +pbjs.setConfig({ + ..., + realTimeData: { + auctionDelay: 50, // optional auction delay + dataProviders: [{ + name: 'permutive', + waitForIt: true, // should be true if there's an `auctionDelay` + params: { + acBidders: ['appnexus', 'rubicon', 'ozone'] + } + }] + }, + ... +}) +``` + +## Supported Bidders +The below bidders are currently support by the Permutive RTD module. Please reach out to your Permutive Account Manager to request support for any additional bidders. + +| Bidder | ID | First-party segments | Audience Connector | +| ----------- | ---------- | -------------------- | ------------------ | +| Xandr | `appnexus` | Yes | Yes | +| Magnite | `rubicon` | Yes | Yes | +| Ozone | `ozone` | No | Yes | + +* **First-party segments:** When enabling the respective Activation for a segment in Permutive, this module will automatically attach that segment 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 Permutive. Permutive segments will be sent in the `permutive` key-value. + +* **Audience Connector:** You'll need to define which bidder should receive Audience Connector segments. You need to include the `ID` of any bidder in the `acBidders` array. Audience Connector segments 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 segments. Pleasee see `Supported Bidders` for bidder support and possible values. | `[]` | +| params.maxSegs | Integer | Maximum number of segments to be included in either the `permutive` or `p_standard` key-value. | `500` | +| params.overwrites | Object | See `Custom Bidder Setup` for details on how to define custom bidder functions. | `{}` | + + +## Custom Bidder Setup +You can overwrite the default bidder function, for example to include a different set of segments or to support additional bidders. The below example modifies what first-party segments Magnite receives (segments from `gam` instead of `rubicon`). As best practise we recommend to first call `defaultFn` and then only overwrite specific key-values. The below example only overwrites `permutive` while `p_standard` are still set by `defaultFn` (if `rubicon` is an enabled `acBidder`). + +```javascript +pbjs.setConfig({ + ..., + realTimeData: { + auctionDelay: 50, + dataProviders: [{ + name: 'permutive', + waitForIt: true, + params: { + acBidders: ['appnexus', 'rubicon'], + maxSegs: 450, + overwrites: { + rubicon: function (bid, data, acEnabled, utils, defaultFn) { + if (defaultFn){ + bid = defaultFn(bid, data, acEnabled) + } + if (data.gam && data.gam.length) { + utils.deepSetValue(bid, 'params.visitor.permutive', data.gam) + } + } + } + } + }] + }, + ... +}) +``` +Any custom bidder function will receive the following parameters: + +| Name | Type | Description | +| ------------- |-------------- | --------------------------------------- | +| bid | Object | The bidder specific bidder object. You will mutate this object to set the appropriate targeting keys. | +| data | Object | An object containing Permutive segments | +| data.appnexus | string[] | Segments exposed by the Xandr SSP integration | +| data.rubicon | string[] | Segments exposed by the Magnite SSP integration | +| data.gam | string[] | Segments exposed by the Google Ad Manager integration | +| data.ac | string[] | Segments exposed by the Audience Connector | +| acEnabled | Boolean | `true` if the current bidder in included in `params.acBidders` | +| utils | {} | An object containing references to various util functions used by `permutiveRtdProvider.js`. Please make sure not to overwrite any of these. | +| defaultFn | Function | The default function for this bidder. Please note that this can be `undefined` if there is no default function for this bidder (see `Supported Bidders`). The function expect the following parameters: `bid`, `data`, `acEnabled` and will return `bid`. | + +**Warning** + +The custom bidder function will mutate the `bid` object. Please be aware that this could break your bid request if you accidentally overwrite any fields other than the `permutive` or `p_standard` key-values or if you change the structure of the `bid` object in any way. diff --git a/modules/prebidServerBidAdapter/index.js b/modules/prebidServerBidAdapter/index.js index 7c7962781d2..2d8acb34225 100644 --- a/modules/prebidServerBidAdapter/index.js +++ b/modules/prebidServerBidAdapter/index.js @@ -12,20 +12,28 @@ import includes from 'core-js-pure/features/array/includes.js'; import { S2S_VENDORS } from './config.js'; import { ajax } from '../../src/ajax.js'; import find from 'core-js-pure/features/array/find.js'; +import { getPrebidInternal } from '../../src/utils.js'; const getConfig = config.getConfig; const TYPE = S2S.SRC; -let _synced = false; +let _syncCount = 0; const DEFAULT_S2S_TTL = 60; const DEFAULT_S2S_CURRENCY = 'USD'; const DEFAULT_S2S_NETREVENUE = true; -let _s2sConfig; +let _s2sConfigs; + +let eidPermissions; /** * @typedef {Object} AdapterOptions * @summary s2sConfig parameter that adds arguments to resulting OpenRTB payload that goes to Prebid Server + * @property {string} adapter + * @property {boolean} enabled + * @property {string} endpoint + * @property {string} syncEndpoint + * @property {number} timeout * @example * // example of multiple bidder configuration * pbjs.setConfig({ @@ -40,11 +48,24 @@ let _s2sConfig; /** * @typedef {Object} S2SDefaultConfig - * @property {boolean} enabled - * @property {number} timeout - * @property {number} maxBids - * @property {string} adapter - * @property {AdapterOptions} adapterOptions + * @summary Base config properties for server to server header bidding + * @property {string} [adapter='prebidServer'] adapter code to use for S2S + * @property {boolean} [enabled=false] enables S2S bidding + * @property {number} [timeout=1000] timeout for S2S bidders - should be lower than `pbjs.requestBids({timeout})` + * @property {number} [maxBids=1] + * @property {AdapterOptions} [adapterOptions] adds arguments to resulting OpenRTB payload to Prebid Server + * @property {Object} [syncUrlModifier] + */ + +/** + * @typedef {S2SDefaultConfig} S2SConfig + * @summary Configuration for server to server header bidding + * @property {string[]} bidders bidders to request S2S + * @property {string} endpoint endpoint to contact + * @property {string} [defaultVendor] used as key to select the bidder's default config from ßprebidServer/config.js + * @property {boolean} [cacheMarkup] whether to cache the adm result + * @property {string} [syncEndpoint] endpoint URL for syncing cookies + * @property {Object} [extPrebid] properties will be merged into request.ext.prebid */ /** @@ -64,30 +85,19 @@ config.setDefaults({ }); /** - * Set config for server to server header bidding - * @typedef {Object} options - required - * @property {boolean} enabled enables S2S bidding - * @property {string[]} bidders bidders to request S2S - * @property {string} endpoint endpoint to contact - * === optional params below === - * @property {number} [timeout] timeout for S2S bidders - should be lower than `pbjs.requestBids({timeout})` - * @property {number} [defaultTtl] ttl for S2S bidders when pbs does not return a ttl on the response - defaults to 60` - * @property {boolean} [cacheMarkup] whether to cache the adm result - * @property {string} [adapter] adapter code to use for S2S - * @property {string} [syncEndpoint] endpoint URL for syncing cookies - * @property {Object} [extPrebid] properties will be merged into request.ext.prebid - * @property {AdapterOptions} [adapterOptions] adds arguments to resulting OpenRTB payload to Prebid Server + * @param {S2SConfig} option + * @return {boolean} */ -function setS2sConfig(options) { - if (options.defaultVendor) { - let vendor = options.defaultVendor; - let optionKeys = Object.keys(options); +function updateConfigDefaultVendor(option) { + if (option.defaultVendor) { + let vendor = option.defaultVendor; + let optionKeys = Object.keys(option); if (S2S_VENDORS[vendor]) { // vendor keys will be set if either: the key was not specified by user // or if the user did not set their own distinct value (ie using the system default) to override the vendor Object.keys(S2S_VENDORS[vendor]).forEach((vendorKey) => { - if (s2sDefaultConfig[vendorKey] === options[vendorKey] || !includes(optionKeys, vendorKey)) { - options[vendorKey] = S2S_VENDORS[vendor][vendorKey]; + if (s2sDefaultConfig[vendorKey] === option[vendorKey] || !includes(optionKeys, vendorKey)) { + option[vendorKey] = S2S_VENDORS[vendor][vendorKey]; } }); } else { @@ -95,9 +105,14 @@ function setS2sConfig(options) { return false; } } +} - let keys = Object.keys(options); - +/** + * @param {S2SConfig} option + * @return {boolean} + */ +function validateConfigRequiredProps(option) { + const keys = Object.keys(option); if (['accountId', 'bidders', 'endpoint'].filter(key => { if (!includes(keys, key)) { utils.logError(key + ' missing in server to server config'); @@ -105,10 +120,43 @@ function setS2sConfig(options) { } return false; }).length > 0) { + return false; + } +} + +/** + * @param {(S2SConfig[]|S2SConfig)} options + */ +function setS2sConfig(options) { + if (!options) { return; } + const normalizedOptions = Array.isArray(options) ? options : [options]; + + const activeBidders = []; + const optionsValid = normalizedOptions.every((option, i, array) => { + const updateSuccess = updateConfigDefaultVendor(option); + if (updateSuccess !== false) { + const valid = validateConfigRequiredProps(option); + if (valid !== false) { + if (Array.isArray(option['bidders'])) { + array[i]['bidders'] = option['bidders'].filter(bidder => { + if (activeBidders.indexOf(bidder) === -1) { + activeBidders.push(bidder); + return true; + } + return false; + }); + } + return true; + } + } + return false; + }); - _s2sConfig = options; + if (optionsValid) { + return _s2sConfigs = normalizedOptions; + } } getConfig('s2sConfig', ({s2sConfig}) => setS2sConfig(s2sConfig)); @@ -116,25 +164,25 @@ getConfig('s2sConfig', ({s2sConfig}) => setS2sConfig(s2sConfig)); * resets the _synced variable back to false, primiarily used for testing purposes */ export function resetSyncedStatus() { - _synced = false; + _syncCount = 0; } /** * @param {Array} bidderCodes list of bidders to request user syncs for. */ -function queueSync(bidderCodes, gdprConsent, uspConsent) { - if (_synced) { +function queueSync(bidderCodes, gdprConsent, uspConsent, s2sConfig) { + if (_s2sConfigs.length === _syncCount) { return; } - _synced = true; + _syncCount++; const payload = { uuid: utils.generateUUID(), bidders: bidderCodes, - account: _s2sConfig.accountId + account: s2sConfig.accountId }; - let userSyncLimit = _s2sConfig.userSyncLimit; + let userSyncLimit = s2sConfig.userSyncLimit; if (utils.isNumber(userSyncLimit) && userSyncLimit > 0) { payload['limit'] = userSyncLimit; } @@ -156,11 +204,11 @@ function queueSync(bidderCodes, gdprConsent, uspConsent) { } const jsonPayload = JSON.stringify(payload); - ajax(_s2sConfig.syncEndpoint, + ajax(s2sConfig.syncEndpoint, (response) => { try { response = JSON.parse(response); - doAllSyncs(response.bidder_status); + doAllSyncs(response.bidder_status, s2sConfig); } catch (e) { utils.logError(e); } @@ -172,16 +220,20 @@ function queueSync(bidderCodes, gdprConsent, uspConsent) { }); } -function doAllSyncs(bidders) { +function doAllSyncs(bidders, s2sConfig) { if (bidders.length === 0) { return; } - const thisSync = bidders.pop(); + // pull the syncs off the list in the order that prebid server sends them + const thisSync = bidders.shift(); + + // if PBS reports this bidder doesn't have an ID, then call the sync and recurse to the next sync entry if (thisSync.no_cookie) { - doPreBidderSync(thisSync.usersync.type, thisSync.usersync.url, thisSync.bidder, utils.bind.call(doAllSyncs, null, bidders)); + doPreBidderSync(thisSync.usersync.type, thisSync.usersync.url, thisSync.bidder, utils.bind.call(doAllSyncs, null, bidders, s2sConfig), s2sConfig); } else { - doAllSyncs(bidders); + // bidder already has an ID, so just recurse to the next sync entry + doAllSyncs(bidders, s2sConfig); } } @@ -192,10 +244,11 @@ function doAllSyncs(bidders) { * @param {string} url the url to sync * @param {string} bidder name of bidder doing sync for * @param {function} done an exit callback; to signify this pixel has either: finished rendering or something went wrong + * @param {S2SConfig} s2sConfig */ -function doPreBidderSync(type, url, bidder, done) { - if (_s2sConfig.syncUrlModifier && typeof _s2sConfig.syncUrlModifier[bidder] === 'function') { - const newSyncUrl = _s2sConfig.syncUrlModifier[bidder](type, url, bidder); +function doPreBidderSync(type, url, bidder, done, s2sConfig) { + if (s2sConfig.syncUrlModifier && typeof s2sConfig.syncUrlModifier[bidder] === 'function') { + const newSyncUrl = s2sConfig.syncUrlModifier[bidder](type, url, bidder); doBidderSync(type, newSyncUrl, bidder, done) } else { doBidderSync(type, url, bidder, done) @@ -240,13 +293,13 @@ function doClientSideSyncs(bidders) { }); } -function _appendSiteAppDevice(request, pageUrl) { +function _appendSiteAppDevice(request, pageUrl, accountId) { if (!request) return; // ORTB specifies app OR site if (typeof config.getConfig('app') === 'object') { request.app = config.getConfig('app'); - request.app.publisher = {id: _s2sConfig.accountId} + request.app.publisher = {id: accountId} } else { request.site = {}; if (utils.isPlainObject(config.getConfig('site'))) { @@ -254,7 +307,7 @@ function _appendSiteAppDevice(request, pageUrl) { } // set publisher.id if not already defined if (!utils.deepAccess(request.site, 'publisher.id')) { - utils.deepSetValue(request.site, 'publisher.id', _s2sConfig.accountId); + utils.deepSetValue(request.site, 'publisher.id', accountId); } // set site.page if not already defined if (!request.site.page) { @@ -409,7 +462,7 @@ export function resetWurlMap() { } const OPEN_RTB_PROTOCOL = { - buildRequest(s2sBidRequest, bidRequests, adUnits) { + buildRequest(s2sBidRequest, bidRequests, adUnits, s2sConfig, requestedBidders) { let imps = []; let aliases = {}; const firstBidRequest = bidRequests[0]; @@ -492,7 +545,12 @@ const OPEN_RTB_PROTOCOL = { // check for and store valid aliases to add to the request if (adapterManager.aliasRegistry[bid.bidder]) { - aliases[bid.bidder] = adapterManager.aliasRegistry[bid.bidder]; + const bidder = adapterManager.bidderRegistry[bid.bidder]; + // adding alias only if alias source bidder exists and alias isn't configured to be standalone + // pbs adapter + if (bidder && !bidder.getSpec().skipPbsAliasing) { + aliases[bid.bidder] = adapterManager.aliasRegistry[bid.bidder]; + } } }); @@ -550,11 +608,11 @@ const OPEN_RTB_PROTOCOL = { if (adapter && adapter.getSpec().transformBidParams) { bid.params = adapter.getSpec().transformBidParams(bid.params, true); } - acc[bid.bidder] = (_s2sConfig.adapterOptions && _s2sConfig.adapterOptions[bid.bidder]) ? Object.assign({}, bid.params, _s2sConfig.adapterOptions[bid.bidder]) : bid.params; + acc[bid.bidder] = (s2sConfig.adapterOptions && s2sConfig.adapterOptions[bid.bidder]) ? Object.assign({}, bid.params, s2sConfig.adapterOptions[bid.bidder]) : bid.params; return acc; }, {}); - const imp = { id: adUnit.code, ext, secure: _s2sConfig.secure }; + const imp = { id: adUnit.code, ext, secure: s2sConfig.secure }; /** * Prebid AdSlot @@ -584,6 +642,23 @@ const OPEN_RTB_PROTOCOL = { utils.deepSetValue(imp, 'ext.prebid.storedauctionresponse.id', storedAuctionResponseBid.storedAuctionResponse.toString()); } + const getFloorBid = find(firstBidRequest.bids, bid => bid.adUnitCode === adUnit.code && typeof bid.getFloor === 'function'); + + if (getFloorBid) { + let floorInfo; + try { + floorInfo = getFloorBid.getFloor({ + currency: config.getConfig('currency.adServerCurrency') || DEFAULT_S2S_CURRENCY, + }); + } catch (e) { + utils.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 (imp.banner || imp.video || imp.native) { imps.push(imp); } @@ -596,7 +671,7 @@ const OPEN_RTB_PROTOCOL = { const request = { id: s2sBidRequest.tid, source: {tid: s2sBidRequest.tid}, - tmax: _s2sConfig.timeout, + tmax: s2sConfig.timeout, imp: imps, test: getConfig('debug') ? 1 : 0, ext: { @@ -614,8 +689,8 @@ const OPEN_RTB_PROTOCOL = { }; // s2sConfig video.ext.prebid is passed through openrtb to PBS - if (_s2sConfig.extPrebid && typeof _s2sConfig.extPrebid === 'object') { - request.ext.prebid = Object.assign(request.ext.prebid, _s2sConfig.extPrebid); + if (s2sConfig.extPrebid && typeof s2sConfig.extPrebid === 'object') { + request.ext.prebid = Object.assign(request.ext.prebid, s2sConfig.extPrebid); } /** @@ -630,7 +705,7 @@ const OPEN_RTB_PROTOCOL = { request.cur = [adServerCur[0]]; } - _appendSiteAppDevice(request, firstBidRequest.refererInfo.referer); + _appendSiteAppDevice(request, bidRequests[0].refererInfo.referer, s2sConfig.accountId); // pass schain object if it is present const schain = utils.deepAccess(bidRequests, '0.bids.0.schain'); @@ -649,6 +724,17 @@ const OPEN_RTB_PROTOCOL = { utils.deepSetValue(request, 'user.ext.eids', bidUserIdAsEids); } + if (utils.isArray(eidPermissions) && eidPermissions.length > 0) { + if (requestedBidders && utils.isArray(requestedBidders)) { + eidPermissions.forEach(i => { + if (i.bidders) { + i.bidders = i.bidders.filter(bidder => requestedBidders.includes(bidder)) + } + }); + } + utils.deepSetValue(request, 'ext.prebid.data.eidpermissions', eidPermissions); + } + if (bidRequests) { if (firstBidRequest.gdprConsent) { // note - gdprApplies & consentString may be undefined in certain use-cases for consentManagement module @@ -685,7 +771,7 @@ const OPEN_RTB_PROTOCOL = { return request; }, - interpretResponse(response, bidderRequests) { + interpretResponse(response, bidderRequests, s2sConfig) { const bids = []; [['errors', 'serverErrors'], ['responsetimemillis', 'serverResponseTimeMs']] @@ -750,8 +836,8 @@ const OPEN_RTB_PROTOCOL = { if (utils.deepAccess(bid, 'ext.prebid.type') === VIDEO) { bidObject.mediaType = VIDEO; let sizes = bidRequest.sizes && bidRequest.sizes[0]; - bidObject.playerHeight = sizes[0]; - bidObject.playerWidth = sizes[1]; + bidObject.playerWidth = sizes[0]; + bidObject.playerHeight = sizes[1]; // try to get cache values from 'response.ext.prebid.cache.js' // else try 'bid.ext.prebid.targeting' as fallback @@ -842,8 +928,8 @@ const OPEN_RTB_PROTOCOL = { bidObject.meta = bidObject.meta || {}; if (bid.adomain) { bidObject.meta.advertiserDomains = bid.adomain; } - const configTtl = _s2sConfig.defaultTtl || DEFAULT_S2S_TTL; // the OpenRTB location for "TTL" as understood by Prebid.js is "exp" (expiration). + const configTtl = s2sConfig.defaultTtl || DEFAULT_S2S_TTL; bidObject.ttl = (bid.exp) ? bid.exp : configTtl; bidObject.netRevenue = (bid.netRevenue) ? bid.netRevenue : DEFAULT_S2S_NETREVENUE; @@ -892,37 +978,40 @@ export function PrebidServer() { .reduce(utils.flatten) .filter(utils.uniques); - if (_s2sConfig && _s2sConfig.syncEndpoint) { - let gdprConsent, uspConsent; - if (Array.isArray(bidRequests) && bidRequests.length > 0) { - gdprConsent = bidRequests[0].gdprConsent; - uspConsent = bidRequests[0].uspConsent; - } + if (Array.isArray(_s2sConfigs)) { + if (s2sBidRequest.s2sConfig && s2sBidRequest.s2sConfig.syncEndpoint) { + let gdprConsent, uspConsent; + if (Array.isArray(bidRequests) && bidRequests.length > 0) { + gdprConsent = bidRequests[0].gdprConsent; + uspConsent = bidRequests[0].uspConsent; + } - let syncBidders = _s2sConfig.bidders - .map(bidder => adapterManager.aliasRegistry[bidder] || bidder) - .filter((bidder, index, array) => (array.indexOf(bidder) === index)); + let syncBidders = s2sBidRequest.s2sConfig.bidders + .map(bidder => adapterManager.aliasRegistry[bidder] || bidder) + .filter((bidder, index, array) => (array.indexOf(bidder) === index)); - queueSync(syncBidders, gdprConsent, uspConsent); - } + queueSync(syncBidders, gdprConsent, uspConsent, s2sBidRequest.s2sConfig); + } - const request = OPEN_RTB_PROTOCOL.buildRequest(s2sBidRequest, bidRequests, validAdUnits); - const requestJson = request && JSON.stringify(request); - if (request && requestJson) { - ajax( - _s2sConfig.endpoint, - { - success: response => handleResponse(response, requestedBidders, bidRequests, addBidResponse, done), - error: done - }, - requestJson, - { contentType: 'text/plain', withCredentials: true } - ); + const request = OPEN_RTB_PROTOCOL.buildRequest(s2sBidRequest, bidRequests, validAdUnits, s2sBidRequest.s2sConfig, requestedBidders); + const requestJson = request && JSON.stringify(request); + utils.logInfo('BidRequest: ' + requestJson); + if (request && requestJson) { + ajax( + s2sBidRequest.s2sConfig.endpoint, + { + success: response => handleResponse(response, requestedBidders, bidRequests, addBidResponse, done, s2sBidRequest.s2sConfig), + error: done + }, + requestJson, + { contentType: 'text/plain', withCredentials: true } + ); + } } }; /* Notify Prebid of bid responses so bids can get in the auction */ - function handleResponse(response, requestedBidders, bidderRequests, addBidResponse, done) { + function handleResponse(response, requestedBidders, bidderRequests, addBidResponse, done, s2sConfig) { let result; let bids = []; @@ -932,7 +1021,7 @@ export function PrebidServer() { bids = OPEN_RTB_PROTOCOL.interpretResponse( result, bidderRequests, - requestedBidders + s2sConfig ); bids.forEach(({adUnit, bid}) => { @@ -964,4 +1053,14 @@ export function PrebidServer() { }); } +/** + * Global setter that sets eids permissions for bidders + * This setter is to be used by userId module when included + * @param {array} newEidPermissions + */ +function setEidPermissions(newEidPermissions) { + eidPermissions = newEidPermissions; +} +getPrebidInternal().setEidPermissions = setEidPermissions; + adapterManager.registerBidAdapter(new PrebidServer(), 'prebidServer'); diff --git a/modules/priceFloors.js b/modules/priceFloors.js index fd8a46b172f..3555fedbf3a 100644 --- a/modules/priceFloors.js +++ b/modules/priceFloors.js @@ -141,7 +141,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 = utils.deepAccess(getGlobal(), `bidderSettings.${bidderName}.bidCpmAdjustment`); + const adjustmentFunction = utils.deepAccess(getGlobal(), `bidderSettings.${bidderName}.bidCpmAdjustment`) || utils.deepAccess(getGlobal(), 'bidderSettings.standard.bidCpmAdjustment'); if (adjustmentFunction) { return parseFloat(adjustmentFunction(inputCpm, {...bid, cpm: inputCpm})); } @@ -291,6 +291,8 @@ export function updateAdUnitsForAuction(adUnits, floorData, auctionId) { skipRate: floorData.skipRate, floorMin: floorData.floorMin, modelVersion: utils.deepAccess(floorData, 'data.modelVersion'), + modelWeight: utils.deepAccess(floorData, 'data.modelWeight'), + modelTimestamp: utils.deepAccess(floorData, 'data.modelTimestamp'), location: utils.deepAccess(floorData, 'data.location', 'noData'), floorProvider: floorData.floorProvider, fetchStatus: _floorsConfig.fetchStatus diff --git a/modules/pubCommonIdSystem.js b/modules/pubCommonIdSystem.js index cb0c07cefa8..339029120a8 100644 --- a/modules/pubCommonIdSystem.js +++ b/modules/pubCommonIdSystem.js @@ -136,6 +136,17 @@ function handleResponse(pubcid, callback, config) { } } +/** + * Builds and returns the shared Id URL with attached consent data if applicable + * @param {Object} consentData + * @return {string} + */ +function sharedIdUrl(consentData) { + if (!consentData || typeof consentData.gdprApplies !== 'boolean' || !consentData.gdprApplies) return SHAREDID_URL; + + return `${SHAREDID_URL}?gdpr=1&gdpr_consent=${consentData.consentString}` +} + /** * Wraps pixelCallback in order to call sharedid sync * @param {string} pubcid Pubcommon id value @@ -144,12 +155,12 @@ function handleResponse(pubcid, callback, config) { * @return {function(...[*]=)} */ -function getIdCallback(pubcid, pixelCallback, config) { +function getIdCallback(pubcid, pixelCallback, config, consentData) { return function (callback) { if (typeof pixelCallback === 'function') { pixelCallback(); } - ajax(SHAREDID_URL, handleResponse(pubcid, callback, config), undefined, {method: 'GET', withCredentials: true}); + ajax(sharedIdUrl(consentData), handleResponse(pubcid, callback, config), undefined, {method: 'GET', withCredentials: true}); } } @@ -227,7 +238,7 @@ export const pubCommonIdSubmodule = { } const pixelCallback = this.makeCallback(pixelUrl, newId); - const combinedCallback = enableSharedId ? getIdCallback(newId, pixelCallback, config) : pixelCallback; + const combinedCallback = enableSharedId ? getIdCallback(newId, pixelCallback, config, consentData) : pixelCallback; return {id: newId, callback: combinedCallback}; }, @@ -247,10 +258,11 @@ export const pubCommonIdSubmodule = { * * @function * @param {SubmoduleParams} [config] + * @param {ConsentData|undefined} consentData * @param {Object} storedId existing id * @returns {IdResponse|undefined} */ - extendId: function(config = {}, storedId) { + extendId: function(config = {}, consentData, storedId) { const {params: {extend = false, pixelUrl, enableSharedId = SHAREDID_DEFAULT_STATE} = {}} = config; if (extend) { diff --git a/modules/pubgeniusBidAdapter.js b/modules/pubgeniusBidAdapter.js index 88e85a4fd7a..5c750e66c25 100644 --- a/modules/pubgeniusBidAdapter.js +++ b/modules/pubgeniusBidAdapter.js @@ -253,10 +253,6 @@ function interpretBid(bid) { bidResponse.vastXml = bid.adm; } - if (pbadapter.cacheKey) { - bidResponse.videoCacheKey = pbadapter.cacheKey; - } - bidResponse.mediaType = VIDEO; break; default: // banner by default diff --git a/modules/pubmaticBidAdapter.js b/modules/pubmaticBidAdapter.js index b5514ab0344..07e5b62ac26 100644 --- a/modules/pubmaticBidAdapter.js +++ b/modules/pubmaticBidAdapter.js @@ -204,6 +204,9 @@ function _cleanSlot(slotName) { if (utils.isStr(slotName)) { return slotName.replace(/^\s+/g, '').replace(/\s+$/g, ''); } + if (slotName) { + utils.logWarn(BIDDER_CODE + ': adSlot must be a string. Ignoring adSlot'); + } return ''; } @@ -861,8 +864,8 @@ export const spec = { utils.logError(`${LOG_WARN_PREFIX}: no context specified in bid. Rejecting bid: `, bid); return false; } - if (bid.mediaTypes[VIDEO].context === 'outstream' && !utils.isStr(bid.params.outstreamAU)) { - utils.logError(`${LOG_WARN_PREFIX}: for "outstream" bids outstreamAU is required. Rejecting bid: `, bid); + if (bid.mediaTypes[VIDEO].context === 'outstream' && !utils.isStr(bid.params.outstreamAU) && !bid.hasOwnProperty('renderer') && !bid.mediaTypes[VIDEO].hasOwnProperty('renderer')) { + utils.logError(`${LOG_WARN_PREFIX}: for "outstream" bids either outstreamAU parameter must be provided or ad unit supplied renderer is required. Rejecting bid: `, bid); return false; } } else { diff --git a/modules/pubmaticBidAdapter.md b/modules/pubmaticBidAdapter.md index 0ef89a22cbd..e9d93d79758 100644 --- a/modules/pubmaticBidAdapter.md +++ b/modules/pubmaticBidAdapter.md @@ -24,9 +24,9 @@ var adUnits = [ bids: [{ bidder: 'pubmatic', params: { - publisherId: '156209', // required, must be wrapped in quotes - oustreamAU: 'renderer_test_pubmatic', // required if mediaTypes-> video-> context is 'outstream'. This value can be get by BlueBillyWig Team. - adSlot: 'pubmatic_test2', // optional + publisherId: '156209', // required, must be a string, not an integer or other js type. + oustreamAU: 'renderer_test_pubmatic', // required if mediaTypes-> video-> context is 'outstream' and optional if renderer is defined in adUnits or in mediaType video. This value can be get by BlueBillyWig Team. + adSlot: 'pubmatic_test2', // optional, must be a string, not an integer or other js type. pmzoneid: 'zone1, zone11', // optional lat: '40.712775', // optional lon: '-74.005973', // optional diff --git a/modules/pubwiseBidAdapter.js b/modules/pubwiseBidAdapter.js new file mode 100644 index 00000000000..f450a8bede8 --- /dev/null +++ b/modules/pubwiseBidAdapter.js @@ -0,0 +1,777 @@ +import * as utils from '../src/utils.js'; +import { config } from '../src/config.js'; +import { registerBidder } from '../src/adapters/bidderFactory.js'; +import { BANNER, NATIVE } from '../src/mediaTypes.js'; +const VERSION = '0.1.0'; +const GVLID = 842; +const NET_REVENUE = true; +const UNDEFINED = undefined; +const DEFAULT_CURRENCY = 'USD'; +const AUCTION_TYPE = 1; +const BIDDER_CODE = 'pwbid'; +const ENDPOINT_URL = 'https://bid.pubwise.io/prebid'; +const DEFAULT_WIDTH = 0; +const DEFAULT_HEIGHT = 0; +const PREBID_NATIVE_HELP_LINK = 'https://prebid.org/dev-docs/show-native-ads.html'; +// const USERSYNC_URL = '//127.0.0.1:8080/usersync' + +const CUSTOM_PARAMS = { + 'gender': '', // User gender + 'yob': '', // User year of birth + 'lat': '', // User location - Latitude + 'lon': '', // User Location - Longitude +}; + +// rtb native types are meant to be dynamic and extendable +// the extendable data asset types are nicely aligned +// in practice we set an ID that is distinct for each real type of return +const NATIVE_ASSETS = { + 'TITLE': { ID: 1, KEY: 'title', TYPE: 0 }, + 'IMAGE': { ID: 2, KEY: 'image', TYPE: 0 }, + 'ICON': { ID: 3, KEY: 'icon', TYPE: 0 }, + 'SPONSOREDBY': { ID: 4, KEY: 'sponsoredBy', TYPE: 1 }, + 'BODY': { ID: 5, KEY: 'body', TYPE: 2 }, + 'CLICKURL': { ID: 6, KEY: 'clickUrl', TYPE: 0 }, + 'VIDEO': { ID: 7, KEY: 'video', TYPE: 0 }, + 'EXT': { ID: 8, KEY: 'ext', TYPE: 0 }, + 'DATA': { ID: 9, KEY: 'data', TYPE: 0 }, + 'LOGO': { ID: 10, KEY: 'logo', TYPE: 0 }, + 'SPONSORED': { ID: 11, KEY: 'sponsored', TYPE: 1 }, + 'DESC': { ID: 12, KEY: 'data', TYPE: 2 }, + 'RATING': { ID: 13, KEY: 'rating', TYPE: 3 }, + 'LIKES': { ID: 14, KEY: 'likes', TYPE: 4 }, + 'DOWNLOADS': { ID: 15, KEY: 'downloads', TYPE: 5 }, + 'PRICE': { ID: 16, KEY: 'price', TYPE: 6 }, + 'SALEPRICE': { ID: 17, KEY: 'saleprice', TYPE: 7 }, + 'PHONE': { ID: 18, KEY: 'phone', TYPE: 8 }, + 'ADDRESS': { ID: 19, KEY: 'address', TYPE: 9 }, + 'DESC2': { ID: 20, KEY: 'desc2', TYPE: 10 }, + 'DISPLAYURL': { ID: 21, KEY: 'displayurl', TYPE: 11 }, + 'CTA': { ID: 22, KEY: 'cta', TYPE: 12 } +}; + +const NATIVE_ASSET_IMAGE_TYPE = { + 'ICON': 1, + 'LOGO': 2, + 'IMAGE': 3 +} + +// to render any native unit we have to have a few items +const NATIVE_MINIMUM_REQUIRED_IMAGE_ASSETS = [ + { + id: NATIVE_ASSETS.SPONSOREDBY.ID, + required: true, + data: { + type: 1 + } + }, + { + id: NATIVE_ASSETS.TITLE.ID, + required: true, + }, + { + id: NATIVE_ASSETS.IMAGE.ID, + required: true, + } +] + +let isInvalidNativeRequest = false +let NATIVE_ASSET_ID_TO_KEY_MAP = {}; +let NATIVE_ASSET_KEY_TO_ASSET_MAP = {}; + +// together allows traversal of NATIVE_ASSETS_LIST in any direction +// id -> key +utils._each(NATIVE_ASSETS, anAsset => { NATIVE_ASSET_ID_TO_KEY_MAP[anAsset.ID] = anAsset.KEY }); +// key -> asset +utils._each(NATIVE_ASSETS, anAsset => { NATIVE_ASSET_KEY_TO_ASSET_MAP[anAsset.KEY] = anAsset }); + +export const spec = { + code: BIDDER_CODE, + gvlid: GVLID, + supportedMediaTypes: [BANNER, NATIVE], + /** + * Determines whether or not the given bid request is valid. + * + * @param {BidRequest} bid The bid params to validate. + * @return boolean True if this is a valid bid, and false otherwise. + */ + isBidRequestValid: function (bid) { + // siteId is required + if (bid.params && bid.params.siteId) { + // it must be a string + if (!utils.isStr(bid.params.siteId)) { + _logWarn('siteId is required for bid', bid); + return false; + } + } else { + return false; + } + + return true; + }, + /** + * Make a server request from the list of BidRequests. + * + * @param {validBidRequests[]} - an array of bids + * @return ServerRequest Info describing the request to the server. + */ + buildRequests: function (validBidRequests, bidderRequest) { + var refererInfo; + if (bidderRequest && bidderRequest.refererInfo) { + refererInfo = bidderRequest.refererInfo; + } + var conf = _initConf(refererInfo); + var payload = _createOrtbTemplate(conf); + var bidCurrency = ''; + var bid; + var blockedIabCategories = []; + + validBidRequests.forEach(originalBid => { + bid = utils.deepClone(originalBid); + bid.params.adSlot = bid.params.adSlot || ''; + _parseAdSlot(bid); + + conf = _handleCustomParams(bid.params, conf); + conf.transactionId = bid.transactionId; + bidCurrency = bid.params.currency || UNDEFINED; + bid.params.currency = bidCurrency; + + if (bid.params.hasOwnProperty('bcat') && utils.isArray(bid.params.bcat)) { + blockedIabCategories = blockedIabCategories.concat(bid.params.bcat); + } + + var impObj = _createImpressionObject(bid, conf); + if (impObj) { + payload.imp.push(impObj); + } + }); + + // no payload imps, no rason to continue + if (payload.imp.length == 0) { + return; + } + + // test bids can also be turned on here + if (window.location.href.indexOf('pubwiseTestBid=true') !== -1) { + payload.test = 1; + } + + if (bid.params.isTest) { + payload.test = Number(bid.params.isTest) // should be 1 or 0 + } + payload.site.publisher.id = bid.params.siteId.trim(); + payload.user.gender = (conf.gender ? conf.gender.trim() : UNDEFINED); + payload.user.geo = {}; + payload.user.geo.lat = _parseSlotParam('lat', conf.lat); + payload.user.geo.lon = _parseSlotParam('lon', conf.lon); + payload.user.yob = _parseSlotParam('yob', conf.yob); + payload.device.geo = payload.user.geo; + payload.site.page = payload.site.page.trim(); + payload.site.domain = _getDomainFromURL(payload.site.page); + + // add the content object from config in request + if (typeof config.getConfig('content') === 'object') { + payload.site.content = config.getConfig('content'); + } + + // merge the device from config.getConfig('device') + if (typeof config.getConfig('device') === 'object') { + payload.device = Object.assign(payload.device, config.getConfig('device')); + } + + // passing transactionId in source.tid + utils.deepSetValue(payload, 'source.tid', conf.transactionId); + + // schain + if (validBidRequests[0].schain) { + utils.deepSetValue(payload, 'source.ext.schain', validBidRequests[0].schain); + } + + // gdpr consent + if (bidderRequest && bidderRequest.gdprConsent) { + utils.deepSetValue(payload, 'user.ext.consent', bidderRequest.gdprConsent.consentString); + utils.deepSetValue(payload, 'regs.ext.gdpr', (bidderRequest.gdprConsent.gdprApplies ? 1 : 0)); + } + + // ccpa on the root object + if (bidderRequest && bidderRequest.uspConsent) { + utils.deepSetValue(payload, 'regs.ext.us_privacy', bidderRequest.uspConsent); + } + + // if coppa is in effect then note it + if (config.getConfig('coppa') === true) { + utils.deepSetValue(payload, 'regs.coppa', 1); + } + + var options = {contentType: 'text/plain'} + + _logInfo('buildRequests payload', payload); + _logInfo('buildRequests bidderRequest', bidderRequest); + + return { + method: 'POST', + url: ENDPOINT_URL, + data: payload, + options: options, + bidderRequest: bidderRequest, + }; + }, + /** + * 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 (response, request) { + const bidResponses = []; + var respCur = DEFAULT_CURRENCY; + _logInfo('interpretResponse request', request); + let parsedRequest = request.data; // not currently stringified + // let parsedReferrer = parsedRequest.site && parsedRequest.site.ref ? parsedRequest.site.ref : ''; + + // try { + if (response.body && response.body.seatbid && utils.isArray(response.body.seatbid)) { + // Supporting multiple bid responses for same adSize + respCur = response.body.cur || respCur; + response.body.seatbid.forEach(seatbidder => { + seatbidder.bid && + utils.isArray(seatbidder.bid) && + seatbidder.bid.forEach(bid => { + let newBid = { + requestId: bid.impid, + cpm: (parseFloat(bid.price) || 0).toFixed(2), + width: bid.w, + height: bid.h, + creativeId: bid.crid || bid.id, + currency: respCur, + netRevenue: NET_REVENUE, + ttl: 300, + ad: bid.adm, + pw_seat: seatbidder.seat || null, + pw_dspid: bid.ext && bid.ext.dspid ? bid.ext.dspid : null, + partnerImpId: bid.id || '' // partner impression Id + }; + if (parsedRequest.imp && parsedRequest.imp.length > 0) { + parsedRequest.imp.forEach(req => { + if (bid.impid === req.id) { + _checkMediaType(bid.adm, newBid); + switch (newBid.mediaType) { + case BANNER: + break; + case NATIVE: + _parseNativeResponse(bid, newBid); + break; + } + } + }); + } + + newBid.meta = {}; + if (bid.ext && bid.ext.dspid) { + newBid.meta.networkId = bid.ext.dspid; + } + if (bid.ext && bid.ext.advid) { + newBid.meta.buyerId = bid.ext.advid; + } + if (bid.adomain && bid.adomain.length > 0) { + newBid.meta.advertiserDomains = bid.adomain; + newBid.meta.clickUrl = bid.adomain[0]; + } + + bidResponses.push(newBid); + }); + }); + } + // } catch (error) { + // _logError(error); + // } + return bidResponses; + } +} + +function _checkMediaType(adm, newBid) { + // Create a regex here to check the strings + var admJSON = ''; + if (adm.indexOf('"ver":') >= 0) { + try { + admJSON = JSON.parse(adm.replace(/\\/g, '')); + if (admJSON && admJSON.assets) { + newBid.mediaType = NATIVE; + } + } catch (e) { + _logWarn('Error: Cannot parse native reponse for ad response: ' + adm); + } + } else { + newBid.mediaType = BANNER; + } +} + +function _parseNativeResponse(bid, newBid) { + newBid.native = {}; + if (bid.hasOwnProperty('adm')) { + var adm = ''; + try { + adm = JSON.parse(bid.adm.replace(/\\/g, '')); + } catch (ex) { + _logWarn('Error: Cannot parse native reponse for ad response: ' + newBid.adm); + return; + } + if (adm && adm.assets && adm.assets.length > 0) { + newBid.mediaType = NATIVE; + for (let i = 0, len = adm.assets.length; i < len; i++) { + switch (adm.assets[i].id) { + case NATIVE_ASSETS.TITLE.ID: + newBid.native.title = adm.assets[i].title && adm.assets[i].title.text; + break; + case NATIVE_ASSETS.IMAGE.ID: + newBid.native.image = { + url: adm.assets[i].img && adm.assets[i].img.url, + height: adm.assets[i].img && adm.assets[i].img.h, + width: adm.assets[i].img && adm.assets[i].img.w, + }; + break; + case NATIVE_ASSETS.ICON.ID: + newBid.native.icon = { + url: adm.assets[i].img && adm.assets[i].img.url, + height: adm.assets[i].img && adm.assets[i].img.h, + width: adm.assets[i].img && adm.assets[i].img.w, + }; + break; + case NATIVE_ASSETS.SPONSOREDBY.ID: + case NATIVE_ASSETS.BODY.ID: + case NATIVE_ASSETS.LIKES.ID: + case NATIVE_ASSETS.DOWNLOADS.ID: + case NATIVE_ASSETS.PRICE: + case NATIVE_ASSETS.SALEPRICE.ID: + case NATIVE_ASSETS.PHONE.ID: + case NATIVE_ASSETS.ADDRESS.ID: + case NATIVE_ASSETS.DESC2.ID: + case NATIVE_ASSETS.CTA.ID: + case NATIVE_ASSETS.RATING.ID: + case NATIVE_ASSETS.DISPLAYURL.ID: + newBid.native[NATIVE_ASSET_ID_TO_KEY_MAP[adm.assets[i].id]] = adm.assets[i].data && adm.assets[i].data.value; + break; + } + } + newBid.clickUrl = adm.link && adm.link.url; + newBid.clickTrackers = (adm.link && adm.link.clicktrackers) || []; + newBid.impressionTrackers = adm.imptrackers || []; + newBid.jstracker = adm.jstracker || []; + if (!newBid.width) { + newBid.width = DEFAULT_WIDTH; + } + if (!newBid.height) { + newBid.height = DEFAULT_HEIGHT; + } + } + } +} + +function _getDomainFromURL(url) { + let anchor = document.createElement('a'); + anchor.href = url; + return anchor.hostname; +} + +function _handleCustomParams(params, conf) { + var key, value, entry; + for (key in CUSTOM_PARAMS) { + if (CUSTOM_PARAMS.hasOwnProperty(key)) { + value = params[key]; + if (value) { + entry = CUSTOM_PARAMS[key]; + + if (typeof entry === 'object') { + // will be used in future when we want to + // process a custom param before using + // 'keyname': {f: function() {}} + value = entry.f(value, conf); + } + + if (utils.isStr(value)) { + conf[key] = value; + } else { + _logWarn('Ignoring param : ' + key + ' with value : ' + CUSTOM_PARAMS[key] + ', expects string-value, found ' + typeof value); + } + } + } + } + return conf; +} + +function _createOrtbTemplate(conf) { + return { + id: '' + new Date().getTime(), + at: AUCTION_TYPE, + cur: [DEFAULT_CURRENCY], + imp: [], + site: { + page: conf.pageURL, + ref: conf.refURL, + publisher: {} + }, + device: { + ua: navigator.userAgent, + js: 1, + dnt: (navigator.doNotTrack == 'yes' || navigator.doNotTrack == '1' || navigator.msDoNotTrack == '1') ? 1 : 0, + h: screen.height, + w: screen.width, + language: navigator.language + }, + user: {}, + ext: { + version: VERSION + } + }; +} + +function _createImpressionObject(bid, conf) { + var impObj = {}; + var bannerObj; + var nativeObj = {}; + var mediaTypes = ''; + + impObj = { + id: bid.bidId, + tagid: bid.params.adUnit || undefined, + bidfloor: _parseSlotParam('bidFloor', bid.params.bidFloor), // capitalization dicated by 3.2.4 spec + secure: 1, + bidfloorcur: bid.params.currency ? _parseSlotParam('currency', bid.params.currency) : DEFAULT_CURRENCY // capitalization dicated by 3.2.4 spec + }; + + if (bid.hasOwnProperty('mediaTypes')) { + for (mediaTypes in bid.mediaTypes) { + switch (mediaTypes) { + case BANNER: + bannerObj = _createBannerRequest(bid); + if (bannerObj !== UNDEFINED) { + impObj.banner = bannerObj; + } + break; + case NATIVE: + nativeObj['request'] = JSON.stringify(_createNativeRequest(bid.nativeParams)); + if (!isInvalidNativeRequest) { + impObj.native = nativeObj; + } else { + _logWarn('Error: Error in Native adunit ' + bid.params.adUnit + '. Ignoring the adunit. Refer to ' + PREBID_NATIVE_HELP_LINK + ' for more details.'); + } + break; + } + } + } else { + _logWarn('MediaTypes are Required for all Adunit Configs', bid) + } + + _addFloorFromFloorModule(impObj, bid); + + return impObj.hasOwnProperty(BANNER) || + impObj.hasOwnProperty(NATIVE) ? impObj : UNDEFINED; +} + +function _parseSlotParam(paramName, paramValue) { + if (!utils.isStr(paramValue)) { + paramValue && _logWarn('Ignoring param key: ' + paramName + ', expects string-value, found ' + typeof paramValue); + return UNDEFINED; + } + + switch (paramName) { + case 'bidFloor': + return parseFloat(paramValue) || UNDEFINED; + case 'lat': + return parseFloat(paramValue) || UNDEFINED; + case 'lon': + return parseFloat(paramValue) || UNDEFINED; + case 'yob': + return parseInt(paramValue) || UNDEFINED; + default: + return paramValue; + } +} + +function _parseAdSlot(bid) { + _logInfo('parseAdSlot bid', bid) + bid.params.adUnit = ''; + bid.params.width = 0; + bid.params.height = 0; + bid.params.adSlot = _cleanSlotName(bid.params.adSlot); + + if (bid.hasOwnProperty('mediaTypes')) { + if (bid.mediaTypes.hasOwnProperty(BANNER) && + bid.mediaTypes.banner.hasOwnProperty('sizes')) { // if its a banner, has mediaTypes and sizes + var i = 0; + var sizeArray = []; + for (;i < bid.mediaTypes.banner.sizes.length; i++) { + if (bid.mediaTypes.banner.sizes[i].length === 2) { // sizes[i].length will not be 2 in case where size is set as fluid, we want to skip that entry + sizeArray.push(bid.mediaTypes.banner.sizes[i]); + } + } + bid.mediaTypes.banner.sizes = sizeArray; + if (bid.mediaTypes.banner.sizes.length >= 1) { + // if there is more than one size then pop one onto the banner params width + // pop the first into the params, then remove it from mediaTypes + bid.params.width = bid.mediaTypes.banner.sizes[0][0]; + bid.params.height = bid.mediaTypes.banner.sizes[0][1]; + bid.mediaTypes.banner.sizes = bid.mediaTypes.banner.sizes.splice(1, bid.mediaTypes.banner.sizes.length - 1); + } + } + } else { + _logWarn('MediaTypes are Required for all Adunit Configs', bid) + } +} + +function _cleanSlotName(slotName) { + if (utils.isStr(slotName)) { + return slotName.replace(/^\s+/g, '').replace(/\s+$/g, ''); + } + return ''; +} + +function _initConf(refererInfo) { + return { + pageURL: (refererInfo && refererInfo.referer) ? refererInfo.referer : window.location.href, + refURL: window.document.referrer + }; +} + +function _commonNativeRequestObject(nativeAsset, params) { + var key = nativeAsset.KEY; + return { + id: nativeAsset.ID, + required: params[key].required ? 1 : 0, + data: { + type: nativeAsset.TYPE, + len: params[key].len, + ext: params[key].ext + } + }; +} + +function _addFloorFromFloorModule(impObj, bid) { + let bidFloor = -1; // indicates no floor + + // get lowest floor from floorModule + if (typeof bid.getFloor === 'function' && !config.getConfig('pubwise.disableFloors')) { + [BANNER, NATIVE].forEach(mediaType => { + if (impObj.hasOwnProperty(mediaType)) { + let floorInfo = bid.getFloor({ currency: impObj.bidFloorCur, mediaType: mediaType, size: '*' }); + if (typeof floorInfo === 'object' && floorInfo.currency === impObj.bidFloorCur && !isNaN(parseInt(floorInfo.floor))) { + let mediaTypeFloor = parseFloat(floorInfo.floor); + bidFloor = (bidFloor == -1 ? mediaTypeFloor : Math.min(mediaTypeFloor, bidFloor)) + } + } + }); + } + + // get highest, if none then take the default -1 + if (impObj.bidfloor) { + bidFloor = Math.max(bidFloor, impObj.bidfloor) + } + + // assign if it has a valid floor - > 0 + impObj.bidfloor = ((!isNaN(bidFloor) && bidFloor > 0) ? bidFloor : UNDEFINED); +} + +function _createNativeRequest(params) { + var nativeRequestObject = { + assets: [] + }; + for (var key in params) { + if (params.hasOwnProperty(key)) { + var assetObj = {}; + if (!(nativeRequestObject.assets && nativeRequestObject.assets.length > 0 && nativeRequestObject.assets.hasOwnProperty(key))) { + switch (key) { + case NATIVE_ASSETS.TITLE.KEY: + if (params[key].len || params[key].length) { + assetObj = { + id: NATIVE_ASSETS.TITLE.ID, + required: params[key].required ? 1 : 0, + title: { + len: params[key].len || params[key].length, + ext: params[key].ext + } + }; + } else { + _logWarn('Error: Title Length is required for native ad: ' + JSON.stringify(params)); + } + break; + case NATIVE_ASSETS.IMAGE.KEY: + if (params[key].sizes && params[key].sizes.length > 0) { + assetObj = { + id: NATIVE_ASSETS.IMAGE.ID, + required: params[key].required ? 1 : 0, + img: { + type: NATIVE_ASSET_IMAGE_TYPE.IMAGE, + w: params[key].w || params[key].width || (params[key].sizes ? params[key].sizes[0] : UNDEFINED), + h: params[key].h || params[key].height || (params[key].sizes ? params[key].sizes[1] : UNDEFINED), + wmin: params[key].wmin || params[key].minimumWidth || (params[key].minsizes ? params[key].minsizes[0] : UNDEFINED), + hmin: params[key].hmin || params[key].minimumHeight || (params[key].minsizes ? params[key].minsizes[1] : UNDEFINED), + mimes: params[key].mimes, + ext: params[key].ext, + } + }; + } else { + _logWarn('Error: Image sizes is required for native ad: ' + JSON.stringify(params)); + } + break; + case NATIVE_ASSETS.ICON.KEY: + if (params[key].sizes && params[key].sizes.length > 0) { + assetObj = { + id: NATIVE_ASSETS.ICON.ID, + required: params[key].required ? 1 : 0, + img: { + type: NATIVE_ASSET_IMAGE_TYPE.ICON, + w: params[key].w || params[key].width || (params[key].sizes ? params[key].sizes[0] : UNDEFINED), + h: params[key].h || params[key].height || (params[key].sizes ? params[key].sizes[1] : UNDEFINED), + } + }; + } else { + _logWarn('Error: Icon sizes is required for native ad: ' + JSON.stringify(params)); + }; + break; + case NATIVE_ASSETS.VIDEO.KEY: + assetObj = { + id: NATIVE_ASSETS.VIDEO.ID, + required: params[key].required ? 1 : 0, + video: { + minduration: params[key].minduration, + maxduration: params[key].maxduration, + protocols: params[key].protocols, + mimes: params[key].mimes, + ext: params[key].ext + } + }; + break; + case NATIVE_ASSETS.EXT.KEY: + assetObj = { + id: NATIVE_ASSETS.EXT.ID, + required: params[key].required ? 1 : 0, + }; + break; + case NATIVE_ASSETS.LOGO.KEY: + assetObj = { + id: NATIVE_ASSETS.LOGO.ID, + required: params[key].required ? 1 : 0, + img: { + type: NATIVE_ASSET_IMAGE_TYPE.LOGO, + w: params[key].w || params[key].width || (params[key].sizes ? params[key].sizes[0] : UNDEFINED), + h: params[key].h || params[key].height || (params[key].sizes ? params[key].sizes[1] : UNDEFINED) + } + }; + break; + case NATIVE_ASSETS.SPONSOREDBY.KEY: + case NATIVE_ASSETS.BODY.KEY: + case NATIVE_ASSETS.RATING.KEY: + case NATIVE_ASSETS.LIKES.KEY: + case NATIVE_ASSETS.DOWNLOADS.KEY: + case NATIVE_ASSETS.PRICE.KEY: + case NATIVE_ASSETS.SALEPRICE.KEY: + case NATIVE_ASSETS.PHONE.KEY: + case NATIVE_ASSETS.ADDRESS.KEY: + case NATIVE_ASSETS.DESC2.KEY: + case NATIVE_ASSETS.DISPLAYURL.KEY: + case NATIVE_ASSETS.CTA.KEY: + assetObj = _commonNativeRequestObject(NATIVE_ASSET_KEY_TO_ASSET_MAP[key], params); + break; + } + } + } + if (assetObj && assetObj.id) { + nativeRequestObject.assets[nativeRequestObject.assets.length] = assetObj; + } + }; + + // for native image adtype prebid has to have few required assests i.e. title,sponsoredBy, image + // if any of these are missing from the request then request will not be sent + var requiredAssetCount = NATIVE_MINIMUM_REQUIRED_IMAGE_ASSETS.length; + var presentrequiredAssetCount = 0; + NATIVE_MINIMUM_REQUIRED_IMAGE_ASSETS.forEach(ele => { + var lengthOfExistingAssets = nativeRequestObject.assets.length; + for (var i = 0; i < lengthOfExistingAssets; i++) { + if (ele.id == nativeRequestObject.assets[i].id) { + presentrequiredAssetCount++; + break; + } + } + }); + if (requiredAssetCount == presentrequiredAssetCount) { + isInvalidNativeRequest = false; + } else { + isInvalidNativeRequest = true; + } + return nativeRequestObject; +} + +function _createBannerRequest(bid) { + var sizes = bid.mediaTypes.banner.sizes; + var format = []; + var bannerObj; + if (sizes !== UNDEFINED && utils.isArray(sizes)) { + bannerObj = {}; + if (!bid.params.width && !bid.params.height) { + if (sizes.length === 0) { + // i.e. since bid.params does not have width or height, and length of sizes is 0, need to ignore this banner imp + bannerObj = UNDEFINED; + _logWarn('Error: mediaTypes.banner.size missing for adunit: ' + bid.params.adUnit + '. Ignoring the banner impression in the adunit.'); + return bannerObj; + } else { + bannerObj.w = parseInt(sizes[0][0], 10); + bannerObj.h = parseInt(sizes[0][1], 10); + sizes = sizes.splice(1, sizes.length - 1); + } + } else { + bannerObj.w = bid.params.width; + bannerObj.h = bid.params.height; + } + if (sizes.length > 0) { + format = []; + sizes.forEach(function (size) { + if (size.length > 1) { + format.push({ w: size[0], h: size[1] }); + } + }); + if (format.length > 0) { + bannerObj.format = format; + } + } + bannerObj.pos = 0; + bannerObj.topframe = utils.inIframe() ? 0 : 1; + } else { + _logWarn('Error: mediaTypes.banner.size missing for adunit: ' + bid.params.adUnit + '. Ignoring the banner impression in the adunit.'); + bannerObj = UNDEFINED; + } + return bannerObj; +} + +// various error levels are not always used +// eslint-disable-next-line no-unused-vars +function _logMessage(textValue, objectValue) { + utils.logMessage('PubWise: ' + textValue, objectValue); +} + +// eslint-disable-next-line no-unused-vars +function _logInfo(textValue, objectValue) { + utils.logInfo('PubWise: ' + textValue, objectValue); +} + +// eslint-disable-next-line no-unused-vars +function _logWarn(textValue, objectValue) { + utils.logWarn('PubWise: ' + textValue, objectValue); +} + +// eslint-disable-next-line no-unused-vars +function _logError(textValue, objectValue) { + utils.logError('PubWise: ' + textValue, objectValue); +} + +// function _decorateLog() { +// arguments[0] = 'PubWise' + arguments[0]; +// return arguments +// } + +// these are exported only for testing so maintaining the JS convention of _ to indicate the intent +export { + _checkMediaType, + _parseAdSlot +} + +registerBidder(spec); diff --git a/modules/pubwiseBidAdapter.md b/modules/pubwiseBidAdapter.md new file mode 100644 index 00000000000..8cf38a63913 --- /dev/null +++ b/modules/pubwiseBidAdapter.md @@ -0,0 +1,78 @@ +# Overview + +``` +Module Name: PubWise Bid Adapter +Module Type: Bidder Adapter +Maintainer: info@pubwise.io +``` + +# Description + +Connects to PubWise exchange for bids. + +# Sample Banner Ad Unit: For Publishers + +With isTest parameter the system will respond in whatever dimensions provided. + +## Params + + + +## Banner +``` +var adUnits = [ + { + code: "div-gpt-ad-1460505748561-0", + mediaTypes: { + banner: { + sizes: [[300, 250]] + } + }, + bids: [{ + bidder: 'pubwise', + params: { + siteId: "xxxxxx", + isTest: true + } + }] + } +] +``` +## Native +``` +var adUnits = [ + { + code: 'div-gpt-ad-1460505748561-1', + sizes: [[1, 1]], + mediaTypes: { + native: { + title: { + required: true, + len: 80 + }, + body: { + required: true + }, + image: { + required: true, + sizes: [150, 50] + }, + sponsoredBy: { + required: true + }, + icon: { + required: false + } + } + }, + bids: [{ + bidder: 'pubwise', + params: { + siteId: "xxxxxx", + isTest: true, + }, + }] + } +] +``` + diff --git a/modules/pubxBidAdapter.js b/modules/pubxBidAdapter.js index 44c95e8e19a..35d75e96f95 100644 --- a/modules/pubxBidAdapter.js +++ b/modules/pubxBidAdapter.js @@ -1,6 +1,7 @@ import { registerBidder } from '../src/adapters/bidderFactory.js'; const BIDDER_CODE = 'pubx'; const BID_ENDPOINT = 'https://api.primecaster.net/adlogue/api/slot/bid'; +const USER_SYNC_URL = 'https://api.primecaster.net/primecaster_dmppv.html' export const spec = { code: BIDDER_CODE, isBidRequestValid: function(bid) { @@ -42,6 +43,52 @@ export const spec = { bidResponses.push(bidResponse); } else {}; return bidResponses; + }, + /** + * Determine which user syncs should occur + * @param {object} syncOptions + * @param {array} serverResponses + * @returns {array} User sync pixels + */ + getUserSyncs: function (syncOptions, serverResponses) { + const kwTag = document.getElementsByName('keywords'); + let kwString = ''; + let kwEnc = ''; + let titleContent = !!document.title && document.title; + let titleEnc = ''; + let descContent = !!document.getElementsByName('description') && !!document.getElementsByName('description')[0] && document.getElementsByName('description')[0].content; + let descEnc = ''; + const pageUrl = location.href.replace(/\?.*$/, ''); + const pageEnc = encodeURIComponent(pageUrl); + const refUrl = document.referrer.replace(/\?.*$/, ''); + const refEnc = encodeURIComponent(refUrl); + if (kwTag.length) { + const kwContents = kwTag[0].content; + if (kwContents.length > 20) { + const kwArray = kwContents.substr(0, 20).split(','); + kwArray.pop(); + kwString = kwArray.join(); + } else { + kwString = kwContents; + } + kwEnc = encodeURIComponent(kwString) + } else { } + if (titleContent) { + if (titleContent.length > 30) { + titleContent = titleContent.substr(0, 30); + } else {}; + titleEnc = encodeURIComponent(titleContent); + } else { }; + if (descContent) { + if (descContent.length > 60) { + descContent = descContent.substr(0, 60); + } else {}; + descEnc = encodeURIComponent(descContent); + } else { }; + return (syncOptions.iframeEnabled) ? [{ + type: 'iframe', + url: USER_SYNC_URL + '?pkw=' + kwEnc + '&pd=' + descEnc + '&pu=' + pageEnc + '&pref=' + refEnc + '&pt=' + titleEnc + }] : []; } } registerBidder(spec); diff --git a/modules/pubxaiAnalyticsAdapter.js b/modules/pubxaiAnalyticsAdapter.js index 7e2f5061621..894752d607b 100644 --- a/modules/pubxaiAnalyticsAdapter.js +++ b/modules/pubxaiAnalyticsAdapter.js @@ -27,9 +27,13 @@ var pubxaiAnalyticsAdapter = Object.assign(adapter( if (eventType === CONSTANTS.EVENTS.BID_TIMEOUT) { args.forEach(item => { mapBidResponse(item, 'timeout'); }); } else if (eventType === CONSTANTS.EVENTS.AUCTION_INIT) { - events.auctionInit = args; + events.floorDetail = {}; + if (typeof args.bidderRequests[0].bids[0] !== 'undefined' && typeof args.bidderRequests[0].bids[0].floorData !== 'undefined') { + Object.assign(events.floorDetail, args.bidderRequests[0].bids[0].floorData); + } auctionTimestamp = args.timestamp; } else if (eventType === CONSTANTS.EVENTS.BID_REQUESTED) { + events.bids = []; mapBidRequests(args).forEach(item => { events.bids.push(item) }); } else if (eventType === CONSTANTS.EVENTS.BID_RESPONSE) { mapBidResponse(args, 'response'); @@ -56,6 +60,8 @@ function mapBidRequests(params) { adUnitCode: bid.adUnitCode, requestId: bid.bidderRequestId, auctionId: bid.auctionId, + placementId: bid.params.placementId, + floorData: bid.floorData, transactionId: bid.transactionId, sizes: utils.parseSizesInput(bid.mediaTypes.banner.sizes).toString(), renderStatus: 1, @@ -134,8 +140,9 @@ pubxaiAnalyticsAdapter.shouldFireEventRequest = function (samplingRate = 1) { function send(data, status) { if (pubxaiAnalyticsAdapter.shouldFireEventRequest(initOptions.samplingRate)) { let location = utils.getWindowLocation(); - if (typeof data !== 'undefined' && typeof data.auctionInit !== 'undefined') { - Object.assign(data.auctionInit, { host: location.host, path: location.pathname, search: location.search }); + if (typeof data !== 'undefined') { + data.pageDetail = {}; + Object.assign(data.pageDetail, { host: location.host, path: location.pathname, search: location.search }); } data.initOptions = initOptions; diff --git a/modules/qwarryBidAdapter.js b/modules/qwarryBidAdapter.js index 7c2ec0f085b..7cb83520979 100644 --- a/modules/qwarryBidAdapter.js +++ b/modules/qwarryBidAdapter.js @@ -4,7 +4,7 @@ import { ajax } from '../src/ajax.js'; import { VIDEO } from '../src/mediaTypes.js'; const BIDDER_CODE = 'qwarry'; -export const ENDPOINT = 'https://ui-bidder.kantics.co/bid/adtag?prebid=true' +export const ENDPOINT = 'https://bidder.qwarry.co/bid/adtag?prebid=true' export const spec = { code: BIDDER_CODE, diff --git a/modules/readpeakBidAdapter.js b/modules/readpeakBidAdapter.js index c72bbdd658f..2f4173f240b 100644 --- a/modules/readpeakBidAdapter.js +++ b/modules/readpeakBidAdapter.js @@ -98,7 +98,8 @@ function impression(slot) { id: slot.bidId, native: nativeImpression(slot), bidfloor: slot.params.bidfloor || 0, - bidfloorcur: slot.params.bidfloorcur || 'USD' + bidfloorcur: slot.params.bidfloorcur || 'USD', + tagId: slot.params.tagId || '0' }; } diff --git a/modules/readpeakBidAdapter.md b/modules/readpeakBidAdapter.md index a15767f29a7..da250e7f77a 100644 --- a/modules/readpeakBidAdapter.md +++ b/modules/readpeakBidAdapter.md @@ -4,7 +4,7 @@ Module Name: ReadPeak Bid Adapter Module Type: Bidder Adapter -Maintainer: kurre.stahlberg@readpeak.com +Maintainer: devteam@readpeak.com # Description @@ -23,7 +23,8 @@ Please reach out to your account team or hello@readpeak.com for more information params: { bidfloor: 5.00, publisherId: 'test', - siteId: 'test' + siteId: 'test', + tagId: 'test-tag-1' }, }] }]; diff --git a/modules/reconciliationRtdProvider.js b/modules/reconciliationRtdProvider.js index bd3d8861c5b..f8636862d4c 100644 --- a/modules/reconciliationRtdProvider.js +++ b/modules/reconciliationRtdProvider.js @@ -65,7 +65,7 @@ function handleAdMessage(e) { adDeliveryId = adSlot.getTargeting('RSDK_ADID'); adDeliveryId = adDeliveryId.length ? adDeliveryId[0] - : utils.generateUUID(); + : `${utils.timestamp()}-${utils.generateUUID()}`; } } } @@ -246,7 +246,7 @@ function getReconciliationData(adUnitsCodes) { const adSlot = getSlotByCode(adUnitCode); const adUnitId = adSlot ? adSlot.getAdUnitPath() : adUnitCode; - const adDeliveryId = utils.generateUUID(); + const adDeliveryId = `${utils.timestamp()}-${utils.generateUUID()}`; dataToReturn[adUnitCode] = { RSDK_AUID: adUnitId, diff --git a/modules/relevantAnalyticsAdapter.js b/modules/relevantAnalyticsAdapter.js new file mode 100644 index 00000000000..5917262c810 --- /dev/null +++ b/modules/relevantAnalyticsAdapter.js @@ -0,0 +1,33 @@ +import adapter from '../src/AnalyticsAdapter.js'; +import adapterManager from '../src/adapterManager.js'; + +const relevantAnalytics = adapter({ analyticsType: 'bundle', handler: 'on' }); + +const { enableAnalytics: orgEnableAnalytics } = relevantAnalytics; + +Object.assign(relevantAnalytics, { + /** + * Save event in the global array that will be consumed later by the Relevant Yield library + */ + track: ({ eventType: ev, args }) => { + window.relevantDigital.pbEventLog.push({ ev, args, ts: new Date() }); + }, + + /** + * Before forwarding the call to the original enableAnalytics function - + * create (if needed) the global array that is used to pass events to the Relevant Yield library + * by the 'track' function above. + */ + enableAnalytics: function(...args) { + window.relevantDigital = window.relevantDigital || {}; + window.relevantDigital.pbEventLog = window.relevantDigital.pbEventLog || []; + return orgEnableAnalytics.call(this, ...args); + }, +}); + +adapterManager.registerAnalyticsAdapter({ + adapter: relevantAnalytics, + code: 'relevant', +}); + +export default relevantAnalytics; diff --git a/modules/relevantAnalyticsAdapter.md b/modules/relevantAnalyticsAdapter.md new file mode 100644 index 00000000000..e6383fa77e1 --- /dev/null +++ b/modules/relevantAnalyticsAdapter.md @@ -0,0 +1,13 @@ +# Overview + +Module Name: Relevant Yield Analytics Adapter + +Module Type: Analytics Adapter + +Maintainer: [support@relevant-digital.com](mailto:support@relevant-digital.com) + +# Description + +Analytics adapter to be used with [Relevant Yield](https://www.relevant-digital.com/relevantyield) + +Contact [sales@relevant-digital.com](mailto:sales@relevant-digital.com) for information. diff --git a/modules/richaudienceBidAdapter.js b/modules/richaudienceBidAdapter.js index a6b4202fc91..07de3e40594 100755 --- a/modules/richaudienceBidAdapter.js +++ b/modules/richaudienceBidAdapter.js @@ -49,7 +49,10 @@ export const spec = { timeout: config.getConfig('bidderTimeout'), user: raiSetEids(bid), demand: raiGetDemandType(bid), - videoData: raiGetVideoInfo(bid) + videoData: raiGetVideoInfo(bid), + scr_rsl: raiGetResolution(), + cpuc: (typeof window.navigator != 'undefined' ? window.navigator.hardwareConcurrency : null), + kws: (!utils.isEmpty(bid.params.keywords) ? bid.params.keywords : null) }; REFERER = (typeof bidderRequest.refererInfo.referer != 'undefined' ? encodeURIComponent(bidderRequest.refererInfo.referer) : null) @@ -242,3 +245,11 @@ function renderAd(bid) { window.raParams(raPlayerHB, raOutstreamHBPassback, true); } + +function raiGetResolution() { + let resolution = ''; + if (typeof window.screen != 'undefined') { + resolution = window.screen.width + 'x' + window.screen.height; + } + return resolution; +} diff --git a/modules/richaudienceBidAdapter.md b/modules/richaudienceBidAdapter.md index 932cdb8f8de..fbf59a0208a 100644 --- a/modules/richaudienceBidAdapter.md +++ b/modules/richaudienceBidAdapter.md @@ -39,6 +39,7 @@ Please reach out to your account manager for more information. "pid":"ADb1f40rmo", "supplyType":"site", "bidfloor":0.40, + "keywords": "bici=scott;coche=audi;coche=mercedes;" } }] } diff --git a/modules/riseBidAdapter.js b/modules/riseBidAdapter.js new file mode 100644 index 00000000000..a891e0aa883 --- /dev/null +++ b/modules/riseBidAdapter.js @@ -0,0 +1,250 @@ +import {registerBidder} from '../src/adapters/bidderFactory.js'; +import * as utils from '../src/utils.js'; +import {VIDEO} from '../src/mediaTypes.js'; +import {config} from '../src/config.js'; + +const SUPPORTED_AD_TYPES = [VIDEO]; +const BIDDER_CODE = 'rise'; +const BIDDER_VERSION = '4.0.0'; +const TTL = 360; +const SELLER_ENDPOINT = 'https://hb.yellowblue.io/'; +const MODES = { + PRODUCTION: 'hb', + TEST: 'hb-test' +} +const SUPPORTED_SYNC_METHODS = { + IFRAME: 'iframe', + PIXEL: 'pixel' +} + +export const spec = { + code: BIDDER_CODE, + version: BIDDER_VERSION, + supportedMediaTypes: SUPPORTED_AD_TYPES, + isBidRequestValid: function(bidRequest) { + return !!(bidRequest.params.org); + }, + buildRequests: function (bidRequests, bidderRequest) { + if (bidRequests.length === 0) { + return []; + } + + const requests = []; + + bidRequests.forEach(bid => { + requests.push(buildVideoRequest(bid, bidderRequest)); + }); + + return requests; + }, + interpretResponse: function({body}) { + const bidResponses = []; + + const bidResponse = { + requestId: body.requestId, + cpm: body.cpm, + width: body.width, + height: body.height, + creativeId: body.requestId, + currency: body.currency, + netRevenue: body.netRevenue, + ttl: body.ttl || TTL, + vastXml: body.vastXml, + mediaType: VIDEO + }; + + bidResponses.push(bidResponse); + + return bidResponses; + }, + getUserSyncs: function(syncOptions, serverResponses) { + const syncs = []; + for (const response of serverResponses) { + if (syncOptions.iframeEnabled && response.body.userSyncURL) { + syncs.push({ + type: 'iframe', + url: response.body.userSyncURL + }); + } + if (syncOptions.pixelEnabled && utils.isArray(response.body.userSyncPixels)) { + const pixels = response.body.userSyncPixels.map(pixel => { + return { + type: 'image', + url: pixel + } + }) + syncs.push(...pixels) + } + } + return syncs; + } +}; + +registerBidder(spec); + +/** + * Build the video request + * @param bid {bid} + * @param bidderRequest {bidderRequest} + * @returns {Object} + */ +function buildVideoRequest(bid, bidderRequest) { + const sellerParams = generateParameters(bid, bidderRequest); + const {params} = bid; + return { + method: 'GET', + url: getEndpoint(params.testMode), + data: sellerParams + }; +} + +/** + * Get the the ad size from the bid + * @param bid {bid} + * @returns {Array} + */ +function getSizes(bid) { + if (utils.deepAccess(bid, 'mediaTypes.video.sizes')) { + return bid.mediaTypes.video.sizes[0]; + } else if (Array.isArray(bid.sizes) && bid.sizes.length > 0) { + return bid.sizes[0]; + } + return []; +} + +/** + * Get schain string value + * @param schainObject {Object} + * @returns {string} + */ +function getSupplyChain(schainObject) { + if (utils.isEmpty(schainObject)) { + return ''; + } + let scStr = `${schainObject.ver},${schainObject.complete}`; + schainObject.nodes.forEach((node) => { + scStr += '!'; + scStr += `${getEncodedValIfNotEmpty(node.asi)},`; + scStr += `${getEncodedValIfNotEmpty(node.sid)},`; + scStr += `${getEncodedValIfNotEmpty(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 !utils.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 = utils.isArray(syncRule.bidders) ? syncRule.bidders : [bidderCode]; + return isInclude && utils.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; +} + +/** + * Generate query parameters for the request + * @param bid {bid} + * @param bidderRequest {bidderRequest} + * @returns {Object} + */ +function generateParameters(bid, bidderRequest) { + const timeout = config.getConfig('bidderTimeout'); + const { syncEnabled, filterSettings } = config.getConfig('userSync') || {}; + const [ width, height ] = getSizes(bid); + const { params } = bid; + const { bidderCode } = bidderRequest; + const domain = window.location.hostname; + + const requestParams = { + auction_start: utils.timestamp(), + ad_unit_code: utils.getBidIdParameter('adUnitCode', bid), + tmax: timeout, + width: width, + height: height, + publisher_id: params.org, + floor_price: params.floorPrice, + ua: navigator.userAgent, + bid_id: utils.getBidIdParameter('bidId', bid), + bidder_request_id: utils.getBidIdParameter('bidderRequestId', bid), + transaction_id: utils.getBidIdParameter('transactionId', bid), + session_id: utils.getBidIdParameter('auctionId', bid), + publisher_name: domain, + site_domain: domain, + bidder_version: BIDDER_VERSION + }; + + if (syncEnabled) { + const allowedSyncMethod = getAllowedSyncMethod(filterSettings, bidderCode); + if (allowedSyncMethod) { + requestParams.cs_method = allowedSyncMethod; + } + } + + if (bidderRequest.uspConsent) { + requestParams.us_privacy = bidderRequest.uspConsent; + } + + if (bidderRequest && bidderRequest.gdprConsent && bidderRequest.gdprConsent.gdprApplies) { + requestParams.gdpr = bidderRequest.gdprConsent.gdprApplies; + requestParams.gdpr_consent = bidderRequest.gdprConsent.consentString; + } + + if (params.ifa) { + requestParams.ifa = params.ifa; + } + + if (bid.schain) { + requestParams.schain = getSupplyChain(bid.schain); + } + + if (bidderRequest && bidderRequest.refererInfo) { + requestParams.referrer = utils.deepAccess(bidderRequest, 'refererInfo.referer'); + requestParams.page_url = config.getConfig('pageUrl') || utils.deepAccess(window, 'location.href'); + } + + return requestParams; +} diff --git a/modules/riseBidAdapter.md b/modules/riseBidAdapter.md new file mode 100644 index 00000000000..67eeab18226 --- /dev/null +++ b/modules/riseBidAdapter.md @@ -0,0 +1,51 @@ +#Overview + +Module Name: Rise Bidder Adapter + +Module Type: Bidder Adapter + +Maintainer: prebid-rise-engage@risecodes.com + + +# Description + +Module that connects to Rise's demand sources. + +The Rise adapter requires setup and approval from the Rise. Please reach out to prebid-rise-engage@risecodes.com to create an Rise account. + +The adapter supports Video(instream). For the integration, Rise returns content as vastXML and requires the publisher to define the cache url in config passed to Prebid for it to be valid in the auction. + +# Bid Parameters +## Video + +| Name | Scope | Type | Description | Example +| ---- | ----- | ---- | ----------- | ------- +| `org` | required | String | Rise publisher Id provided by your Rise representative | "56f91cd4d3e3660002000033" +| `floorPrice` | optional | Number | Minimum price in USD. Misuse of this parameter can impact revenue | 2.00 +| `ifa` | optional | String | The ID for advertisers (also referred to as "IDFA") | "XXX-XXX" +| `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: 'rise', + params: { + org: '56f91cd4d3e3660002000033', // Required + floorPrice: 2.00, // Optional + ifa: 'XXX-XXX', // Optional + testMode: false // Optional + } + }] + } + ]; +``` diff --git a/modules/rubiconAnalyticsAdapter.js b/modules/rubiconAnalyticsAdapter.js index ff8cb7895b9..f1ba86de30f 100644 --- a/modules/rubiconAnalyticsAdapter.js +++ b/modules/rubiconAnalyticsAdapter.js @@ -60,7 +60,8 @@ const cache = { const BID_REJECTED_IPF = 'rejected-ipf'; export let rubiConf = { - pvid: utils.generateUUID().slice(0, 8) + pvid: utils.generateUUID().slice(0, 8), + analyticsEventDelay: 0 }; // 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 @@ -162,13 +163,18 @@ function sendMessage(auctionId, bidWonId) { let message = { eventTimeMillis: Date.now(), integration: rubiConf.int_type || DEFAULT_INTEGRATION, - ruleId: rubiConf.rule_name, version: '$prebid.version$', referrerUri: referrer, referrerHostname: rubiconAdapter.referrerHostname || getHostNameFromReferer(referrer), channel: 'web', - wrapperName: rubiConf.wrapperName }; + 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]; @@ -181,7 +187,8 @@ function sendMessage(auctionId, bidWonId) { 'dimensions', 'adserverTargeting', () => stringProperties(cache.targeting[bid.adUnit.adUnitCode] || {}), 'gam', - 'pbAdSlot' + 'pbAdSlot', + 'pattern' ]); adUnit.bids = []; adUnit.status = 'no-bid'; // default it to be no bid @@ -237,6 +244,8 @@ function sendMessage(auctionId, bidWonId) { auction.floors = utils.pick(auctionCache.floorData, [ 'location', 'modelVersion as modelName', + 'modelWeight', + 'modelTimestamp', 'skipped', 'enforcement', () => utils.deepAccess(auctionCache.floorData, 'enforcements.enforceJS'), 'dealsEnforced', () => utils.deepAccess(auctionCache.floorData, 'enforcements.floorDeals'), @@ -382,8 +391,12 @@ function getUtmParams() { function getFpkvs() { rubiConf.fpkvs = Object.assign((rubiConf.fpkvs || {}), getUtmParams()); - const isValid = rubiConf.fpkvs && typeof rubiConf.fpkvs === 'object' && Object.keys(rubiConf.fpkvs).every(key => typeof rubiConf.fpkvs[key] === 'string'); - return isValid ? rubiConf.fpkvs : {}; + // convert all values to strings + Object.keys(rubiConf.fpkvs).forEach(key => { + rubiConf.fpkvs[key] = rubiConf.fpkvs[key] + ''; + }); + + return rubiConf.fpkvs; } let samplingFactor = 1; @@ -457,6 +470,9 @@ function subscribeToGamSlots() { let bid = cache.auctions[auctionId].bids[bidId]; // if this slot matches this bids adUnit, add the adUnit info if (isMatchingAdSlot(bid.adUnit.adUnitCode)) { + // mark this adUnit as having been rendered by gam + cache.auctions[auctionId].gamHasRendered[bid.adUnit.adUnitCode] = true; + bid.adUnit.gam = utils.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 @@ -468,6 +484,16 @@ function subscribeToGamSlots() { ]); } }); + // Now if all adUnits have gam rendered, send the payload + if (rubiConf.waitForGamSlots && !cache.auctions[auctionId].sent && Object.keys(cache.auctions[auctionId].gamHasRendered).every(adUnitCode => cache.auctions[auctionId].gamHasRendered[adUnitCode])) { + clearTimeout(cache.timeouts[auctionId]); + delete cache.timeouts[auctionId]; + if (rubiConf.analyticsEventDelay > 0) { + setTimeout(() => sendMessage.call(rubiconAdapter, auctionId), rubiConf.analyticsEventDelay) + } else { + sendMessage.call(rubiconAdapter, auctionId) + } + } }); }); } @@ -532,6 +558,7 @@ let rubiconAdapter = Object.assign({}, baseAdapter, { ]); cacheEntry.bids = {}; cacheEntry.bidsWon = {}; + cacheEntry.gamHasRendered = {}; cacheEntry.referrer = utils.deepAccess(args, 'bidderRequests.0.refererInfo.referer'); const floorData = utils.deepAccess(args, 'bidderRequests.0.bids.0.floorData'); if (floorData) { @@ -544,6 +571,13 @@ let rubiconAdapter = Object.assign({}, baseAdapter, { if (!cache.gpt.registered && utils.isGptPubadsDefined()) { subscribeToGamSlots(); cache.gpt.registered = true; + } else if (!cache.gpt.registered) { + cache.gpt.registered = true; + window.googletag = window.googletag || {}; + window.googletag.cmd = window.googletag.cmd || []; + window.googletag.cmd.push(function() { + subscribeToGamSlots(); + }); } break; case BID_REQUESTED: @@ -551,6 +585,10 @@ let rubiconAdapter = Object.assign({}, baseAdapter, { // mark adUnits we expect bidWon events for cache.auctions[args.auctionId].bidsWon[bid.adUnitCode] = false; + if (rubiConf.waitForGamSlots) { + cache.auctions[args.auctionId].gamHasRendered[bid.adUnitCode] = false; + } + memo[bid.bidId] = utils.pick(bid, [ 'bidder', bidder => bidder.toLowerCase(), 'bidId', @@ -620,7 +658,8 @@ let rubiconAdapter = Object.assign({}, baseAdapter, { return {adSlot: bid.fpd.context.adServer.adSlot} } }, - 'pbAdSlot', () => utils.deepAccess(bid, 'fpd.context.pbAdSlot') + 'pbAdSlot', () => utils.deepAccess(bid, 'fpd.context.pbAdSlot'), + 'pattern', () => utils.deepAccess(bid, 'fpd.context.aupName') ]) ]); return memo; @@ -696,7 +735,7 @@ let rubiconAdapter = Object.assign({}, baseAdapter, { // check if this BID_WON missed the boat, if so send by itself if (auctionCache.sent === true) { sendMessage.call(this, args.auctionId, args.requestId); - } else if (Object.keys(auctionCache.bidsWon).reduce((memo, adUnitCode) => { + } else if (!rubiConf.waitForGamSlots && Object.keys(auctionCache.bidsWon).reduce((memo, adUnitCode) => { // only send if we've received bidWon events for all adUnits in auction memo = memo && auctionCache.bidsWon[adUnitCode]; return memo; @@ -711,7 +750,7 @@ let rubiconAdapter = Object.assign({}, baseAdapter, { // start timer to send batched payload just in case we don't hear any BID_WON events cache.timeouts[args.auctionId] = setTimeout(() => { sendMessage.call(this, args.auctionId); - }, SEND_TIMEOUT); + }, rubiConf.analyticsBatchTimeout || SEND_TIMEOUT); break; case BID_TIMEOUT: args.forEach(badBid => { diff --git a/modules/rubiconBidAdapter.js b/modules/rubiconBidAdapter.js index e439f7fd2a4..7297c82440f 100644 --- a/modules/rubiconBidAdapter.js +++ b/modules/rubiconBidAdapter.js @@ -103,7 +103,9 @@ var sizeMap = { 278: '320x500', 282: '320x400', 288: '640x380', - 548: '500x1000' + 548: '500x1000', + 550: '980x480', + 552: '300x200' }; utils._each(sizeMap, (item, key) => sizeMap[item] = key); @@ -254,46 +256,7 @@ export const spec = { utils.deepSetValue(data, 'source.ext.schain', bidRequest.schain); } - const siteData = Object.assign({}, bidRequest.params.inventory, config.getConfig('fpd.context')); - const userData = Object.assign({}, bidRequest.params.visitor, config.getConfig('fpd.user')); - if (!utils.isEmpty(siteData) || !utils.isEmpty(userData)) { - const bidderData = { - bidders: [ bidderRequest.bidderCode ], - config: { - fpd: {} - } - }; - - if (!utils.isEmpty(siteData)) { - bidderData.config.fpd.site = siteData; - } - - if (!utils.isEmpty(userData)) { - bidderData.config.fpd.user = userData; - } - - utils.deepSetValue(data, 'ext.prebid.bidderconfig.0', bidderData); - } - - /** - * Prebid AdSlot - * @type {(string|undefined)} - */ - const pbAdSlot = utils.deepAccess(bidRequest, 'fpd.context.pbAdSlot'); - if (typeof pbAdSlot === 'string' && pbAdSlot) { - utils.deepSetValue(data.imp[0].ext, 'context.data.pbadslot', pbAdSlot); - } - - /** - * Copy GAM AdUnit and Name to imp - */ - ['name', 'adSlot'].forEach(name => { - /** @type {(string|undefined)} */ - const value = utils.deepAccess(bidRequest, `fpd.context.adserver.${name}`); - if (typeof value === 'string' && value) { - utils.deepSetValue(data.imp[0].ext, `context.data.adserver.${name.toLowerCase()}`, value); - } - }); + applyFPD(bidRequest, VIDEO, data); // if storedAuctionResponse has been set, pass SRID if (bidRequest.storedAuctionResponse) { @@ -516,7 +479,7 @@ export const spec = { } else if (eid.source === 'sharedid.org') { data['eid_sharedid.org'] = `${eid.uids[0].id}^${eid.uids[0].atype}^${(eid.uids[0].ext && eid.uids[0].ext.third) || ''}`; } else if (eid.source === 'id5-sync.com') { - data['eid_id5-sync.com'] = `${eid.uids[0].id}^${eid.uids[0].atype}^${(eid.ext && eid.ext.linkType) || ''}`; + data['eid_id5-sync.com'] = `${eid.uids[0].id}^${eid.uids[0].atype}^${(eid.uids[0].ext && eid.uids[0].ext.linkType) || ''}`; } else { // add anything else with this generic format data[`eid_${eid.source}`] = `${eid.uids[0].id}^${eid.uids[0].atype || ''}`; @@ -547,49 +510,7 @@ export const spec = { data['us_privacy'] = encodeURIComponent(bidderRequest.uspConsent); } - // visitor properties - const visitorData = Object.assign({}, params.visitor, config.getConfig('fpd.user')); - Object.keys(visitorData).forEach((key) => { - if (visitorData[key] != null && key !== 'keywords') { - data[`tg_v.${key}`] = typeof visitorData[key] === 'object' && !Array.isArray(visitorData[key]) - ? JSON.stringify(visitorData[key]) - : visitorData[key].toString(); // initialize array; - } - }); - - // inventory properties - const inventoryData = Object.assign({}, params.inventory, config.getConfig('fpd.context')); - Object.keys(inventoryData).forEach((key) => { - if (inventoryData[key] != null && key !== 'keywords') { - data[`tg_i.${key}`] = typeof inventoryData[key] === 'object' && !Array.isArray(inventoryData[key]) - ? JSON.stringify(inventoryData[key]) - : inventoryData[key].toString(); - } - }); - - // keywords - const keywords = (params.keywords || []).concat( - utils.deepAccess(config.getConfig('fpd.user'), 'keywords') || [], - utils.deepAccess(config.getConfig('fpd.context'), 'keywords') || []); - data.kw = Array.isArray(keywords) && keywords.length ? keywords.join(',') : ''; - - /** - * Prebid AdSlot - * @type {(string|undefined)} - */ - const pbAdSlot = utils.deepAccess(bidRequest, 'fpd.context.pbAdSlot'); - if (typeof pbAdSlot === 'string' && pbAdSlot) { - data['tg_i.pbadslot'] = pbAdSlot.replace(/^\/+/, ''); - } - - /** - * GAM Ad Unit - * @type {(string|undefined)} - */ - const gamAdUnit = utils.deepAccess(bidRequest, 'fpd.context.adServer.adSlot'); - if (typeof gamAdUnit === 'string' && gamAdUnit) { - data['tg_i.dfp_ad_unit_code'] = gamAdUnit.replace(/^\/+/, ''); - } + applyFPD(bidRequest, BANNER, data); if (config.getConfig('coppa') === true) { data['coppa'] = 1; @@ -949,6 +870,81 @@ function addVideoParameters(data, bidRequest) { data.imp[0].video.h = size[1] } +function applyFPD(bidRequest, mediaType, data) { + const bidFpd = { + user: {...bidRequest.params.visitor}, + context: {...bidRequest.params.inventory} + }; + + if (bidRequest.params.keywords) bidFpd.context.keywords = (utils.isArray(bidRequest.params.keywords)) ? bidRequest.params.keywords.join(',') : bidRequest.params.keywords; + + let fpd = utils.mergeDeep({}, config.getConfig('fpd') || {}, bidRequest.fpd || {}, bidFpd); + + const map = {user: {banner: 'tg_v.', code: 'user'}, context: {banner: 'tg_i.', code: 'site'}, adserver: 'dfp_ad_unit_code'}; + let obj = {}; + let impData = {}; + let keywords = []; + const validate = function(prop, key) { + if (typeof prop === 'object' && !Array.isArray(prop)) { + utils.logWarn('Rubicon: Filtered FPD key: ', key, ': Expected value to be string, integer, or an array of strings/ints'); + } else if (typeof prop !== 'undefined') { + return (Array.isArray(prop)) ? prop.filter(value => { + if (typeof value !== 'object' && typeof value !== 'undefined') return value.toString(); + + utils.logWarn('Rubicon: Filtered value: ', value, 'for key', key, ': Expected value to be string, integer, or an array of strings/ints'); + }).toString() : prop.toString(); + } + }; + + Object.keys(fpd).filter(value => fpd[value] && map[value] && typeof fpd[value] === 'object').forEach((type) => { + obj[map[type].code] = Object.keys(fpd[type]).filter(value => typeof fpd[type][value] !== 'undefined').reduce((result, key) => { + if (key === 'keywords') { + if (!Array.isArray(fpd[type][key]) && mediaType === BANNER) fpd[type][key] = [fpd[type][key]] + + result[key] = fpd[type][key]; + + if (mediaType === BANNER) keywords = keywords.concat(fpd[type][key]); + } else if (key === 'data') { + utils.mergeDeep(result, {ext: {data: fpd[type][key]}}); + } else if (key === 'adServer' || key === 'pbAdSlot') { + (key === 'adServer') ? ['name', 'adSlot'].forEach(name => { + const value = validate(fpd[type][key][name]); + if (value) utils.deepSetValue(impData, `adserver.${name.toLowerCase()}`, value.replace(/^\/+/, '')) + }) : impData[key.toLowerCase()] = fpd[type][key].replace(/^\/+/, '') + } else { + utils.mergeDeep(result, {ext: {data: {[key]: fpd[type][key]}}}); + } + + return result; + }, {}); + + if (mediaType === BANNER) { + let duplicate = (typeof obj[map[type].code].ext === 'object' && obj[map[type].code].ext.data) || {}; + + Object.keys(duplicate).forEach((key) => { + const val = (key === 'adserver') ? duplicate.adserver.adslot : validate(duplicate[key], key); + + if (val) data[(map[key]) ? `${map[type][BANNER]}${map[key]}` : `${map[type][BANNER]}${key}`] = val; + }); + } + }); + + Object.keys(impData).forEach((key) => { + if (mediaType === BANNER) { + (map[key]) ? data[`tg_i.${map[key]}`] = impData[key].adslot : data[`tg_i.${key.toLowerCase()}`] = impData[key]; + } else { + utils.mergeDeep(data.imp[0], {ext: {context: {data: {[key]: impData[key]}}}}); + } + }); + + if (mediaType === BANNER) { + let kw = validate(keywords, 'keywords'); + if (kw) data.kw = kw; + } else { + utils.mergeDeep(data, obj); + } +} + /** * @param sizes * @returns {*} diff --git a/modules/s2sTesting.js b/modules/s2sTesting.js index 98b3b86671c..1f2bb473174 100644 --- a/modules/s2sTesting.js +++ b/modules/s2sTesting.js @@ -1,4 +1,3 @@ -import { config } from '../src/config.js'; import { setS2STestingModule } from '../src/adapterManager.js'; let s2sTesting = {}; @@ -9,39 +8,31 @@ const CLIENT = 'client'; s2sTesting.SERVER = SERVER; s2sTesting.CLIENT = CLIENT; -var testing = false; // whether testing is turned on -var bidSource = {}; // store bidder sources determined from s2sConfing bidderControl +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% -// load s2sConfig -config.getConfig('s2sConfig', config => { - testing = config.s2sConfig && config.s2sConfig.testing; - s2sTesting.calculateBidSources(config.s2sConfig); -}); - -s2sTesting.getSourceBidderMap = function(adUnits = []) { +s2sTesting.getSourceBidderMap = function(adUnits = [], allS2SBidders = []) { var sourceBidders = {[SERVER]: {}, [CLIENT]: {}}; - // bail if testing is not turned on - if (!testing) { - return {[SERVER]: [], [CLIENT]: []}; - } - adUnits.forEach((adUnit) => { // if any adUnit bidders specify a bidSource, include them (adUnit.bids || []).forEach((bid) => { + // When a s2sConfig does not have testing=true and did not calc bid sources + if (allS2SBidders.indexOf(bid.bidder) > -1 && !s2sTesting.bidSource[bid.bidder]) { + s2sTesting.bidSource[bid.bidder] = SERVER; + } // calculate the source once and store on bid object bid.calcSource = bid.calcSource || s2sTesting.getSource(bid.bidSource); // if no bidSource at bid level, default to bidSource from bidder - bid.finalSource = bid.calcSource || bidSource[bid.bidder] || CLIENT; // default to client + bid.finalSource = bid.calcSource || s2sTesting.bidSource[bid.bidder] || CLIENT; // default to client // add bidder to sourceBidders data structure sourceBidders[bid.finalSource][bid.bidder] = true; }); }); // make sure all bidders in bidSource are in sourceBidders - Object.keys(bidSource).forEach((bidder) => { - sourceBidders[bidSource[bidder]][bidder] = true; + Object.keys(s2sTesting.bidSource).forEach((bidder) => { + sourceBidders[s2sTesting.bidSource[bidder]][bidder] = true; }); // return map of source => array of bidders @@ -53,18 +44,14 @@ s2sTesting.getSourceBidderMap = function(adUnits = []) { /** * @function calculateBidSources determines the source for each s2s bidder based on bidderControl weightings. these can be overridden at the adUnit level - * @param s2sConfig server-to-server configuration + * @param s2sConfigs server-to-server configuration */ s2sTesting.calculateBidSources = function(s2sConfig = {}) { - // bail if testing is not turned on - if (!testing) { - return; - } - bidSource = {}; // reset bid sources // calculate bid source (server/client) for each s2s bidder + var bidderControl = s2sConfig.bidderControl || {}; (s2sConfig.bidders || []).forEach((bidder) => { - bidSource[bidder] = s2sTesting.getSource(bidderControl[bidder] && bidderControl[bidder].bidSource) || SERVER; // default to server + s2sTesting.bidSource[bidder] = s2sTesting.getSource(bidderControl[bidder] && bidderControl[bidder].bidSource) || SERVER; // default to server }); }; diff --git a/modules/sharedIdSystem.js b/modules/sharedIdSystem.js index 49cac46f1df..762454af5fa 100644 --- a/modules/sharedIdSystem.js +++ b/modules/sharedIdSystem.js @@ -276,6 +276,17 @@ function detectPrng(root) { return () => Math.random(); } +/** + * Builds and returns the shared Id URL with attached consent data if applicable + * @param {Object} consentData + * @return {string} + */ +function sharedIdUrl(consentData) { + if (!consentData || typeof consentData.gdprApplies !== 'boolean' || !consentData.gdprApplies) return ID_SVC; + + return `${ID_SVC}?gdpr=1&gdpr_consent=${consentData.consentString}` +} + /** @type {Submodule} */ export const sharedIdSubmodule = { /** @@ -303,12 +314,13 @@ export const sharedIdSubmodule = { * performs action to obtain id and return a value. * @function * @param {SubmoduleConfig} [config] + * @param {ConsentData|undefined} consentData * @returns {sharedId} */ - getId(config) { + getId(config, consentData) { const resp = function (callback) { utils.logInfo('SharedId: Sharedid doesnt exists, new cookie creation'); - ajax(ID_SVC, idGenerationCallback(callback), undefined, {method: 'GET', withCredentials: true}); + ajax(sharedIdUrl(consentData), idGenerationCallback(callback), undefined, {method: 'GET', withCredentials: true}); }; return {callback: resp}; }, @@ -316,10 +328,11 @@ export const sharedIdSubmodule = { /** * performs actions even if the id exists and returns a value * @param config + * @param consentData * @param storedId * @returns {{callback: *}} */ - extendId(config, storedId) { + extendId(config, consentData, storedId) { const configParams = (config && config.params) || {}; utils.logInfo('SharedId: Existing shared id ' + storedId.id); const resp = function (callback) { @@ -329,7 +342,7 @@ export const sharedIdSubmodule = { const sharedIdPayload = {}; sharedIdPayload.sharedId = storedId.id; const payloadString = JSON.stringify(sharedIdPayload); - ajax(ID_SVC, existingIdCallback(storedId, callback), payloadString, {method: 'POST', withCredentials: true}); + ajax(sharedIdUrl(consentData), existingIdCallback(storedId, callback), payloadString, {method: 'POST', withCredentials: true}); } }; return {callback: resp}; diff --git a/modules/sharethroughBidAdapter.js b/modules/sharethroughBidAdapter.js index 7df161db713..24be8673615 100644 --- a/modules/sharethroughBidAdapter.js +++ b/modules/sharethroughBidAdapter.js @@ -1,7 +1,7 @@ import { registerBidder } from '../src/adapters/bidderFactory.js'; import * as utils from '../src/utils.js'; -const VERSION = '3.2.1'; +const VERSION = '3.3.1'; const BIDDER_CODE = 'sharethrough'; const STR_ENDPOINT = 'https://btlr.sharethrough.com/WYu2BXv1/v1'; const DEFAULT_SIZE = [1, 1]; @@ -31,6 +31,8 @@ export const sharethroughAdapterSpec = { strVersion: VERSION }; + Object.assign(query, handleUniversalIds(bidRequest)); + const nonHttp = sharethroughInternal.getProtocol().indexOf('http') < 0; query.secure = nonHttp || (sharethroughInternal.getProtocol().indexOf('https') > -1); @@ -46,16 +48,6 @@ export const sharethroughAdapterSpec = { query.us_privacy = bidderRequest.uspConsent } - if (bidRequest.userId && bidRequest.userId.tdid) { - query.ttduid = bidRequest.userId.tdid; - } - - if (bidRequest.userId && bidRequest.userId.pubcid) { - query.pubcid = bidRequest.userId.pubcid; - } else if (bidRequest.crumbs && bidRequest.crumbs.pubcid) { - query.pubcid = bidRequest.crumbs.pubcid; - } - if (bidRequest.schain) { query.schain = JSON.stringify(bidRequest.schain); } @@ -64,6 +56,14 @@ export const sharethroughAdapterSpec = { query.bidfloor = parseFloat(bidRequest.bidfloor); } + if (bidRequest.params.badv) { + query.badv = bidRequest.params.badv; + } + + if (bidRequest.params.bcat) { + query.bcat = bidRequest.params.bcat; + } + // Data that does not need to go to the server, // but we need as part of interpretResponse() const strData = { @@ -73,7 +73,7 @@ export const sharethroughAdapterSpec = { }; return { - method: 'GET', + method: 'POST', url: STR_ENDPOINT, data: query, strData: strData @@ -135,6 +135,36 @@ export const sharethroughAdapterSpec = { onSetTargeting: (bid) => {} }; +function handleUniversalIds(bidRequest) { + if (!bidRequest.userId) return {}; + + const universalIds = {}; + + const ttd = utils.deepAccess(bidRequest, 'userId.tdid'); + if (ttd) universalIds.ttduid = ttd; + + const pubc = utils.deepAccess(bidRequest, 'userId.pubcid') || utils.deepAccess(bidRequest, 'crumbs.pubcid'); + if (pubc) universalIds.pubcid = pubc; + + const idl = utils.deepAccess(bidRequest, 'userId.idl_env'); + if (idl) universalIds.idluid = idl; + + const id5 = utils.deepAccess(bidRequest, 'userId.id5id.uid'); + if (id5) { + universalIds.id5uid = { id: id5 }; + const id5link = utils.deepAccess(bidRequest, 'userId.id5id.ext.linkType'); + if (id5link) universalIds.id5uid.linkType = id5link; + } + + const lipb = utils.deepAccess(bidRequest, 'userId.lipb.lipbid'); + if (lipb) universalIds.liuid = lipb; + + const shd = utils.deepAccess(bidRequest, 'userId.sharedid'); + if (shd) universalIds.shduid = shd; // object with keys: id & third + + return universalIds; +} + function getLargestSize(sizes) { function area(size) { return size[0] * size[1]; diff --git a/modules/sharethroughBidAdapter.md b/modules/sharethroughBidAdapter.md index 2290e370cae..396b8164577 100644 --- a/modules/sharethroughBidAdapter.md +++ b/modules/sharethroughBidAdapter.md @@ -29,7 +29,13 @@ Module that connects to Sharethrough's demand sources // OPTIONAL - If iframeSize is provided, we'll use this size for the iframe // otherwise we'll grab the largest size from the sizes array // This is ignored if iframe: false - iframeSize: [250, 250] + iframeSize: [250, 250], + + // OPTIONAL - Blocked Advertiser Domains + badv: ['domain1.com', 'domain2.com'], + + // OPTIONAL - Blocked Categories (IAB codes) + bcat: ['IAB1-1', 'IAB1-2'], } } ] diff --git a/modules/smartadserverBidAdapter.js b/modules/smartadserverBidAdapter.js index ed9003e3b4d..bb9364c72c3 100644 --- a/modules/smartadserverBidAdapter.js +++ b/modules/smartadserverBidAdapter.js @@ -135,7 +135,7 @@ export const spec = { const bidResponses = []; let response = serverResponse.body; try { - if (response) { + if (response && !response.isNoAd) { const bidRequest = JSON.parse(bidRequestString.data); let bidResponse = { @@ -147,7 +147,8 @@ export const spec = { dealId: response.dealId, currency: response.currency, netRevenue: response.isNetCpm, - ttl: response.ttl + ttl: response.ttl, + dspPixels: response.dspPixels }; if (bidRequest.mediaType === VIDEO) { @@ -182,6 +183,13 @@ export const spec = { type: 'iframe', url: serverResponses[0].body.cSyncUrl }); + } else if (syncOptions.pixelEnabled && serverResponses.length > 0 && serverResponses[0].body.dspPixels !== undefined) { + serverResponses[0].body.dspPixels.forEach(function(pixel) { + syncs.push({ + type: 'image', + url: pixel + }); + }); } return syncs; } diff --git a/modules/smartxBidAdapter.js b/modules/smartxBidAdapter.js index 4409e4e9dfb..804b25d1afc 100644 --- a/modules/smartxBidAdapter.js +++ b/modules/smartxBidAdapter.js @@ -328,16 +328,49 @@ export const spec = { } function createOutstreamScript(bid) { - // const slot = utils.getBidIdParameter('slot', bid.renderer.config.outstream_options); + // for SmartPlay 4.12 + function scPrebidClose(ele, completeCollapsed) { + if (completeCollapsed) { + document.getElementById(ele.id).style.display = 'none'; + } + } + + const confMinAdWidth = utils.getBidIdParameter('minAdWidth', bid.renderer.config.outstream_options) || 290; + const confMaxAdWidth = utils.getBidIdParameter('maxAdWidth', bid.renderer.config.outstream_options) || 900; + const confStartOpen = utils.getBidIdParameter('startOpen', bid.renderer.config.outstream_options) || 'false'; + const confEndingScreen = utils.getBidIdParameter('endingScreen', bid.renderer.config.outstream_options) || 'true'; + const confHeaderText = utils.getBidIdParameter('headerText', bid.renderer.config.outstream_options) || ''; + const confSkipOffset = utils.getBidIdParameter('skipOffset', bid.renderer.config.outstream_options) || 0; + const confDesiredBitrate = utils.getBidIdParameter('desiredBitrate', bid.renderer.config.outstream_options) || 1600; + const elementId = utils.getBidIdParameter('slot', bid.renderer.config.outstream_options) || bid.adUnitCode; + + // for SmartPlay 4.12 + let initCollapsed = true; + let completeCollapsed = true; + if (confStartOpen === 'true') { + initCollapsed = false; + } + if (confEndingScreen === 'true') { + completeCollapsed = false; + } + utils.logMessage('[SMARTX][renderer] Handle SmartX outstream renderer'); - const elementId = bid.adUnitCode; + let smartPlayObj = { - minAdWidth: 290, - maxAdWidth: 900, - elementLocator: { - allowInViewport: false, - minimumElementWidth: 290, - scanPixelsBelowViewport: 800 + minAdWidth: confMinAdWidth, + maxAdWidth: confMaxAdWidth, + headerText: confHeaderText, + skipOffset: confSkipOffset, + behaviourMatrix: { + init: { + 'collapsed': initCollapsed + }, + complete: { + 'collapsed': completeCollapsed + } + }, + environmentVars: { + desiredBitrate: confDesiredBitrate, }, onStartCallback: function (m, n) { try { @@ -351,6 +384,7 @@ function createOutstreamScript(bid) { }, onEndCallback: function (m, n) { try { + scPrebidClose(n, completeCollapsed); // for SmartPlay 4.12 window.sc_smartIntxtEnd(n); } catch (f) {} }, diff --git a/modules/smartxBidAdapter.md b/modules/smartxBidAdapter.md index a53af839e2b..082a36f3dde 100644 --- a/modules/smartxBidAdapter.md +++ b/modules/smartxBidAdapter.md @@ -29,11 +29,18 @@ This adapter requires setup and approval from the smartclip team. publisherId: '11986', siteId: '22860', bidfloor: 0.3, - bidfloorcur: "EUR", + bidfloorcur: 'EUR', at: 2, - cur: ["EUR"], + cur: ['EUR'], outstream_options: { - slot: 'video1' + slot: 'video1', + minAdWidth: 290, + maxAdWidth: 900, + headerText: '', + skipOffset: 0, + startOpen: 'true', + endingScreen: 'true', + desiredBitrate: 1600, }, } }], @@ -57,11 +64,18 @@ This adapter requires setup and approval from the smartclip team. publisherId: '11986', siteId: '22860', bidfloor: 0.3, - bidfloorcur: "EUR", + bidfloorcur: 'EUR', at: 2, - cur: ["EUR"], + cur: ['EUR'], outstream_options: { - slot: 'video1' + slot: 'video1', + minAdWidth: 290, + maxAdWidth: 900, + headerText: '', + skipOffset: 0, + startOpen: 'true', + endingScreen: 'true', + desiredBitrate: 1600, }, user: { data: [{ @@ -104,9 +118,9 @@ This adapter requires setup and approval from the smartclip team. publisherId: '11986', siteId: '22860', bidfloor: 0.3, - bidfloorcur: "EUR", + bidfloorcur: 'EUR', at: 2, - cur: ["EUR"] + cur: ['EUR'] } }], }]; @@ -129,9 +143,9 @@ This adapter requires setup and approval from the smartclip team. publisherId: '11986', siteId: '22860', bidfloor: 0.3, - bidfloorcur: "EUR", + bidfloorcur: 'EUR', at: 2, - cur: ["EUR"], + cur: ['EUR'], user: { data: [{ id: 'emq', diff --git a/modules/sovrnBidAdapter.js b/modules/sovrnBidAdapter.js index 62f5e85779e..9ec4d339753 100644 --- a/modules/sovrnBidAdapter.js +++ b/modules/sovrnBidAdapter.js @@ -14,7 +14,7 @@ export const spec = { * @return boolean for whether or not a bid is valid */ isBidRequestValid: function(bid) { - return !!(bid.params.tagid && !isNaN(parseFloat(bid.params.tagid)) && isFinite(bid.params.tagid)); + return !!(bid.params.tagid && !isNaN(parseFloat(bid.params.tagid)) && isFinite(bid.params.tagid)) }, /** @@ -45,15 +45,22 @@ export const spec = { } if (bid.schain) { - schain = schain || bid.schain; + schain = schain || bid.schain } - iv = iv || utils.getBidIdParameter('iv', bid.params); + iv = iv || utils.getBidIdParameter('iv', bid.params) - let bidSizes = (bid.mediaTypes && bid.mediaTypes.banner && bid.mediaTypes.banner.sizes) || bid.sizes; + let bidSizes = (bid.mediaTypes && bid.mediaTypes.banner && bid.mediaTypes.banner.sizes) || bid.sizes bidSizes = ((utils.isArray(bidSizes) && utils.isArray(bidSizes[0])) ? bidSizes : [bidSizes]) bidSizes = bidSizes.filter(size => utils.isArray(size)) const processedSizes = bidSizes.map(size => ({w: parseInt(size[0], 10), h: parseInt(size[1], 10)})) - sovrnImps.push({ + const floorInfo = (bid.getFloor && typeof bid.getFloor === 'function') ? bid.getFloor({ + currency: 'USD', + mediaType: 'banner', + size: '*' + }) : {} + floorInfo.floor = floorInfo.floor || utils.getBidIdParameter('bidfloor', bid.params) + + const imp = { adunitcode: bid.adUnitCode, id: bid.bidId, banner: { @@ -62,8 +69,18 @@ export const spec = { h: 1, }, tagid: String(utils.getBidIdParameter('tagid', bid.params)), - bidfloor: utils.getBidIdParameter('bidfloor', bid.params) - }); + bidfloor: floorInfo.floor + } + + const segmentsString = utils.getBidIdParameter('segments', bid.params) + + if (segmentsString) { + imp.ext = { + deals: segmentsString.split(',').map(deal => deal.trim()) + } + } + + sovrnImps.push(imp); }); const page = bidderRequest.refererInfo.referer @@ -143,7 +160,7 @@ export const spec = { netRevenue: true, mediaType: BANNER, ad: decodeURIComponent(`${sovrnBid.adm}`), - ttl: sovrnBid.ttl || 90 + ttl: sovrnBid.ext ? (sovrnBid.ext.ttl || 90) : 90 }); }); } diff --git a/modules/spotxBidAdapter.js b/modules/spotxBidAdapter.js index 6104fce1d97..c85a836435e 100644 --- a/modules/spotxBidAdapter.js +++ b/modules/spotxBidAdapter.js @@ -51,7 +51,7 @@ export const spec = { return false; } if (!utils.getBidIdParameter('slot', bid.params.outstream_options)) { - utils.logError(BIDDER_CODE + ': please define parameters slot outstream_options object in the configuration.'); + utils.logError(BIDDER_CODE + ': please define parameter slot in outstream_options object in the configuration.'); return false; } } @@ -245,9 +245,9 @@ export const spec = { { source: 'id5-sync.com', uids: [{ - id: bid.userId.id5id.uid - }], - ext: bid.userId.id5id.ext || {} + id: bid.userId.id5id.uid, + ext: bid.userId.id5id.ext || {} + }] } ) } @@ -382,7 +382,7 @@ export const spec = { } }); } catch (err) { - utils.logWarn('Prebid Error calling setRender or setEve,tHandlers on renderer', err); + utils.logWarn('Prebid Error calling setRender or setEventHandlers on renderer', err); } bid.renderer = renderer; } @@ -408,7 +408,7 @@ function createOutstreamScript(bid) { dataSpotXParams['data-spotx_content_page_url'] = bid.renderer.config.content_page_url; dataSpotXParams['data-spotx_ad_unit'] = 'incontent'; - utils.logMessage('[SPOTX][renderer] Default beahavior'); + utils.logMessage('[SPOTX][renderer] Default behavior'); if (utils.getBidIdParameter('ad_mute', bid.renderer.config.outstream_options)) { dataSpotXParams['data-spotx_ad_mute'] = '1'; } @@ -419,30 +419,26 @@ function createOutstreamScript(bid) { const playersizeAutoAdapt = utils.getBidIdParameter('playersize_auto_adapt', bid.renderer.config.outstream_options); if (playersizeAutoAdapt && utils.isBoolean(playersizeAutoAdapt) && playersizeAutoAdapt === true) { - if (bid.width && utils.isNumber(bid.width) && bid.height && utils.isNumber(bid.height)) { - const ratio = bid.width / bid.height; - const slotClientWidth = window.document.getElementById(slot).clientWidth; - let playerWidth = bid.renderer.config.player_width; - let playerHeight = bid.renderer.config.player_height; - let contentWidth = 0; - let contentHeight = 0; - if (slotClientWidth < playerWidth) { - playerWidth = slotClientWidth; - playerHeight = playerWidth / ratio; - } - if (ratio <= 1) { - contentWidth = Math.round(playerHeight * ratio); - contentHeight = playerHeight; - } else { - contentWidth = playerWidth; - contentHeight = Math.round(playerWidth / ratio); - } - - dataSpotXParams['data-spotx_content_width'] = '' + contentWidth; - dataSpotXParams['data-spotx_content_height'] = '' + contentHeight; + const ratio = bid.width && utils.isNumber(bid.width) && bid.height && utils.isNumber(bid.height) ? bid.width / bid.height : 4 / 3; + const slotClientWidth = window.document.getElementById(slot).clientWidth; + let playerWidth = bid.renderer.config.player_width; + let playerHeight = bid.renderer.config.player_height; + let contentWidth = 0; + let contentHeight = 0; + if (slotClientWidth < playerWidth) { + playerWidth = slotClientWidth; + playerHeight = playerWidth / ratio; + } + if (ratio <= 1) { + contentWidth = Math.round(playerHeight * ratio); + contentHeight = playerHeight; } else { - utils.logWarn('[SPOTX][renderer] PlayerSize auto adapt: bid.width and bid.height are incorrect'); + contentWidth = playerWidth; + contentHeight = Math.round(playerWidth / ratio); } + + dataSpotXParams['data-spotx_content_width'] = '' + contentWidth; + dataSpotXParams['data-spotx_content_height'] = '' + contentHeight; } const customOverride = utils.getBidIdParameter('custom_override', bid.renderer.config.outstream_options); diff --git a/modules/sspBCAdapter.js b/modules/sspBCBidAdapter.js similarity index 72% rename from modules/sspBCAdapter.js rename to modules/sspBCBidAdapter.js index 4069c722e9d..b6e5b88e0f4 100644 --- a/modules/sspBCAdapter.js +++ b/modules/sspBCBidAdapter.js @@ -2,17 +2,65 @@ import * as utils from '../src/utils.js'; import { ajax } from '../src/ajax.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; import { BANNER } from '../src/mediaTypes.js'; +import strIncludes from 'core-js-pure/features/string/includes.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 TMAX = 450; -const BIDDER_VERSION = '4.6'; +const BIDDER_VERSION = '4.7'; const W = window; const { navigator } = W; +const oneCodeDetection = {}; var consentApiVersion; +/** + * Get bid parameters for notification + * @param {*} bidData - bid (bidWon), or array of bids (timeout) + */ +const getNotificationPayload = bidData => { + if (bidData) { + const bids = utils.isArray(bidData) ? bidData : [bidData]; + if (bids.length > 0) { + const result = { + requestId: undefined, + siteId: [], + adUnit: [], + slotId: [], + } + bids.forEach(bid => { + let params = utils.isArray(bid.params) ? bid.params[0] : bid.params; + params = params || {}; + + // check for stored detection + if (oneCodeDetection[bid.requestId]) { + params.siteId = oneCodeDetection[bid.requestId][0]; + params.id = oneCodeDetection[bid.requestId][1]; + } + + if (params.siteId) { + result.siteId.push(params.siteId); + } + if (params.id) { + result.slotId.push(params.id); + } + if (bid.cpm) { + const meta = bid.meta || {}; + result.cpm = bid.cpm; + result.creativeId = bid.creativeId; + result.adomain = meta.advertiserDomains && meta.advertiserDomains[0]; + result.networkName = meta.networkName; + } + result.adUnit.push(bid.adUnitCode) + result.requestId = bid.auctionId || result.requestId; + result.timeout = bid.timeout || result.timeout; + }) + return result; + } + } +} + const cookieSupport = () => { const isSafari = /^((?!chrome|android|crios|fxios).)*safari/i.test(navigator.userAgent); const useCookies = navigator.cookieEnabled || !!document.cookie.length; @@ -104,13 +152,13 @@ function mapBanner(slot) { function mapImpression(slot) { const imp = { - id: slot.params.id, + id: (slot.params && slot.params.id) ? slot.params.id : 'bidid-' + slot.bidId, banner: mapBanner(slot), /* native: mapNative(slot), */ - tagid: slot.params.id, + tagid: slot.adUnitCode, }; - const bidfloor = parseFloat(slot.params.bidfloor); + const bidfloor = (slot.params && slot.params.bidFloor) ? parseFloat(slot.params.bidFloor) : undefined; if (bidfloor) { imp.bidfloor = bidfloor; @@ -170,6 +218,7 @@ function renderCreative(site, auctionId, bid, seat, request) { + + + +
+ + + `; +} + +function wrapBanner(bid, bidData) { + return ` + + + + + + + + +
+ + + `; +} + +export const spec = { + code: BIDDER_CODE, + supportedMediaTypes: [BANNER, VIDEO], + + /** + * Determines whether or not the given bid request is valid. + * + * @param {BidRequest} bid The bid params to validate. + * @return boolean True if this is a valid bid, and false otherwise. + */ + isBidRequestValid(bid) { + return ( + !!bid.params.placementId && + !!bid.params.placement && + ( + (getMediaTypeFromBid(bid) === BANNER && bid.params.placement === 'banner') || + (getMediaTypeFromBid(bid) === BANNER && bid.params.placement === 'inImage' && !!bid.params.imageUrl) || + (getMediaTypeFromBid(bid) === VIDEO && bid.params.placement === 'video' && hasVideoMandatoryParams(bid.mediaTypes)) + ) + ); + }, + + /** + * Make a server request from the list of BidRequests. + * + * @param {validBidRequests[]} - an array of bids + * @return ServerRequest Info describing the request to the server. + */ + buildRequests(validBidRequests, bidderRequest) { + const payload = { + url: bidderRequest.refererInfo.referer, + cmp: !!bidderRequest.gdprConsent, + bidRequests: buildBidRequests(validBidRequests) + }; + + if (payload.cmp) { + const gdprApplies = bidderRequest.gdprConsent.gdprApplies; + if (gdprApplies !== undefined) payload['ga'] = gdprApplies; + payload['cs'] = bidderRequest.gdprConsent.consentString; + } + + const payloadString = JSON.stringify(payload); + + return { + method: 'POST', + url: SSP_ENDPOINT, + data: payloadString, + options: { + contentType: 'application/json' + } + } + }, + + /** + * Unpack the response from the server into a list of bids. + * + * @param {ServerResponse} serverResponse A successful response from the server. + * @return {Bid[]} An array of bids which were nested inside the server. + */ + interpretResponse: function(serverResponse, bidRequest) { + let bidRequests = JSON.parse(bidRequest.data).bidRequests; + const serverBody = serverResponse.body; + + if (serverBody && serverBody.bids && utils.isArray(serverBody.bids)) { + return utils._map(serverBody.bids, function(bid) { + let rawBid = find(bidRequests, function (item) { + return item.bidId === bid.bidId; + }); + bid.placement = rawBid.placement; + bid.transactionId = rawBid.transactionId; + bid.placeId = rawBid.placeId; + return buildBid(bid); + }); + } else { + return []; + } + } + +} +registerBidder(spec); diff --git a/modules/voxBidAdapter.md b/modules/voxBidAdapter.md new file mode 100644 index 00000000000..3fc0383e6f8 --- /dev/null +++ b/modules/voxBidAdapter.md @@ -0,0 +1,237 @@ +# Overview + + +**Module Name**: VOX Bidder Adapter +**Module Type**: Bidder Adapter +**Maintainer**: prebid@hybrid.ai + +# Description + +You can use this adapter to get a bid from partners.hybrid.ai + + +## Sample Banner Ad Unit + +```js +var adUnits = [{ + code: 'banner_ad_unit', + mediaTypes: { + banner: { + sizes: [[160, 600]] + } + }, + bids: [{ + bidder: "vox", + params: { + placement: "banner", // required + placementId: "5fc77bc5a757531e24c89a4c" // required + } + }] +}]; +``` + +## Sample Video Ad Unit + +```js +var adUnits = [{ + code: 'video_ad_unit', + mediaTypes: { + video: { + context: 'outstream', // required + playerSize: [[640, 480]] // required + } + }, + bids: [{ + bidder: 'vox', + params: { + placement: "video", // required + placementId: "5fc77a94a757531e24c89a3d" // required + } + }] +}]; +``` + +# Sample In-Image Ad Unit + +```js +var adUnits = [{ + code: 'test-div', + mediaTypes: { + banner: { + sizes: [0, 0] + } + }, + bids: [{ + bidder: "vox", + params: { + placement: "inImage", + placementId: "5fc77b40a757531e24c89a42", + imageUrl: "https://gallery.voxexchange.io/vox-main.png" + } + }] +}]; +``` + +# Example page with In-Image + +```html + + + + + Prebid.js Banner Example + + + + + +

Prebid.js InImage Banner Test

+
+ + +
+ + +``` + +# Example page with In-Image and GPT + +```html + + + + + Prebid.js Banner Example + + + + + + +

Prebid.js Banner Ad Unit Test

+
+ + +
+ + +``` diff --git a/modules/waardexBidAdapter.js b/modules/waardexBidAdapter.js index b9114d4f1bf..1558ea39cd1 100644 --- a/modules/waardexBidAdapter.js +++ b/modules/waardexBidAdapter.js @@ -1,217 +1,295 @@ -import { registerBidder } from '../src/adapters/bidderFactory.js'; -import { BANNER } from '../src/mediaTypes.js'; -import { config } from '../src/config.js'; +import {registerBidder} from '../src/adapters/bidderFactory.js'; +import {BANNER, VIDEO} from '../src/mediaTypes.js'; +import {config} from '../src/config.js'; +import * as utils from '../src/utils.js'; +import find from 'core-js-pure/features/array/find.js'; -const domain = 'hb.justbidit.xyz'; -const httpsPort = 8843; -const path = '/prebid'; +const ENDPOINT = `https://hb.justbidit.xyz:8843/prebid`; +const BIDDER_CODE = 'waardex'; -const ENDPOINT = `https://${domain}:${httpsPort}${path}`; +const isBidRequestValid = bid => { + if (!bid.bidId) { + utils.logError(BIDDER_CODE + ': bid.bidId should be non-empty'); + return false; + } -const BIDDER_CODE = 'waardex'; + if (!bid.params) { + utils.logError(BIDDER_CODE + ': bid.params should be non-empty'); + return false; + } -/** - * @param {Array} requestSizes - * - * @returns {Array} - * */ -function transformSizes(requestSizes) { - let sizes = []; - if ( - Array.isArray(requestSizes) && - !Array.isArray(requestSizes[0]) - ) { - sizes[0] = { - width: parseInt(requestSizes[0], 10) || 0, - height: parseInt(requestSizes[1], 10) || 0, - }; - } else if ( - Array.isArray(requestSizes) && - Array.isArray(requestSizes[0]) - ) { - sizes = requestSizes.map(item => { - return { - width: parseInt(item[0], 10) || 0, - height: parseInt(item[1], 10) || 0, - } - }); + if (!+bid.params.zoneId) { + utils.logError(BIDDER_CODE + ': bid.params.zoneId should be non-empty Number'); + return false; } - return sizes; -} -/** - * @param {Object} banner - * @param {Array} banner.sizes - * - * @returns {Object} - * */ -function createBannerObject(banner) { - return { - sizes: transformSizes(banner.sizes), - }; -} - -/** - * @param {Array} validBidRequests - * - * @returns {Object} - * */ -function buildBidRequests(validBidRequests) { - return validBidRequests.map((validBidRequest) => { - const params = validBidRequest.params; - - const item = { - bidId: validBidRequest.bidId, - bidfloor: parseFloat(params.bidfloor) || 0, - position: parseInt(params.position) || 1, - instl: parseInt(params.instl) || 0, - }; - if (validBidRequest.mediaTypes[BANNER]) { - item[BANNER] = createBannerObject(validBidRequest.mediaTypes[BANNER]); + if (bid.mediaTypes && bid.mediaTypes.video) { + if (!bid.mediaTypes.video.playerSize) { + utils.logError(BIDDER_CODE + ': bid.mediaTypes.video.playerSize should be non-empty'); + return false; + } + + if (!utils.isArray(bid.mediaTypes.video.playerSize)) { + utils.logError(BIDDER_CODE + ': bid.mediaTypes.video.playerSize should be an Array'); + return false; } - return item; - }); -} - -/** - * @param {Object} bidderRequest - * @param {String} bidderRequest.userAgent - * @param {String} bidderRequest.refererInfo - * @param {String} bidderRequest.uspConsent - * @param {Object} bidderRequest.gdprConsent - * @param {String} bidderRequest.gdprConsent.consentString - * @param {String} bidderRequest.gdprConsent.gdprApplies - * - * @returns {Object} - { - * ua: string, - * language: string, - * [referer]: string, - * [us_privacy]: string, - * [consent_string]: string, - * [consent_required]: string, - * [coppa]: boolean, - * } - * */ -function getCommonBidsData(bidderRequest) { + + if (!bid.mediaTypes.video.playerSize[0]) { + utils.logError(BIDDER_CODE + ': bid.mediaTypes.video.playerSize should be non-empty'); + return false; + } + + if (!utils.isArray(bid.mediaTypes.video.playerSize[0])) { + utils.logError(BIDDER_CODE + ': bid.mediaTypes.video.playerSize should be non-empty Array'); + return false; + } + } + + return true; +}; + +const buildRequests = (validBidRequests, bidderRequest) => { + const dataToSend = { + ...getCommonBidsData(bidderRequest), + bidRequests: getBidRequestsToSend(validBidRequests) + }; + + let zoneId = ''; + if (validBidRequests[0] && validBidRequests[0].params && +validBidRequests[0].params.zoneId) { + zoneId = +validBidRequests[0].params.zoneId; + } + + return {method: 'POST', url: `${ENDPOINT}?pubId=${zoneId}`, data: dataToSend}; +}; + +const getCommonBidsData = bidderRequest => { const payload = { ua: navigator.userAgent || '', language: navigator.language && navigator.language.indexOf('-') !== -1 ? navigator.language.split('-')[0] : '', - }; + if (bidderRequest && bidderRequest.refererInfo) { payload.referer = encodeURIComponent(bidderRequest.refererInfo.referer); } + if (bidderRequest && bidderRequest.uspConsent) { payload.us_privacy = bidderRequest.uspConsent; } + if (bidderRequest && bidderRequest.gdprConsent) { payload.gdpr_consent = { consent_string: bidderRequest.gdprConsent.consentString, consent_required: bidderRequest.gdprConsent.gdprApplies, } } + payload.coppa = !!config.getConfig('coppa'); return payload; -} - -/** - * this function checks either bid response is valid or noе - * - * @param {Object} bid - * @param {string} bid.requestId - * @param {number} bid.cpm - * @param {string} bid.creativeId - * @param {number} bid.ttl - * @param {string} bid.currency - * @param {number} bid.width - * @param {number} bid.height - * @param {string} bid.ad - * - * @returns {boolean} - * */ -function isBidValid(bid) { - if (!bid.requestId || !bid.cpm || !bid.creativeId || !bid.ttl || !bid.currency) { - return false; +}; + +const getBidRequestsToSend = validBidRequests => { + return validBidRequests.map(getBidRequestToSend); +}; + +const getBidRequestToSend = validBidRequest => { + const result = { + bidId: validBidRequest.bidId, + bidfloor: parseFloat(validBidRequest.params.bidfloor) || 0, + position: parseInt(validBidRequest.params.position) || 1, + instl: parseInt(validBidRequest.params.instl) || 0, + }; + + if (validBidRequest.mediaTypes[BANNER]) { + result[BANNER] = createBannerObject(validBidRequest.mediaTypes[BANNER]); } - return Boolean(bid.width && bid.height && bid.ad); -} - -/** - * @param {Object} serverBid - * - * @returns {Object|null} - * */ -function createBid(serverBid) { - const bid = { - requestId: serverBid.id, - cpm: serverBid.price, + if (validBidRequest.mediaTypes[VIDEO]) { + result[VIDEO] = createVideoObject(validBidRequest.mediaTypes[VIDEO], validBidRequest.params); + } + + return result; +}; + +const createBannerObject = banner => { + return { + sizes: transformSizes(banner.sizes), + }; +}; + +const transformSizes = requestSizes => { + let result = []; + + if (Array.isArray(requestSizes) && !Array.isArray(requestSizes[0])) { + result[0] = { + width: parseInt(requestSizes[0], 10) || 0, + height: parseInt(requestSizes[1], 10) || 0, + }; + } else if (Array.isArray(requestSizes) && Array.isArray(requestSizes[0])) { + result = requestSizes.map(item => { + return { + width: parseInt(item[0], 10) || 0, + height: parseInt(item[1], 10) || 0, + } + }); + } + + return result; +}; + +const createVideoObject = (videoMediaTypes, videoParams) => { + return { + w: utils.deepAccess(videoMediaTypes, 'playerSize')[0][0], + h: utils.deepAccess(videoMediaTypes, 'playerSize')[0][1], + mimes: utils.getBidIdParameter('mimes', videoParams) || ['application/javascript', 'video/mp4', 'video/webm'], + minduration: utils.getBidIdParameter('minduration', videoParams) || 0, + maxduration: utils.getBidIdParameter('maxduration', videoParams) || 500, + protocols: utils.getBidIdParameter('protocols', videoParams) || [2, 3, 5, 6], + startdelay: utils.getBidIdParameter('startdelay', videoParams) || 0, + placement: utils.getBidIdParameter('placement', videoParams) || videoMediaTypes.context === 'outstream' ? 3 : 1, + skip: utils.getBidIdParameter('skip', videoParams) || 1, + skipafter: utils.getBidIdParameter('skipafter', videoParams) || 0, + minbitrate: utils.getBidIdParameter('minbitrate', videoParams) || 0, + maxbitrate: utils.getBidIdParameter('maxbitrate', videoParams) || 3500, + delivery: utils.getBidIdParameter('delivery', videoParams) || [2], + playbackmethod: utils.getBidIdParameter('playbackmethod', videoParams) || [1, 2, 3, 4], + api: utils.getBidIdParameter('api', videoParams) || [2], + linearity: utils.getBidIdParameter('linearity', videoParams) || 1 + }; +}; + +const interpretResponse = (serverResponse, bidRequest) => { + try { + const responseBody = serverResponse.body; + + if (!responseBody.seatbid || !responseBody.seatbid[0]) { + return []; + } + + return responseBody.seatbid[0].bid + .map(openRtbBid => { + const hbRequestBid = getHbRequestBid(openRtbBid, bidRequest.data); + if (!hbRequestBid) return; + + const hbRequestMediaType = getHbRequestMediaType(hbRequestBid); + if (!hbRequestMediaType) return; + + return mapOpenRtbToHbBid(openRtbBid, hbRequestMediaType, hbRequestBid); + }) + .filter(x => x); + } catch (e) { + return []; + } +}; + +const getHbRequestBid = (openRtbBid, bidRequest) => { + return find(bidRequest.bidRequests, x => x.bidId === openRtbBid.impid); +}; + +const getHbRequestMediaType = hbRequestBid => { + if (hbRequestBid.banner) return BANNER; + if (hbRequestBid.video) return VIDEO; + return null; +}; + +const mapOpenRtbToHbBid = (openRtbBid, mediaType, hbRequestBid) => { + let bid = null; + + if (mediaType === BANNER) { + bid = mapOpenRtbBannerToHbBid(openRtbBid, hbRequestBid); + } + + if (mediaType === VIDEO) { + bid = mapOpenRtbVideoToHbBid(openRtbBid, hbRequestBid); + } + + return isBidValid(bid) ? bid : null; +}; + +const mapOpenRtbBannerToHbBid = (openRtbBid, hbRequestBid) => { + return { + mediaType: BANNER, + requestId: hbRequestBid.bidId, + cpm: openRtbBid.price, currency: 'USD', - width: serverBid.w, - height: serverBid.h, - creativeId: serverBid.crid, + width: openRtbBid.w, + height: openRtbBid.h, + creativeId: openRtbBid.crid, netRevenue: true, ttl: 3000, - ad: serverBid.adm, - dealId: serverBid.dealid, + ad: openRtbBid.adm, + dealId: openRtbBid.dealid, meta: { - cid: serverBid.cid, - adomain: serverBid.adomain, - mediaType: serverBid.ext.mediaType + cid: openRtbBid.cid, + adomain: openRtbBid.adomain, + mediaType: openRtbBid.ext && openRtbBid.ext.mediaType }, }; +}; - return isBidValid(bid) ? bid : null; -} +const mapOpenRtbVideoToHbBid = (openRtbBid, hbRequestBid) => { + return { + mediaType: VIDEO, + requestId: hbRequestBid.bidId, + cpm: openRtbBid.price, + currency: 'USD', + width: hbRequestBid.video.w, + height: hbRequestBid.video.h, + ad: openRtbBid.adm, + ttl: 3000, + creativeId: openRtbBid.crid, + netRevenue: true, + vastUrl: getVastUrl(openRtbBid), + // An impression tracking URL to serve with video Ad + // Optional; only usable with vastUrl and requires prebid cache to be enabled + // Example: "https://vid.exmpale.com/imp/134" + // For now we don't need this field + // vastImpUrl: null, + vastXml: openRtbBid.adm, + dealId: openRtbBid.dealid, + meta: { + cid: openRtbBid.cid, + adomain: openRtbBid.adomain, + networkId: null, + networkName: null, + agencyId: null, + agencyName: null, + advertiserId: null, + advertiserName: null, + advertiserDomains: null, + brandId: null, + brandName: null, + primaryCatId: null, + secondaryCatIds: null, + mediaType: 'video', + }, + } +}; -export const spec = { - code: BIDDER_CODE, - supportedMediaTypes: [BANNER], - - isBidRequestValid: (bid) => Boolean(bid.bidId && bid.params && +bid.params.zoneId), - - /** - * @param {Object[]} validBidRequests - array of valid bid requests - * @param {Object} bidderRequest - an array of valid bid requests - * - * */ - buildRequests(validBidRequests, bidderRequest) { - const payload = getCommonBidsData(bidderRequest); - payload.bidRequests = buildBidRequests(validBidRequests); - - let zoneId = ''; - if (validBidRequests[0] && validBidRequests[0].params && +validBidRequests[0].params.zoneId) { - zoneId = +validBidRequests[0].params.zoneId; - } +const getVastUrl = openRtbBid => { + const adm = (openRtbBid.adm || '').trim(); - const url = `${ENDPOINT}?pubId=${zoneId}`; + if (adm.startsWith('http')) { + return adm; + } else { + return null + } +}; - return { - method: 'POST', - url, - data: payload - }; - }, - - /** - * Unpack the response from the server into a list of bids. - */ - interpretResponse(serverResponse, bidRequest) { - const bids = []; - serverResponse = serverResponse.body; - - if (serverResponse.seatbid && serverResponse.seatbid[0]) { - const oneSeatBid = serverResponse.seatbid[0]; - oneSeatBid.bid.forEach(serverBid => { - const bid = createBid(serverBid); - if (bid) { - bids.push(bid); - } - }); - } - return bids; - }, -} +const isBidValid = bid => { + if (!bid.requestId || !bid.cpm || !bid.creativeId || !bid.ttl || !bid.currency) { + return false; + } + + return Boolean(bid.width && bid.height && bid.ad); +}; + +export const spec = { + code: BIDDER_CODE, + supportedMediaTypes: [BANNER, VIDEO], + isBidRequestValid, + buildRequests, + interpretResponse, +}; registerBidder(spec); diff --git a/modules/waardexBidAdapter.md b/modules/waardexBidAdapter.md index 44ee720d31a..8eade8409f4 100644 --- a/modules/waardexBidAdapter.md +++ b/modules/waardexBidAdapter.md @@ -10,51 +10,84 @@ Maintainer: info@prebid.org Connects to Waardex exchange for bids. -Waardex bid adapter supports Banner. +Waardex bid adapter supports Banner and Video. # Test Parameters - -``` - +## Banner +```javascript var sizes = [ [300, 250] ]; var PREBID_TIMEOUT = 5000; var FAILSAFE_TIMEOUT = 5000; -var adUnits = [{ - code: '/19968336/header-bid-tag-0', - mediaTypes: { - banner: { - sizes: sizes, - }, - }, - bids: [{ - bidder: 'waardex', - params: { - placementId: 13144370, - position: 1, // add position openrtb - bidfloor: 0.5, - instl: 0, // 1 - full screen - pubId: 1, +var adUnits = [ + { + code: '/19968336/header-bid-tag-0', + mediaTypes: { + banner: { + sizes: sizes + } + }, + bids: [ + { + bidder: 'waardex', + params: { + bidfloor: 1.5, + position: 1, + instl: 1, + zoneId: 1 + } + } + ] } - }] -},{ - code: '/19968336/header-bid-tag-1', - mediaTypes: { - banner: { - sizes: sizes, +]; +``` + +## Video + +```javascript +const PREBID_TIMEOUT = 1000; +const FAILSAFE_TIMEOUT = 3000; + +const sizes = [ + [640, 480] +]; + +const adUnits = [ + { + code: 'video1', + mediaTypes: { + video: { + context: 'instream', + playerSize: sizes[0] + } }, - }, - bids: [{ - bidder: 'waardex', - params: { - placementId: 333333333333, - position: 1, // add position openrtb - bidfloor: 0.5, - instl: 0, // 1 - full screen - pubId: 1, - } - }] -}]; + bids: [ + { + bidder: 'waardex', + params: { + bidfloor: 1.5, + position: 1, + instl: 1, + zoneId: 1, + mimes: ['video/x-ms-wmv', 'video/mp4'], + minduration: 2, + maxduration: 10, + protocols: ['VAST 1.0', 'VAST 2.0'], + startdelay: -1, + placement: 1, + skip: 1, + skipafter: 2, + minbitrate: 0, + maxbitrate: 0, + delivery: [1, 2, 3], + playbackmethod: [1, 2], + api: [1, 2, 3, 4, 5, 6], + linearity: 1, + } + } + ] + } +] ``` diff --git a/modules/yieldlabBidAdapter.js b/modules/yieldlabBidAdapter.js index 5465a10a884..9c1c54cfb2b 100644 --- a/modules/yieldlabBidAdapter.js +++ b/modules/yieldlabBidAdapter.js @@ -8,7 +8,7 @@ const ENDPOINT = 'https://ad.yieldlab.net' const BIDDER_CODE = 'yieldlab' const BID_RESPONSE_TTL_SEC = 300 const CURRENCY_CODE = 'EUR' -const OUTSTREAMPLAYER_URL = 'https://ad2.movad.net/dynamic.ad?a=o193092&ma_loadEvent=ma-start-event' +const OUTSTREAMPLAYER_URL = 'https://ad.adition.com/dynamic.ad?a=o193092&ma_loadEvent=ma-start-event' export const spec = { code: BIDDER_CODE, @@ -96,8 +96,8 @@ export const spec = { }) if (matchedBid) { - const primarysize = bidRequest.sizes.length === 2 && !utils.isArray(bidRequest.sizes[0]) ? bidRequest.sizes : bidRequest.sizes[0] - const customsize = bidRequest.params.adSize !== undefined ? parseSize(bidRequest.params.adSize) : primarysize + const adUnitSize = bidRequest.sizes.length === 2 && !utils.isArray(bidRequest.sizes[0]) ? bidRequest.sizes : bidRequest.sizes[0] + const adSize = bidRequest.params.adSize !== undefined ? parseSize(bidRequest.params.adSize) : (matchedBid.adsize !== undefined) ? parseSize(matchedBid.adsize) : adUnitSize const extId = bidRequest.params.extId !== undefined ? '&id=' + bidRequest.params.extId : '' const adType = matchedBid.adtype !== undefined ? matchedBid.adtype : '' const gdprApplies = reqParams.gdpr ? '&gdpr=' + reqParams.gdpr : '' @@ -106,15 +106,15 @@ export const spec = { const bidResponse = { requestId: bidRequest.bidId, cpm: matchedBid.price / 100, - width: customsize[0], - height: customsize[1], + width: adSize[0], + height: adSize[1], creativeId: '' + matchedBid.id, dealId: (matchedBid['c.dealid']) ? matchedBid['c.dealid'] : matchedBid.pid, currency: CURRENCY_CODE, netRevenue: false, ttl: BID_RESPONSE_TTL_SEC, referrer: '', - ad: `` + ad: `` } if (isVideo(bidRequest, adType)) { @@ -124,8 +124,7 @@ export const spec = { bidResponse.height = playersize[1] } bidResponse.mediaType = VIDEO - bidResponse.vastUrl = `${ENDPOINT}/d/${matchedBid.id}/${bidRequest.params.supplyId}/${customsize[0]}x${customsize[1]}?ts=${timestamp}${extId}${gdprApplies}${gdprConsent}` - + bidResponse.vastUrl = `${ENDPOINT}/d/${matchedBid.id}/${bidRequest.params.supplyId}/?ts=${timestamp}${extId}${gdprApplies}${gdprConsent}` if (isOutstream(bidRequest)) { const renderer = Renderer.install({ id: bidRequest.bidId, diff --git a/modules/yieldlabBidAdapter.md b/modules/yieldlabBidAdapter.md index 37897b83f12..a7a3f2715dc 100644 --- a/modules/yieldlabBidAdapter.md +++ b/modules/yieldlabBidAdapter.md @@ -21,7 +21,6 @@ Module that connects to Yieldlab's demand sources params: { adslotId: "5220336", supplyId: "1381604", - adSize: "728x90", targeting: { key1: "value1", key2: "value2" @@ -41,8 +40,7 @@ Module that connects to Yieldlab's demand sources bidder: "yieldlab", params: { adslotId: "5220339", - supplyId: "1381604", - adSize: "640x480" + supplyId: "1381604" } }] } diff --git a/modules/yieldmoBidAdapter.js b/modules/yieldmoBidAdapter.js index 05af0bf0d66..c1151617ac4 100644 --- a/modules/yieldmoBidAdapter.js +++ b/modules/yieldmoBidAdapter.js @@ -2,6 +2,7 @@ import * as utils from '../src/utils.js'; import { BANNER, VIDEO } from '../src/mediaTypes.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; import includes from 'core-js-pure/features/array/includes'; +import find from 'core-js-pure/features/array/find.js'; const BIDDER_CODE = 'yieldmo'; const CURRENCY = 'USD'; @@ -197,7 +198,7 @@ function createNewBannerBid(response) { * @param bidRequest server request */ function createNewVideoBid(response, bidRequest) { - const imp = (utils.deepAccess(bidRequest, 'data.imp') || []).find(imp => imp.id === response.impid); + const imp = find((utils.deepAccess(bidRequest, 'data.imp') || []), imp => imp.id === response.impid); return { requestId: imp.id, cpm: response.price, diff --git a/modules/yuktamediaAnalyticsAdapter.js b/modules/yuktamediaAnalyticsAdapter.js index caca3bf3341..2ef2d251ace 100644 --- a/modules/yuktamediaAnalyticsAdapter.js +++ b/modules/yuktamediaAnalyticsAdapter.js @@ -5,6 +5,7 @@ import CONSTANTS from '../src/constants.json'; import * as utils from '../src/utils.js'; import { getStorageManager } from '../src/storageManager.js'; import { getRefererInfo } from '../src/refererDetection.js'; +import strIncludes from 'core-js-pure/features/string/includes.js'; const storage = getStorageManager(); const yuktamediaAnalyticsVersion = 'v3.1.0'; @@ -152,7 +153,7 @@ var yuktamediaAnalyticsAdapter = Object.assign(adapter({ analyticsType: 'endpoin bidResponse.responseTimestamp = args.responseTimestamp; bidResponse.bidForSize = args.size; for (const [adserverTargetingKey, adserverTargetingValue] of Object.entries(args.adserverTargeting)) { - if (['body', 'icon', 'image', 'linkurl', 'host', 'path'].every((ele) => !adserverTargetingKey.includes(ele))) { + if (['body', 'icon', 'image', 'linkurl', 'host', 'path'].every((ele) => !strIncludes(adserverTargetingKey, ele))) { bidResponse['adserverTargeting-' + adserverTargetingKey] = adserverTargetingValue; } } diff --git a/modules/zemantaBidAdapter.js b/modules/zemantaBidAdapter.js new file mode 100644 index 00000000000..d0ac64e48e4 --- /dev/null +++ b/modules/zemantaBidAdapter.js @@ -0,0 +1,257 @@ +// jshint esversion: 6, es3: false, node: true +'use strict'; + +import { + registerBidder +} from '../src/adapters/bidderFactory.js'; +import { NATIVE, BANNER } from '../src/mediaTypes.js'; +import * as utils from '../src/utils.js'; +import { ajax } from '../src/ajax.js'; +import { config } from '../src/config.js'; + +const BIDDER_CODE = 'zemanta'; +const CURRENCY = 'USD'; +const NATIVE_ASSET_IDS = { 0: 'title', 2: 'icon', 3: 'image', 5: 'sponsoredBy', 4: 'body', 1: 'cta' }; +const NATIVE_PARAMS = { + title: { id: 0, name: 'title' }, + icon: { id: 2, type: 1, name: 'img' }, + image: { id: 3, type: 3, name: 'img' }, + sponsoredBy: { id: 5, name: 'data', type: 1 }, + body: { id: 4, name: 'data', type: 2 }, + cta: { id: 1, type: 12, name: 'data' } +}; + +export const spec = { + code: BIDDER_CODE, + aliases: ['outbrain'], + supportedMediaTypes: [ NATIVE, BANNER ], + isBidRequestValid: (bid) => { + return ( + (!!config.getConfig('zemanta.bidderUrl') || !!config.getConfig('outbrain.bidderUrl')) && + !!utils.deepAccess(bid, 'params.publisher.id') && + !!(bid.nativeParams || bid.sizes) + ); + }, + buildRequests: (validBidRequests, bidderRequest) => { + const page = bidderRequest.refererInfo.referer; + const ua = navigator.userAgent; + const test = setOnAny(validBidRequests, 'params.test'); + const publisher = setOnAny(validBidRequests, 'params.publisher'); + const cur = CURRENCY; + const endpointUrl = config.getConfig('zemanta.bidderUrl') || config.getConfig('outbrain.bidderUrl'); + const timeout = bidderRequest.timeout; + + const imps = validBidRequests.map((bid, id) => { + bid.netRevenue = 'net'; + const imp = { + id: id + 1 + '' + } + + if (bid.params.tagid) { + imp.tagid = bid.params.tagid + } + + if (bid.nativeParams) { + imp.native = { + request: JSON.stringify({ + assets: getNativeAssets(bid) + }) + } + } else { + imp.banner = { + format: transformSizes(bid.sizes) + } + } + + return imp; + }); + + const request = { + id: bidderRequest.auctionId, + site: { page, publisher }, + device: { ua }, + source: { fd: 1 }, + cur: [cur], + tmax: timeout, + imp: imps + }; + + if (test) { + request.is_debug = !!test; + request.test = 1; + } + + if (utils.deepAccess(bidderRequest, 'gdprConsent.gdprApplies')) { + utils.deepSetValue(request, 'user.ext.consent', bidderRequest.gdprConsent.consentString) + utils.deepSetValue(request, 'regs.ext.gdpr', bidderRequest.gdprConsent.gdprApplies & 1) + } + if (bidderRequest.uspConsent) { + utils.deepSetValue(request, 'regs.ext.us_privacy', bidderRequest.uspConsent) + } + if (config.getConfig('coppa') === true) { + utils.deepSetValue(request, 'regs.coppa', config.getConfig('coppa') & 1) + } + + return { + method: 'POST', + url: endpointUrl, + data: JSON.stringify(request), + bids: validBidRequests + }; + }, + interpretResponse: (serverResponse, { bids }) => { + if (!serverResponse.body) { + return []; + } + const { seatbid, cur } = serverResponse.body; + + const bidResponses = flatten(seatbid.map(seat => seat.bid)).reduce((result, bid) => { + result[bid.impid - 1] = bid; + return result; + }, []); + + return bids.map((bid, id) => { + const bidResponse = bidResponses[id]; + if (bidResponse) { + const type = bid.nativeParams ? NATIVE : BANNER; + const bidObject = { + requestId: bid.bidId, + cpm: bidResponse.price, + creativeId: bidResponse.crid, + ttl: 360, + netRevenue: bid.netRevenue === 'net', + currency: cur, + mediaType: type, + nurl: bidResponse.nurl, + }; + if (type === NATIVE) { + bidObject.native = parseNative(bidResponse); + } else { + bidObject.ad = bidResponse.adm; + bidObject.width = bidResponse.w; + bidObject.height = bidResponse.h; + } + return bidObject; + } + }).filter(Boolean); + }, + getUserSyncs: (syncOptions) => { + const syncs = []; + const syncUrl = config.getConfig('zemanta.usersyncUrl') || config.getConfig('outbrain.usersyncUrl'); + if (syncOptions.pixelEnabled && syncUrl) { + syncs.push({ + type: 'image', + url: syncUrl + }); + } + return syncs; + }, + onBidWon: (bid) => { + ajax(utils.replaceAuctionPrice(bid.nurl, bid.originalCpm)) + } +}; + +registerBidder(spec); + +function parseNative(bid) { + const { assets, link, eventtrackers } = JSON.parse(bid.adm); + const result = { + clickUrl: link.url, + clickTrackers: link.clicktrackers || undefined + }; + assets.forEach(asset => { + const kind = NATIVE_ASSET_IDS[asset.id]; + const content = kind && asset[NATIVE_PARAMS[kind].name]; + if (content) { + result[kind] = content.text || content.value || { url: content.url, width: content.w, height: content.h }; + } + }); + if (eventtrackers) { + result.impressionTrackers = []; + eventtrackers.forEach(tracker => { + if (tracker.event !== 1) return; + switch (tracker.method) { + case 1: // img + result.impressionTrackers.push(tracker.url); + break; + case 2: // js + result.javascriptTrackers = ``; + break; + } + }); + } + return result; +} + +function setOnAny(collection, key) { + for (let i = 0, result; i < collection.length; i++) { + result = utils.deepAccess(collection[i], key); + if (result) { + return result; + } + } +} + +function flatten(arr) { + return [].concat(...arr); +} + +function getNativeAssets(bid) { + return utils._map(bid.nativeParams, (bidParams, key) => { + const props = NATIVE_PARAMS[key]; + const asset = { + required: bidParams.required & 1, + }; + if (props) { + asset.id = props.id; + let wmin, hmin, w, h; + let aRatios = bidParams.aspect_ratios; + + if (aRatios && aRatios[0]) { + aRatios = aRatios[0]; + wmin = aRatios.min_width || 0; + hmin = aRatios.ratio_height * wmin / aRatios.ratio_width | 0; + } + + if (bidParams.sizes) { + const sizes = flatten(bidParams.sizes); + w = sizes[0]; + h = sizes[1]; + } + + asset[props.name] = { + len: bidParams.len, + type: props.type, + wmin, + hmin, + w, + h + }; + + return asset; + } + }).filter(Boolean); +} + +/* Turn bid request sizes into ut-compatible format */ +function transformSizes(requestSizes) { + if (!utils.isArray(requestSizes)) { + return []; + } + + if (requestSizes.length === 2 && !utils.isArray(requestSizes[0])) { + return [{ + w: parseInt(requestSizes[0], 10), + h: parseInt(requestSizes[1], 10) + }]; + } else if (utils.isArray(requestSizes[0])) { + return requestSizes.map(item => + ({ + w: parseInt(item[0], 10), + h: parseInt(item[1], 10) + }) + ); + } + + return []; +} diff --git a/modules/zemantaBidAdapter.md b/modules/zemantaBidAdapter.md new file mode 100644 index 00000000000..d991b67d429 --- /dev/null +++ b/modules/zemantaBidAdapter.md @@ -0,0 +1,107 @@ +# Overview + +``` +Module Name: Zemanta Adapter +Module Type: Bidder Adapter +Maintainer: prog-ops-team@outbrain.com +``` + +# Description + +Module that connects to zemanta bidder to fetch bids. +Both native and display formats are supported but not at the same time. Using OpenRTB standard. + +# Configuration + +## Bidder and usersync URLs + +The Zemanta adapter does not work without setting the correct bidder and usersync URLs. +You will receive the URLs when contacting us. + +``` +pbjs.setConfig({ + zemanta: { + bidderUrl: 'https://bidder-url.com', + usersyncUrl: 'https://usersync-url.com' + } +}); +``` + + +# Test Native Parameters +``` + var adUnits = [ + code: '/19968336/prebid_native_example_1', + mediaTypes: { + native: { + image: { + required: false, + sizes: [100, 50] + }, + title: { + required: false, + len: 140 + }, + sponsoredBy: { + required: false + }, + clickUrl: { + required: false + }, + body: { + required: false + }, + icon: { + required: false, + sizes: [50, 50] + } + } + }, + bids: [{ + bidder: 'zemanta', + params: { + publisher: { + id: '2706', // required + name: 'Publishers Name', + domain: 'publisher.com' + }, + tagid: 'tag-id' + } + }] + ]; + + pbjs.setConfig({ + zemanta: { + bidderUrl: 'https://prebidtest.zemanta.com/api/bidder/prebidtest/bid/' + } + }); +``` + +# Test Display Parameters +``` + var adUnits = [ + code: '/19968336/prebid_display_example_1', + mediaTypes: { + banner: { + sizes: [[300, 250]] + } + }, + bids: [{ + bidder: 'zemanta', + params: { + publisher: { + id: '2706', // required + name: 'Publishers Name', + domain: 'publisher.com' + }, + tagid: 'tag-id' + }, + }] + ]; + + pbjs.setConfig({ + zemanta: { + bidderUrl: 'https://prebidtest.zemanta.com/api/bidder/prebidtest/bid/' + } + }); +``` diff --git a/modules/zeotapIdPlusIdSystem.js b/modules/zeotapIdPlusIdSystem.js index d800286b00e..8f26cc827d6 100644 --- a/modules/zeotapIdPlusIdSystem.js +++ b/modules/zeotapIdPlusIdSystem.js @@ -9,23 +9,35 @@ import {submodule} from '../src/hook.js'; import { getStorageManager } from '../src/storageManager.js'; const ZEOTAP_COOKIE_NAME = 'IDP'; -export const storage = getStorageManager(); +const ZEOTAP_VENDOR_ID = 301; +const ZEOTAP_MODULE_NAME = 'zeotapIdPlus'; function readCookie() { - return storage.cookiesAreEnabled ? storage.getCookie(ZEOTAP_COOKIE_NAME) : null; + return storage.cookiesAreEnabled() ? storage.getCookie(ZEOTAP_COOKIE_NAME) : null; } function readFromLocalStorage() { - return storage.localStorageIsEnabled ? storage.getDataFromLocalStorage(ZEOTAP_COOKIE_NAME) : null; + return storage.localStorageIsEnabled() ? storage.getDataFromLocalStorage(ZEOTAP_COOKIE_NAME) : null; } +export function getStorage() { + return getStorageManager(ZEOTAP_VENDOR_ID, ZEOTAP_MODULE_NAME); +} + +export const storage = getStorage(); + /** @type {Submodule} */ export const zeotapIdPlusSubmodule = { /** * used to link submodule with config * @type {string} */ - name: 'zeotapIdPlus', + name: ZEOTAP_MODULE_NAME, + /** + * Vendor ID of Zeotap + * @type {Number} + */ + gvlid: ZEOTAP_VENDOR_ID, /** * decode the stored id value for passing to bid requests * @function diff --git a/modules/zetaBidAdapter.js b/modules/zetaBidAdapter.js index f60e8946799..09d631b3d18 100644 --- a/modules/zetaBidAdapter.js +++ b/modules/zetaBidAdapter.js @@ -20,19 +20,37 @@ export const spec = { */ isBidRequestValid: function(bid) { // check for all required bid fields - let isValid = !!( - bid && - bid.bidId && - bid.params && - bid.params.ip && - bid.params.user && - bid.params.user.buyeruid && - bid.params.definerId - ); - if (!isValid) { - utils.logWarn('Invalid bid request'); + if (!(bid && + bid.bidId && + bid.params)) { + utils.logWarn('Invalid bid request - missing required bid data'); + return false; } - return isValid; + + if (!(bid.params.user && + bid.params.user.buyeruid)) { + utils.logWarn('Invalid bid request - missing required user data'); + return false; + } + + if (!(bid.params.device && + bid.params.device.ip)) { + utils.logWarn('Invalid bid request - missing required device data'); + return false; + } + + if (!(bid.params.device.geo && + bid.params.device.geo.country)) { + utils.logWarn('Invalid bid request - missing required geo data'); + return false; + } + + if (!bid.params.definerId) { + utils.logWarn('Invalid bid request - missing required definer data'); + return false; + } + + return true; }, /** @@ -51,27 +69,23 @@ export const spec = { secure: secure, banner: buildBanner(request) }; - let isMobile = /(ios|ipod|ipad|iphone|android)/i.test(navigator.userAgent) ? 1 : 0; let payload = { id: bidderRequest.auctionId, cur: [DEFAULT_CUR], imp: [impData], - site: { - mobile: isMobile, - page: bidderRequest.refererInfo.referer - }, - device: { - ua: navigator.userAgent, - ip: params.ip - }, - user: { - buyeruid: params.user.buyeruid, - uid: params.user.uid - }, + site: params.site ? params.site : {}, + device: params.device ? params.device : {}, + user: params.user ? params.user : {}, + app: params.app ? params.app : {}, ext: { definerId: params.definerId } }; + + payload.device.ua = navigator.userAgent; + payload.site.page = bidderRequest.refererInfo.referer; + payload.site.mobile = /(ios|ipod|ipad|iphone|android)/i.test(navigator.userAgent) ? 1 : 0; + if (params.test) { payload.test = params.test; } diff --git a/modules/zetaBidAdapter.md b/modules/zetaBidAdapter.md index ce19b831d4d..d99846c7822 100644 --- a/modules/zetaBidAdapter.md +++ b/modules/zetaBidAdapter.md @@ -29,7 +29,12 @@ Module that connects to Zeta's demand sources uid: 12345, buyeruid: 12345 }, - ip: '111.222.33.44', + device: { + ip: '111.222.33.44', + geo: { + country: "USA" + } + }, definerId: 1, test: 1 } diff --git a/package-lock.json b/package-lock.json index bf95487ed9d..330298cbaac 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "prebid.js", - "version": "4.14.0-pre", + "version": "4.15.0-pre", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -1920,11 +1920,6 @@ "schema-utils": "^2.7.0" } }, - "@kiosked/ulid": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@kiosked/ulid/-/ulid-3.0.0.tgz", - "integrity": "sha512-ZKt2KIgGHDaGfKt6FjYvCpDvBXZRRoE8b+wDOlAV76aXKpq6ITiSUnPYevR4y55NKDnwCvwOrjWe+aVOCAK8kQ==" - }, "@sindresorhus/is": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-3.0.0.tgz", @@ -2771,7 +2766,8 @@ "abab": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.3.tgz", - "integrity": "sha512-tsFzPpcttalNjFBCFMqsKYQcWxxen1pgJR56by//QwvJc4/OUS3kPOOttx2tSIfjsylB0pYu7f5D3K1RCxUnUg==" + "integrity": "sha512-tsFzPpcttalNjFBCFMqsKYQcWxxen1pgJR56by//QwvJc4/OUS3kPOOttx2tSIfjsylB0pYu7f5D3K1RCxUnUg==", + "dev": true }, "abbrev": { "version": "1.0.9", @@ -5069,11 +5065,6 @@ "integrity": "sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8=", "dev": true }, - "browser-cookies": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/browser-cookies/-/browser-cookies-1.2.0.tgz", - "integrity": "sha1-/KP/ubamOq3E2MCZnGtX0Pp9KbU=" - }, "browser-process-hrtime": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/browser-process-hrtime/-/browser-process-hrtime-1.0.0.tgz", @@ -15727,13 +15718,10 @@ "dev": true }, "live-connect-js": { - "version": "1.1.23", - "resolved": "https://registry.npmjs.org/live-connect-js/-/live-connect-js-1.1.23.tgz", - "integrity": "sha512-alOXlYyDdMXt8zzCIs3+iCrdi6r/69c7YRN3sMETa3b2cCOxep3i9j2O0iepk2hxT5JxiR1MvqlqdWAL9d2Hcg==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/live-connect-js/-/live-connect-js-2.0.0.tgz", + "integrity": "sha512-Xhrj1JU5LoLjJuujjTlvDfc/n3Shzk2hPlYmLdCx/lsltFFVuCFa9uM8u5mcHlmOUKP5pu9I54bAITxZBMHoXg==", "requires": { - "@kiosked/ulid": "^3.0.0", - "abab": "^2.0.3", - "browser-cookies": "^1.2.0", "tiny-hashes": "1.0.1" } }, diff --git a/package.json b/package.json index ed4514cee42..e992a108c5c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "prebid.js", - "version": "4.19.0-pre", + "version": "4.29.0-pre", "description": "Header Bidding Management Library", "main": "src/prebid.js", "scripts": { @@ -112,6 +112,6 @@ "fun-hooks": "^0.9.9", "jsencrypt": "^3.0.0-rc.1", "just-clone": "^1.0.2", - "live-connect-js": "^1.1.23" + "live-connect-js": "2.0.0" } } diff --git a/src/adapterManager.js b/src/adapterManager.js index 5124ae99694..cb84607e130 100644 --- a/src/adapterManager.js +++ b/src/adapterManager.js @@ -22,9 +22,11 @@ let adapterManager = {}; let _bidderRegistry = adapterManager.bidderRegistry = {}; let _aliasRegistry = adapterManager.aliasRegistry = {}; -let _s2sConfig = {}; +let _s2sConfigs = []; config.getConfig('s2sConfig', config => { - _s2sConfig = config.s2sConfig; + if (config && config.s2sConfig) { + _s2sConfigs = Array.isArray(config.s2sConfig) ? config.s2sConfig : [config.s2sConfig]; + } }); var _analyticsRegistry = {}; @@ -118,15 +120,15 @@ function getBids({bidderCode, auctionId, bidderRequestId, adUnits, labels, src}) const hookedGetBids = hook('sync', getBids, 'getBids'); -function getAdUnitCopyForPrebidServer(adUnits) { - let adaptersServerSide = _s2sConfig.bidders; +function getAdUnitCopyForPrebidServer(adUnits, s2sConfig) { + let adaptersServerSide = s2sConfig.bidders; let adUnitsCopy = utils.deepClone(adUnits); adUnitsCopy.forEach((adUnit) => { // filter out client side bids adUnit.bids = adUnit.bids.filter((bid) => { return includes(adaptersServerSide, bid.bidder) && - (!doingS2STesting() || bid.finalSource !== s2sTestingModule.CLIENT); + (!doingS2STesting(s2sConfig) || bid.finalSource !== s2sTestingModule.CLIENT); }).map((bid) => { bid.bid_id = utils.getUniqueIdentifierStr(); return bid; @@ -145,7 +147,7 @@ function getAdUnitCopyForClientAdapters(adUnits) { // filter out s2s bids adUnitsClientCopy.forEach((adUnit) => { adUnit.bids = adUnit.bids.filter((bid) => { - return !doingS2STesting() || bid.finalSource !== s2sTestingModule.SERVER; + return !clientTestAdapters.length || bid.finalSource !== s2sTestingModule.SERVER; }) }); @@ -177,6 +179,21 @@ export let uspDataHandler = { } }; +// export for testing +export let clientTestAdapters = []; +export const allS2SBidders = []; + +export function getAllS2SBidders() { + adapterManager.s2STestingEnabled = false; + _s2sConfigs.forEach(s2sConfig => { + if (s2sConfig && s2sConfig.enabled) { + if (s2sConfig.bidders && s2sConfig.bidders.length) { + allS2SBidders.push(...s2sConfig.bidders); + } + } + }) +} + adapterManager.makeBidRequests = hook('sync', function (adUnits, auctionStart, auctionId, cbTimeout, labels) { /** * emit and pass adunits for external modification @@ -184,8 +201,6 @@ adapterManager.makeBidRequests = hook('sync', function (adUnits, auctionStart, a */ events.emit(CONSTANTS.EVENTS.BEFORE_REQUEST_BIDS, adUnits); - let bidRequests = []; - let bidderCodes = getBidderCodes(adUnits); if (config.getConfig('bidderSequence') === RANDOM) { bidderCodes = shuffle(bidderCodes); @@ -193,70 +208,86 @@ adapterManager.makeBidRequests = hook('sync', function (adUnits, auctionStart, a const refererInfo = getRefererInfo(); let clientBidderCodes = bidderCodes; - let clientTestAdapters = []; - - if (_s2sConfig.enabled) { - // if s2sConfig.bidderControl testing is turned on - if (doingS2STesting()) { - // get all adapters doing client testing - const bidderMap = s2sTestingModule.getSourceBidderMap(adUnits); - clientTestAdapters = bidderMap[s2sTestingModule.CLIENT]; - } - - // these are called on the s2s adapter - let adaptersServerSide = _s2sConfig.bidders; - // don't call these client side (unless client request is needed for testing) - clientBidderCodes = bidderCodes.filter(elm => - !includes(adaptersServerSide, elm) || includes(clientTestAdapters, elm) - ); + let bidRequests = []; - const adUnitsContainServerRequests = adUnits => Boolean( - find(adUnits, adUnit => find(adUnit.bids, bid => ( - bid.bidSource || - (_s2sConfig.bidderControl && _s2sConfig.bidderControl[bid.bidder]) - ) && bid.finalSource === s2sTestingModule.SERVER)) - ); + if (allS2SBidders.length === 0) { + getAllS2SBidders(); + } - if (isTestingServerOnly() && adUnitsContainServerRequests(adUnits)) { - clientBidderCodes.length = 0; + _s2sConfigs.forEach(s2sConfig => { + if (s2sConfig && s2sConfig.enabled) { + if (doingS2STesting(s2sConfig)) { + s2sTestingModule.calculateBidSources(s2sConfig); + const bidderMap = s2sTestingModule.getSourceBidderMap(adUnits, allS2SBidders); + // get all adapters doing client testing + bidderMap[s2sTestingModule.CLIENT].forEach(bidder => { + if (!includes(clientTestAdapters, bidder)) { + clientTestAdapters.push(bidder); + } + }) + } } + }) - let adUnitsS2SCopy = getAdUnitCopyForPrebidServer(adUnits); - let tid = utils.generateUUID(); - adaptersServerSide.forEach(bidderCode => { - const bidderRequestId = utils.getUniqueIdentifierStr(); - const bidderRequest = { - bidderCode, - auctionId, - bidderRequestId, - tid, - bids: hookedGetBids({bidderCode, auctionId, bidderRequestId, 'adUnits': utils.deepClone(adUnitsS2SCopy), labels, src: CONSTANTS.S2S.SRC}), - auctionStart: auctionStart, - timeout: _s2sConfig.timeout, - src: CONSTANTS.S2S.SRC, - refererInfo - }; - if (bidderRequest.bids.length !== 0) { - bidRequests.push(bidderRequest); + // don't call these client side (unless client request is needed for testing) + clientBidderCodes = bidderCodes.filter(bidderCode => { + return !includes(allS2SBidders, bidderCode) || includes(clientTestAdapters, bidderCode) + }); + + // these are called on the s2s adapter + let adaptersServerSide = allS2SBidders; + + const adUnitsContainServerRequests = (adUnits, s2sConfig) => Boolean( + find(adUnits, adUnit => find(adUnit.bids, bid => ( + bid.bidSource || + (s2sConfig.bidderControl && s2sConfig.bidderControl[bid.bidder]) + ) && bid.finalSource === s2sTestingModule.SERVER)) + ); + + _s2sConfigs.forEach(s2sConfig => { + if (s2sConfig && s2sConfig.enabled) { + if ((isTestingServerOnly(s2sConfig) && adUnitsContainServerRequests(adUnits, s2sConfig))) { + utils.logWarn('testServerOnly: True. All client requests will be suppressed.'); + clientBidderCodes.length = 0; } - }); - // update the s2sAdUnits object and remove all bids that didn't pass sizeConfig/label checks from getBids() - // this is to keep consistency and only allow bids/adunits that passed the checks to go to pbs - adUnitsS2SCopy.forEach((adUnitCopy) => { - let validBids = adUnitCopy.bids.filter((adUnitBid) => { - return find(bidRequests, request => { - return find(request.bids, (reqBid) => reqBid.bidId === adUnitBid.bid_id); - }); + let adUnitsS2SCopy = getAdUnitCopyForPrebidServer(adUnits, s2sConfig); + let tid = utils.generateUUID(); + adaptersServerSide.forEach(bidderCode => { + const bidderRequestId = utils.getUniqueIdentifierStr(); + const bidderRequest = { + bidderCode, + auctionId, + bidderRequestId, + tid, + bids: hookedGetBids({bidderCode, auctionId, bidderRequestId, 'adUnits': utils.deepClone(adUnitsS2SCopy), labels, src: CONSTANTS.S2S.SRC}), + auctionStart: auctionStart, + timeout: s2sConfig.timeout, + src: CONSTANTS.S2S.SRC, + refererInfo + }; + if (bidderRequest.bids.length !== 0) { + bidRequests.push(bidderRequest); + } }); - adUnitCopy.bids = validBids; - }); - bidRequests.forEach(request => { - request.adUnitsS2SCopy = adUnitsS2SCopy.filter(adUnitCopy => adUnitCopy.bids.length > 0); - }); - } + // update the s2sAdUnits object and remove all bids that didn't pass sizeConfig/label checks from getBids() + // this is to keep consistency and only allow bids/adunits that passed the checks to go to pbs + adUnitsS2SCopy.forEach((adUnitCopy) => { + let validBids = adUnitCopy.bids.filter((adUnitBid) => + find(bidRequests, request => + find(request.bids, (reqBid) => reqBid.bidId === adUnitBid.bid_id))); + adUnitCopy.bids = validBids; + }); + + bidRequests.forEach(request => { + if (request.adUnitsS2SCopy === undefined) { + request.adUnitsS2SCopy = adUnitsS2SCopy.filter(adUnitCopy => adUnitCopy.bids.length > 0); + } + }); + } + }) // client adapters let adUnitsClientCopy = getAdUnitCopyForClientAdapters(adUnits); @@ -306,56 +337,74 @@ adapterManager.callBids = (adUnits, bidRequests, addBidResponse, doneCb, request return partitions; }, [[], []]); - if (serverBidRequests.length) { - // s2s should get the same client side timeout as other client side requests. - const s2sAjax = ajaxBuilder(requestBidsTimeout, requestCallbacks ? { - request: requestCallbacks.request.bind(null, 's2s'), - done: requestCallbacks.done - } : undefined); - let adaptersServerSide = _s2sConfig.bidders; - const s2sAdapter = _bidderRegistry[_s2sConfig.adapter]; - let tid = serverBidRequests[0].tid; - let adUnitsS2SCopy = serverBidRequests[0].adUnitsS2SCopy; - - if (s2sAdapter) { - let s2sBidRequest = {tid, 'ad_units': adUnitsS2SCopy}; - if (s2sBidRequest.ad_units.length) { - let doneCbs = serverBidRequests.map(bidRequest => { - bidRequest.start = timestamp(); - return doneCb.bind(bidRequest); - }); - - // only log adapters that actually have adUnit bids - let allBidders = s2sBidRequest.ad_units.reduce((adapters, adUnit) => { - return adapters.concat((adUnit.bids || []).reduce((adapters, bid) => { return adapters.concat(bid.bidder) }, [])); - }, []); - utils.logMessage(`CALLING S2S HEADER BIDDERS ==== ${adaptersServerSide.filter(adapter => { - return includes(allBidders, adapter); - }).join(',')}`); - - // fire BID_REQUESTED event for each s2s bidRequest - serverBidRequests.forEach(bidRequest => { - events.emit(CONSTANTS.EVENTS.BID_REQUESTED, bidRequest); - }); + var uniqueServerBidRequests = []; + serverBidRequests.forEach(serverBidRequest => { + var index = -1; + for (var i = 0; i < uniqueServerBidRequests.length; ++i) { + if (serverBidRequest.tid === uniqueServerBidRequests[i].tid) { + index = i; + break; + } + } + if (index <= -1) { + uniqueServerBidRequests.push(serverBidRequest); + } + }); - // make bid requests - s2sAdapter.callBids( - s2sBidRequest, - serverBidRequests, - function(adUnitCode, bid) { - let bidderRequest = getBidderRequest(serverBidRequests, bid.bidderCode, adUnitCode); - if (bidderRequest) { - addBidResponse.call(bidderRequest, adUnitCode, bid) - } - }, - () => doneCbs.forEach(done => done()), - s2sAjax - ); + let counter = 0 + _s2sConfigs.forEach((s2sConfig) => { + if (s2sConfig && uniqueServerBidRequests[counter] && includes(s2sConfig.bidders, uniqueServerBidRequests[counter].bidderCode)) { + // s2s should get the same client side timeout as other client side requests. + const s2sAjax = ajaxBuilder(requestBidsTimeout, requestCallbacks ? { + request: requestCallbacks.request.bind(null, 's2s'), + done: requestCallbacks.done + } : undefined); + let adaptersServerSide = s2sConfig.bidders; + const s2sAdapter = _bidderRegistry[s2sConfig.adapter]; + let tid = uniqueServerBidRequests[counter].tid; + let adUnitsS2SCopy = uniqueServerBidRequests[counter].adUnitsS2SCopy; + + let uniqueServerRequests = serverBidRequests.filter(serverBidRequest => serverBidRequest.tid === tid) + + if (s2sAdapter) { + let s2sBidRequest = {tid, 'ad_units': adUnitsS2SCopy, s2sConfig}; + if (s2sBidRequest.ad_units.length) { + let doneCbs = uniqueServerRequests.map(bidRequest => { + bidRequest.start = timestamp(); + return doneCb.bind(bidRequest); + }); + + // only log adapters that actually have adUnit bids + let allBidders = s2sBidRequest.ad_units.reduce((adapters, adUnit) => { + return adapters.concat((adUnit.bids || []).reduce((adapters, bid) => adapters.concat(bid.bidder), [])); + }, []); + utils.logMessage(`CALLING S2S HEADER BIDDERS ==== ${adaptersServerSide.filter(adapter => includes(allBidders, adapter)).join(',')}`); + + // fire BID_REQUESTED event for each s2s bidRequest + uniqueServerRequests.forEach(bidRequest => { + events.emit(CONSTANTS.EVENTS.BID_REQUESTED, bidRequest); + }); + + // make bid requests + s2sAdapter.callBids( + s2sBidRequest, + serverBidRequests, + (adUnitCode, bid) => { + let bidderRequest = getBidderRequest(serverBidRequests, bid.bidderCode, adUnitCode); + if (bidderRequest) { + addBidResponse.call(bidderRequest, adUnitCode, bid) + } + }, + () => doneCbs.forEach(done => done()), + s2sAjax + ); + } + } else { + utils.logError('missing ' + s2sConfig.adapter); } - } else { - utils.logError('missing ' + _s2sConfig.adapter); + counter++ } - } + }); // handle client adapter requests clientBidRequests.forEach(bidRequest => { @@ -390,27 +439,27 @@ adapterManager.callBids = (adUnits, bidRequests, addBidResponse, doneCb, request }); }; -function doingS2STesting() { - return _s2sConfig && _s2sConfig.enabled && _s2sConfig.testing && s2sTestingModule; +function doingS2STesting(s2sConfig) { + return s2sConfig && s2sConfig.enabled && s2sConfig.testing && s2sTestingModule; } -function isTestingServerOnly() { - return Boolean(doingS2STesting() && _s2sConfig.testServerOnly); +function isTestingServerOnly(s2sConfig) { + return Boolean(doingS2STesting(s2sConfig) && s2sConfig.testServerOnly); }; function getSupportedMediaTypes(bidderCode) { - let result = []; - if (includes(adapterManager.videoAdapters, bidderCode)) result.push('video'); - if (includes(nativeAdapters, bidderCode)) result.push('native'); - return result; + let supportedMediaTypes = []; + if (includes(adapterManager.videoAdapters, bidderCode)) supportedMediaTypes.push('video'); + if (includes(nativeAdapters, bidderCode)) supportedMediaTypes.push('native'); + return supportedMediaTypes; } adapterManager.videoAdapters = []; // added by adapterLoader for now -adapterManager.registerBidAdapter = function (bidAdaptor, bidderCode, {supportedMediaTypes = []} = {}) { - if (bidAdaptor && bidderCode) { - if (typeof bidAdaptor.callBids === 'function') { - _bidderRegistry[bidderCode] = bidAdaptor; +adapterManager.registerBidAdapter = function (bidAdapter, bidderCode, {supportedMediaTypes = []} = {}) { + if (bidAdapter && bidderCode) { + if (typeof bidAdapter.callBids === 'function') { + _bidderRegistry[bidderCode] = bidAdapter; if (includes(supportedMediaTypes, 'video')) { adapterManager.videoAdapters.push(bidderCode); @@ -422,7 +471,7 @@ adapterManager.registerBidAdapter = function (bidAdaptor, bidderCode, {supported utils.logError('Bidder adaptor error for bidder code: ' + bidderCode + 'bidder must implement a callBids() function'); } } else { - utils.logError('bidAdaptor or bidderCode not specified'); + utils.logError('bidAdapter or bidderCode not specified'); } }; @@ -430,30 +479,37 @@ adapterManager.aliasBidAdapter = function (bidderCode, alias, options) { let existingAlias = _bidderRegistry[alias]; if (typeof existingAlias === 'undefined') { - let bidAdaptor = _bidderRegistry[bidderCode]; - if (typeof bidAdaptor === 'undefined') { + let bidAdapter = _bidderRegistry[bidderCode]; + if (typeof bidAdapter === 'undefined') { // check if alias is part of s2sConfig and allow them to register if so (as base bidder may be s2s-only) - const s2sConfig = config.getConfig('s2sConfig'); - const s2sBidders = s2sConfig && s2sConfig.bidders; - - if (!(s2sBidders && includes(s2sBidders, alias))) { + const nonS2SAlias = []; + _s2sConfigs.forEach(s2sConfig => { + if (s2sConfig.bidders && s2sConfig.bidders.length) { + const s2sBidders = s2sConfig && s2sConfig.bidders; + if (!(s2sConfig && includes(s2sBidders, alias))) { + nonS2SAlias.push(bidderCode); + } else { + _aliasRegistry[alias] = bidderCode; + } + } + }); + nonS2SAlias.forEach(bidderCode => { utils.logError('bidderCode "' + bidderCode + '" is not an existing bidder.', 'adapterManager.aliasBidAdapter'); - } else { - _aliasRegistry[alias] = bidderCode; - } + }) } else { try { let newAdapter; let supportedMediaTypes = getSupportedMediaTypes(bidderCode); // Have kept old code to support backward compatibilitiy. // Remove this if loop when all adapters are supporting bidderFactory. i.e When Prebid.js is 1.0 - if (bidAdaptor.constructor.prototype != Object.prototype) { - newAdapter = new bidAdaptor.constructor(); + if (bidAdapter.constructor.prototype != Object.prototype) { + newAdapter = new bidAdapter.constructor(); newAdapter.setBidderCode(alias); } else { - let spec = bidAdaptor.getSpec(); + let spec = bidAdapter.getSpec(); let gvlid = options && options.gvlid; - newAdapter = newBidder(Object.assign({}, spec, { code: alias, gvlid })); + let skipPbsAliasing = options && options.skipPbsAliasing; + newAdapter = newBidder(Object.assign({}, spec, { code: alias, gvlid, skipPbsAliasing })); _aliasRegistry[alias] = bidderCode; } adapterManager.registerBidAdapter(newAdapter, alias, { @@ -550,4 +606,8 @@ adapterManager.callSetTargetingBidder = function(bidder, bid) { tryCallBidderMethod(bidder, 'onSetTargeting', bid); }; +adapterManager.callBidViewableBidder = function(bidder, bid) { + tryCallBidderMethod(bidder, 'onBidViewable', bid); +}; + export default adapterManager; diff --git a/src/adapters/bidderFactory.js b/src/adapters/bidderFactory.js index 3b6260efc88..27dcc9c0115 100644 --- a/src/adapters/bidderFactory.js +++ b/src/adapters/bidderFactory.js @@ -155,12 +155,14 @@ export function registerBidder(spec) { spec.aliases.forEach(alias => { let aliasCode = alias; let gvlid; + let skipPbsAliasing; if (isPlainObject(alias)) { aliasCode = alias.code; gvlid = alias.gvlid; + skipPbsAliasing = alias.skipPbsAliasing } adapterManager.aliasRegistry[aliasCode] = spec.code; - putBidder(Object.assign({}, spec, { code: aliasCode, gvlid })); + putBidder(Object.assign({}, spec, { code: aliasCode, gvlid, skipPbsAliasing })); }); } } diff --git a/src/auction.js b/src/auction.js index c94e3adc9a7..ec9ff7d8209 100644 --- a/src/auction.js +++ b/src/auction.js @@ -458,7 +458,7 @@ function tryAddVideoBid(auctionInstance, bidResponse, bidRequests, afterBidAdded const context = videoMediaType && deepAccess(videoMediaType, 'context'); if (config.getConfig('cache.url') && context !== OUTSTREAM) { - if (!bidResponse.videoCacheKey) { + if (!bidResponse.videoCacheKey || config.getConfig('cache.ignoreBidderCacheKey')) { addBid = false; callPrebidCache(auctionInstance, bidResponse, afterBidAdded, bidderRequest); } else if (!bidResponse.vastUrl) { @@ -626,6 +626,16 @@ export const getPriceByGranularity = (granularity) => { } } +/** + * This function returns a function to get first advertiser domain from bid response meta + * @returns {function} + */ +export const getAdvertiserDomain = () => { + return (bid) => { + return (bid.meta && bid.meta.advertiserDomains && bid.meta.advertiserDomains.length > 0) ? bid.meta.advertiserDomains[0] : ''; + } +} + /** * @param {string} mediaType * @param {string} bidderCode @@ -662,6 +672,7 @@ export function getStandardBidderSettings(mediaType, bidderCode, bidReq) { createKeyVal(TARGETING_KEYS.DEAL, 'dealId'), createKeyVal(TARGETING_KEYS.SOURCE, 'source'), createKeyVal(TARGETING_KEYS.FORMAT, 'mediaType'), + createKeyVal(TARGETING_KEYS.ADOMAIN, getAdvertiserDomain()), ] } diff --git a/src/constants.json b/src/constants.json index 7c0af445cdb..e6b9687f911 100644 --- a/src/constants.json +++ b/src/constants.json @@ -38,10 +38,11 @@ "ADD_AD_UNITS": "addAdUnits", "AD_RENDER_FAILED": "adRenderFailed", "TCF2_ENFORCEMENT": "tcf2Enforcement", - "AUCTION_DEBUG": "auctionDebug" + "AUCTION_DEBUG": "auctionDebug", + "BID_VIEWABLE": "bidViewable" }, "AD_RENDER_FAILED_REASON" : { - "PREVENT_WRITING_ON_MAIN_DOCUMENT": "preventWritingOnMainDocuemnt", + "PREVENT_WRITING_ON_MAIN_DOCUMENT": "preventWritingOnMainDocument", "NO_AD": "noAd", "EXCEPTION": "exception", "CANNOT_FIND_AD": "cannotFindAd", @@ -59,6 +60,19 @@ "CUSTOM": "custom" }, "TARGETING_KEYS": { + "BIDDER": "hb_bidder", + "AD_ID": "hb_adid", + "PRICE_BUCKET": "hb_pb", + "SIZE": "hb_size", + "DEAL": "hb_deal", + "SOURCE": "hb_source", + "FORMAT": "hb_format", + "UUID": "hb_uuid", + "CACHE_ID": "hb_cache_id", + "CACHE_HOST": "hb_cache_host", + "ADOMAIN" : "hb_adomain" + }, + "DEFAULT_TARGETING_KEYS": { "BIDDER": "hb_bidder", "AD_ID": "hb_adid", "PRICE_BUCKET": "hb_pb", @@ -88,7 +102,9 @@ "likes": "hb_native_likes", "phone": "hb_native_phone", "price": "hb_native_price", - "salePrice": "hb_native_saleprice" + "salePrice": "hb_native_saleprice", + "rendererUrl": "hb_renderer_url", + "adTemplate": "hb_adTemplate" }, "S2S" : { "SRC" : "s2s", diff --git a/src/native.js b/src/native.js index e41d7740ffa..b43e582327b 100644 --- a/src/native.js +++ b/src/native.js @@ -154,21 +154,48 @@ export function fireNativeTrackers(message, adObject) { export function getNativeTargeting(bid, bidReq) { let keyValues = {}; - Object.keys(bid['native']).forEach(asset => { - const key = CONSTANTS.NATIVE_KEYS[asset]; - let value = getAssetValue(bid['native'][asset]); + if (deepAccess(bidReq, 'nativeParams.rendererUrl')) { + bid['native']['rendererUrl'] = getAssetValue(bidReq.nativeParams['rendererUrl']); + } else if (deepAccess(bidReq, 'nativeParams.adTemplate')) { + bid['native']['adTemplate'] = getAssetValue(bidReq.nativeParams['adTemplate']); + } + + const globalSendTargetingKeys = deepAccess( + bidReq, + `nativeParams.sendTargetingKeys` + ) !== false; + + const nativeKeys = getNativeKeys(bidReq); + + const flatBidNativeKeys = { ...bid.native, ...bid.native.ext }; + delete flatBidNativeKeys.ext; - const sendPlaceholder = deepAccess( - bidReq, - `mediaTypes.native.${asset}.sendId` - ); + Object.keys(flatBidNativeKeys).forEach(asset => { + const key = nativeKeys[asset]; + let value = getAssetValue(bid.native[asset]) || getAssetValue(deepAccess(bid, `native.ext.${asset}`)); + + if (asset === 'adTemplate' || !key || !value) { + return; + } + + let sendPlaceholder = deepAccess(bidReq, `nativeParams.${asset}.sendId`); + if (typeof sendPlaceholder !== 'boolean') { + sendPlaceholder = deepAccess(bidReq, `nativeParams.ext.${asset}.sendId`); + } if (sendPlaceholder) { const placeholder = `${key}:${bid.adId}`; value = placeholder; } - if (key && value) { + let assetSendTargetingKeys = deepAccess(bidReq, `nativeParams.${asset}.sendTargetingKeys`) + if (typeof assetSendTargetingKeys !== 'boolean') { + assetSendTargetingKeys = deepAccess(bidReq, `nativeParams.ext.${asset}.sendTargetingKeys`); + } + + const sendTargeting = typeof assetSendTargetingKeys === 'boolean' ? assetSendTargetingKeys : globalSendTargetingKeys; + + if (sendTargeting) { keyValues[key] = value; } }); @@ -187,6 +214,12 @@ export function getAssetMessage(data, adObject) { assets: [], }; + if (adObject.native.hasOwnProperty('adTemplate')) { + message.adTemplate = getAssetValue(adObject.native['adTemplate']); + } if (adObject.native.hasOwnProperty('rendererUrl')) { + message.rendererUrl = getAssetValue(adObject.native['rendererUrl']); + } + data.assets.forEach(asset => { const key = getKeyByValue(CONSTANTS.NATIVE_KEYS, asset); const value = getAssetValue(adObject.native[key]); @@ -197,6 +230,35 @@ export function getAssetMessage(data, adObject) { return message; } +export function getAllAssetsMessage(data, adObject) { + const message = { + message: 'assetResponse', + adId: data.adId, + assets: [] + }; + + Object.keys(adObject.native).forEach(function(key, index) { + if (key === 'adTemplate' && adObject.native[key]) { + message.adTemplate = getAssetValue(adObject.native[key]); + } else if (key === 'rendererUrl' && adObject.native[key]) { + message.rendererUrl = getAssetValue(adObject.native[key]); + } else if (key === 'ext') { + Object.keys(adObject.native[key]).forEach(extKey => { + if (adObject.native[key][extKey]) { + const value = getAssetValue(adObject.native[key][extKey]); + message.assets.push({ key: extKey, value }); + } + }) + } else if (adObject.native[key] && CONSTANTS.NATIVE_KEYS.hasOwnProperty(key)) { + const value = getAssetValue(adObject.native[key]); + + message.assets.push({ key, value }); + } + }); + + return message; +} + /** * Native assets can be a string or an object with a url prop. Returns the value * appropriate for sending in adserver targeting or placeholder replacement. @@ -208,3 +270,18 @@ function getAssetValue(value) { return value; } + +function getNativeKeys(bidReq) { + const extraNativeKeys = {} + + if (deepAccess(bidReq, 'nativeParams.ext')) { + Object.keys(bidReq.nativeParams.ext).forEach(extKey => { + extraNativeKeys[extKey] = `hb_native_${extKey}`; + }) + } + + return { + ...CONSTANTS.NATIVE_KEYS, + ...extraNativeKeys + } +} diff --git a/src/prebid.js b/src/prebid.js index 0f72ca878e5..3452107effd 100644 --- a/src/prebid.js +++ b/src/prebid.js @@ -488,6 +488,18 @@ $$PREBID_GLOBAL$$.requestBids = hook('async', function ({ bidsBackHandler, timeo utils.logInfo('Invoking $$PREBID_GLOBAL$$.requestBids', arguments); + let _s2sConfigs = []; + const s2sBidders = []; + config.getConfig('s2sConfig', config => { + if (config && config.s2sConfig) { + _s2sConfigs = Array.isArray(config.s2sConfig) ? config.s2sConfig : [config.s2sConfig]; + } + }); + + _s2sConfigs.forEach(s2sConfig => { + s2sBidders.push(...s2sConfig.bidders); + }); + adUnits = checkAdUnitSetup(adUnits); if (adUnitCodes && adUnitCodes.length) { @@ -512,11 +524,7 @@ $$PREBID_GLOBAL$$.requestBids = hook('async', function ({ bidsBackHandler, timeo const allBidders = adUnit.bids.map(bid => bid.bidder); const bidderRegistry = adapterManager.bidderRegistry; - const s2sConfig = config.getConfig('s2sConfig'); - const s2sBidders = s2sConfig && s2sConfig.bidders; - const bidders = (s2sBidders) ? allBidders.filter(bidder => { - return !includes(s2sBidders, bidder); - }) : allBidders; + const bidders = (s2sBidders) ? allBidders.filter(bidder => !includes(s2sBidders, bidder)) : allBidders; adUnit.transactionId = utils.generateUUID(); diff --git a/src/secureCreatives.js b/src/secureCreatives.js index cb192dd773e..def1a9abdbb 100644 --- a/src/secureCreatives.js +++ b/src/secureCreatives.js @@ -4,7 +4,7 @@ */ import events from './events.js'; -import { fireNativeTrackers, getAssetMessage } from './native.js'; +import { fireNativeTrackers, getAssetMessage, getAllAssetsMessage } from './native.js'; import constants from './constants.json'; import { logWarn, replaceAuctionPrice } from './utils.js'; import { auctionManager } from './auctionManager.js'; @@ -51,6 +51,13 @@ function receiveMessage(ev) { const message = getAssetMessage(data, adObject); ev.source.postMessage(JSON.stringify(message), ev.origin); return; + } else if (data.action === 'allAssetRequest') { + const message = getAllAssetsMessage(data, adObject); + ev.source.postMessage(JSON.stringify(message), ev.origin); + } else if (data.action === 'resizeNativeHeight') { + adObject.height = data.height; + adObject.width = data.width; + resizeRemoteCreative(adObject); } const trackerType = fireNativeTrackers(data, adObject); diff --git a/src/targeting.js b/src/targeting.js index b6a38bdbb61..cb53ea0fed3 100644 --- a/src/targeting.js +++ b/src/targeting.js @@ -5,6 +5,7 @@ import { auctionManager } from './auctionManager.js'; import { sizeSupported } from './sizeMapping.js'; import { ADPOD } from './mediaTypes.js'; import includes from 'core-js-pure/features/array/includes.js'; +import find from 'core-js-pure/features/array/find.js'; const utils = require('./utils.js'); var CONSTANTS = require('./constants.json'); @@ -19,7 +20,7 @@ export const TARGETING_KEYS = Object.keys(CONSTANTS.TARGETING_KEYS).map( ); // return unexpired bids -const isBidNotExpired = (bid) => (bid.responseTimestamp + bid.ttl * 1000 + TTL_BUFFER) > timestamp(); +const isBidNotExpired = (bid) => (bid.responseTimestamp + bid.ttl * 1000 - TTL_BUFFER) > timestamp(); // return bids whose status is not set. Winning bids can only have a status of `rendered`. const isUnusedBid = (bid) => bid && ((bid.status && !includes([CONSTANTS.BID_STATUS.RENDERED], bid.status)) || !bid.status); @@ -200,7 +201,7 @@ export function newTargeting(auctionManager) { // check if key is in default keys, if not, it's custom, we won't remove it. const isCustom = defaultKeys.filter(defaultKey => key.indexOf(defaultKeyring[defaultKey]) === 0).length === 0; // check if key explicitly allowed, if not, we'll remove it. - const found = isCustom || allowedKeys.find(allowedKey => { + const found = isCustom || find(allowedKeys, allowedKey => { const allowedKeyName = defaultKeyring[allowedKey]; // we're looking to see if the key exactly starts with one of our default keys. // (which hopefully means it's not custom) @@ -249,7 +250,8 @@ export function newTargeting(auctionManager) { }); }); - const allowedKeys = config.getConfig('targetingControls.allowTargetingKeys'); + const defaultKeys = Object.keys(Object.assign({}, CONSTANTS.DEFAULT_TARGETING_KEYS, CONSTANTS.NATIVE_KEYS)); + const allowedKeys = config.getConfig('targetingControls.allowTargetingKeys') || defaultKeys; if (Array.isArray(allowedKeys) && allowedKeys.length > 0) { targeting = getAllowedTargetingKeyValues(targeting, allowedKeys); } diff --git a/src/utils.js b/src/utils.js index acdf0f101ad..bd3257ab7e7 100644 --- a/src/utils.js +++ b/src/utils.js @@ -43,6 +43,14 @@ export const internal = { deepEqual }; +let prebidInternal = {} +/** + * Returns object that is used as internal prebid namespace + */ +export function getPrebidInternal() { + return prebidInternal; +} + var uniqueRef = {}; export let bind = function(a, b) { return b; }.bind(null, 1, uniqueRef)() === uniqueRef ? Function.prototype.bind diff --git a/test/fixtures/fixtures.js b/test/fixtures/fixtures.js index 2a0a7638fc4..908382f8daa 100644 --- a/test/fixtures/fixtures.js +++ b/test/fixtures/fixtures.js @@ -676,7 +676,13 @@ export function getAdUnits() { 'bidder': 'appnexus', 'params': { 'placementId': '543221', - 'test': 'me' + } + }, + { + 'bidder': 'pubmatic', + 'params': { + 'publisherId': 1234567, + 'adSlot': '1234567@728x90' } } ] diff --git a/test/spec/auctionmanager_spec.js b/test/spec/auctionmanager_spec.js index a50eba5e585..69e34c4a07a 100644 --- a/test/spec/auctionmanager_spec.js +++ b/test/spec/auctionmanager_spec.js @@ -143,6 +143,9 @@ describe('auctionmanager.js', function () { adId: '1adId', source: 'client', mediaType: 'banner', + meta: { + advertiserDomains: ['adomain'] + } }; /* return the expected response for a given bid, filter by keys if given */ @@ -154,6 +157,7 @@ describe('auctionmanager.js', function () { expected[ CONSTANTS.TARGETING_KEYS.SIZE ] = bid.getSize(); expected[ CONSTANTS.TARGETING_KEYS.SOURCE ] = bid.source; expected[ CONSTANTS.TARGETING_KEYS.FORMAT ] = bid.mediaType; + expected[ CONSTANTS.TARGETING_KEYS.ADOMAIN ] = bid.meta.advertiserDomains[0]; if (bid.mediaType === 'video') { expected[ CONSTANTS.TARGETING_KEYS.UUID ] = bid.videoCacheKey; expected[ CONSTANTS.TARGETING_KEYS.CACHE_ID ] = bid.videoCacheKey; @@ -239,6 +243,12 @@ describe('auctionmanager.js', function () { return bidResponse.mediaType; } }, + { + key: CONSTANTS.TARGETING_KEYS.ADOMAIN, + val: function (bidResponse) { + return bidResponse.meta.advertiserDomains[0]; + } + } ] } @@ -309,6 +319,12 @@ describe('auctionmanager.js', function () { val: function (bidResponse) { return bidResponse.videoCacheKey; } + }, + { + key: CONSTANTS.TARGETING_KEYS.ADOMAIN, + val: function (bidResponse) { + return bidResponse.meta.advertiserDomains[0]; + } } ] diff --git a/test/spec/modules/adWMGBidAdapter_spec.js b/test/spec/modules/adWMGBidAdapter_spec.js new file mode 100644 index 00000000000..1f881897fd8 --- /dev/null +++ b/test/spec/modules/adWMGBidAdapter_spec.js @@ -0,0 +1,332 @@ +import { expect } from 'chai'; +import { spec } from 'modules/adWMGBidAdapter.js'; +import { config } from 'src/config.js'; + +describe('adWMGBidAdapter', function () { + describe('isBidRequestValid', function () { + let bid; + beforeEach(function() { + bid = { + bidder: 'adWMG', + params: { + publisherId: '5cebea3c9eea646c7b623d5e' + }, + mediaTypes: { + banner: { + size: [[300, 250]] + } + } + }; + }); + + it('should return true when valid bid request is set', function() { + expect(spec.isBidRequestValid(bid)).to.equal(true); + }); + + it('should return false when bidder is not set to "adWMG"', function() { + bid.bidder = 'bidder'; + expect(spec.isBidRequestValid(bid)).to.equal(false); + }); + + it('should return false when \'publisherId\' param are not set', function() { + delete bid.params.publisherId; + expect(spec.isBidRequestValid(bid)).to.equal(false); + }); + }); + + describe('parseUserAgent', function() { + let ua_desktop, ua_mobile, ua_tv, ua_tablet; + beforeEach(function() { + ua_desktop = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.66 Safari/537.36'; + ua_tv = 'Mozilla/5.0 (Linux; NetCast; U) AppleWebKit/537.31 (KHTML, like Gecko) Chrome/38.0.2125.122 Safari/537.31 SmartTV/7.0'; + ua_mobile = 'Mozilla/5.0 (Linux; Android 7.0; SAMSUNG SM-G610M Build/NRD90M) AppleWebKit/537.36 (KHTML, like Gecko) SamsungBrowser/7.2 Chrome/59.0.3071.125 Mobile Safari/537.36'; + ua_tablet = 'Mozilla/5.0 (iPad; CPU OS 7_1_2 like Mac OS X) AppleWebKit/537.51.2 (KHTML, like Gecko) Version/7.0 Mobile/11D257 Safari/9537.53'; + }); + + it('should return correct device type: desktop', function() { + let userDeviceInfo = spec.parseUserAgent(ua_desktop); + expect(userDeviceInfo.devicetype).to.equal(2); + }); + + it('should return correct device type: TV', function() { + let userDeviceInfo = spec.parseUserAgent(ua_tv); + expect(userDeviceInfo.devicetype).to.equal(3); + }); + + it('should return correct device type: mobile', function() { + let userDeviceInfo = spec.parseUserAgent(ua_mobile); + expect(userDeviceInfo.devicetype).to.equal(4); + }); + + it('should return correct device type: tablet', function() { + let userDeviceInfo = spec.parseUserAgent(ua_tablet); + expect(userDeviceInfo.devicetype).to.equal(5); + }); + + it('should return correct OS name', function() { + let userDeviceInfo = spec.parseUserAgent(ua_desktop); + expect(userDeviceInfo.os).to.equal('Windows'); + }); + + it('should return correct OS version', function() { + let userDeviceInfo = spec.parseUserAgent(ua_desktop); + expect(userDeviceInfo.osv).to.equal('10.0'); + }); + }); + + describe('buildRequests', function () { + let bidRequests; + beforeEach(function() { + bidRequests = [ + { + bidder: 'adWMG', + adUnitCode: 'adwmg-test-ad', + auctionId: 'test-auction-id', + bidId: 'test-bid-id', + bidRequestsCount: 1, + bidderRequestId: 'bidderrequestid123', + transactionId: 'transaction-id-123', + sizes: [[300, 250]], + requestId: 'requestid123', + params: { + floorPrice: 100, + currency: 'USD' + }, + mediaTypes: { + banner: { + size: [[300, 250]] + } + }, + userId: { + pubcid: 'pubc-id-123' + } + }, { + bidder: 'adWMG', + adUnitCode: 'adwmg-test-ad-2', + auctionId: 'test-auction-id-2', + bidId: 'test-bid-id-2', + bidRequestsCount: 1, + bidderRequestId: 'bidderrequestid456', + transactionId: 'transaction-id-456', + sizes: [[320, 50]], + requestId: 'requestid456', + params: { + floorPrice: 100, + currency: 'USD' + }, + mediaTypes: { + banner: { + size: [[320, 50]] + } + }, + userId: { + pubcid: 'pubc-id-456' + } + } + ]; + }); + + let bidderRequest = { + refererInfo: { + referer: 'https://test.com' + }, + gdprConsent: { + consentString: 'CO9rhBTO9rhBTAcABBENBCCsAP_AAH_AACiQHItf_X_fb3_j-_59_9t0eY1f9_7_v20zjgeds-8Nyd_X_L8X42M7vB36pq4KuR4Eu3LBIQdlHOHcTUmw6IkVqTPsbk2Mr7NKJ7PEinMbe2dYGH9_n9XTuZKY79_s___z__-__v__7_f_r-3_3_vp9V---3YHIgEmGpfARZiWOBJNGlUKIEIVxIdACACihGFomsICVwU7K4CP0EDABAagIwIgQYgoxZBAAAAAElEQEgB4IBEARAIAAQAqQEIACNAEFgBIGAQACgGhYARQBCBIQZHBUcpgQESLRQTyVgCUXexhhCGUUANAg4AA.YAAAAAAAAAAA', + vendorData: {}, + gdprApplies: true, + apiVersion: 2 + } + }; + + it('should not contain a sizes when sizes is not set', function() { + delete bidRequests[0].sizes; + delete bidRequests[1].sizes; + let requests = spec.buildRequests(bidRequests, bidderRequest); + expect(JSON.parse(requests[0].data).sizes).to.be.an('undefined'); + expect(JSON.parse(requests[1].data).sizes).to.be.an('undefined'); + }); + + it('should not contain a userId when userId is not set', function() { + delete bidRequests[0].userId; + delete bidRequests[1].userId; + let requests = spec.buildRequests(bidRequests, bidderRequest); + expect(JSON.parse(requests[0].data).userId).to.be.an('undefined'); + expect(JSON.parse(requests[1].data).userId).to.be.an('undefined'); + }); + + it('should have a post method', function() { + let requests = spec.buildRequests(bidRequests, bidderRequest); + expect(requests[0].method).to.equal('POST'); + expect(requests[1].method).to.equal('POST'); + }); + + it('should contain a request id equals to the bid id', function() { + let requests = spec.buildRequests(bidRequests, bidderRequest); + expect(JSON.parse(requests[0].data).requestId).to.equal(bidRequests[0].bidId); + expect(JSON.parse(requests[1].data).requestId).to.equal(bidRequests[1].bidId); + }); + + it('should have an url that match the default endpoint', function() { + let requests = spec.buildRequests(bidRequests, bidderRequest); + expect(requests[0].url).to.equal('https://rtb.adwmg.com/prebid'); + expect(requests[1].url).to.equal('https://rtb.adwmg.com/prebid'); + }); + + it('should contain GDPR consent data if GDPR set', function() { + let requests = spec.buildRequests(bidRequests, bidderRequest); + expect(JSON.parse(requests[0].data).gdpr.applies).to.be.true; + expect(JSON.parse(requests[0].data).gdpr.consentString).to.equal(bidderRequest.gdprConsent.consentString); + expect(JSON.parse(requests[1].data).gdpr.applies).to.be.true; + expect(JSON.parse(requests[1].data).gdpr.consentString).to.equal(bidderRequest.gdprConsent.consentString); + }) + + it('should not contain GDPR consent data if GDPR not set', function() { + delete bidderRequest.gdprConsent; + let requests = spec.buildRequests(bidRequests, bidderRequest); + expect(JSON.parse(requests[0].data).gdpr).to.be.an('undefined'); + expect(JSON.parse(requests[1].data).gdpr).to.be.an('undefined'); + }) + + it('should set debug mode in requests if enabled', function() { + sinon.stub(config, 'getConfig').withArgs('debug').returns(true); + let requests = spec.buildRequests(bidRequests, bidderRequest); + expect(JSON.parse(requests[0].data).debug).to.be.true; + expect(JSON.parse(requests[1].data).debug).to.be.true; + config.getConfig.restore(); + }) + }); + + describe('interpretResponse', function () { + let serverResponse; + beforeEach(function() { + serverResponse = { + body: { + 'requestId': 'request-id', + 'cpm': 100, + 'width': 300, + 'height': 250, + 'ad': '
ad
', + 'ttl': 300, + 'creativeId': 'creative-id', + 'netRevenue': true, + 'currency': 'USD' + } + }; + }); + + it('should return a valid response', () => { + var responses = spec.interpretResponse(serverResponse); + expect(responses).to.be.an('array').that.is.not.empty; + + let response = responses[0]; + expect(response).to.have.all.keys('requestId', 'cpm', 'width', 'height', 'ad', 'ttl', 'creativeId', + 'netRevenue', 'currency'); + expect(response.requestId).to.equal('request-id'); + expect(response.cpm).to.equal(100); + expect(response.width).to.equal(300); + expect(response.height).to.equal(250); + expect(response.ad).to.equal('
ad
'); + expect(response.ttl).to.equal(300); + expect(response.creativeId).to.equal('creative-id'); + expect(response.netRevenue).to.be.true; + expect(response.currency).to.equal('USD'); + }); + + it('should return an empty array when serverResponse is empty', () => { + serverResponse = {}; + var responses = spec.interpretResponse(serverResponse); + expect(responses).to.deep.equal([]); + }); + }); + + describe('getUserSyncs', function () { + it('should return nothing when sync is disabled', function () { + const syncOptions = { + 'iframeEnabled': false, + 'pixelEnabled': false + }; + + let syncs = spec.getUserSyncs(syncOptions); + expect(syncs).to.deep.equal([]); + }); + + it('should register iframe sync when only iframe is enabled', function () { + const syncOptions = { + 'iframeEnabled': true, + 'pixelEnabled': false + }; + + let syncs = spec.getUserSyncs(syncOptions); + expect(syncs[0].type).to.equal('iframe'); + expect(syncs[0].url).includes('https://rtb.adwmg.com/cphb.html?'); + }); + + it('should register iframe sync when iframe and image are enabled', function () { + const syncOptions = { + 'iframeEnabled': true, + 'pixelEnabled': true + }; + + let syncs = spec.getUserSyncs(syncOptions); + expect(syncs[0].type).to.equal('iframe'); + expect(syncs[0].url).includes('https://rtb.adwmg.com/cphb.html?'); + }); + + it('should send GDPR consent if enabled', function() { + const syncOptions = { + 'iframeEnabled': true, + 'pixelEnabled': true + }; + const gdprConsent = { + consentString: 'CO9rhBTO9rhBTAcABBENBCCsAP_AAH_AACiQHItf_X_fb3_j-_59_9t0eY1f9_7_v20zjgeds-8Nyd_X_L8X42M7vB36pq4KuR4Eu3LBIQdlHOHcTUmw6IkVqTPsbk2Mr7NKJ7PEinMbe2dYGH9_n9XTuZKY79_s___z__-__v__7_f_r-3_3_vp9V---3YHIgEmGpfARZiWOBJNGlUKIEIVxIdACACihGFomsICVwU7K4CP0EDABAagIwIgQYgoxZBAAAAAElEQEgB4IBEARAIAAQAqQEIACNAEFgBIGAQACgGhYARQBCBIQZHBUcpgQESLRQTyVgCUXexhhCGUUANAg4AA.YAAAAAAAAAAA', + vendorData: {}, + gdprApplies: true, + apiVersion: 2 + }; + const serverResponse = {}; + let syncs = spec.getUserSyncs(syncOptions, serverResponse, gdprConsent); + expect(syncs[0].url).includes('gdpr=1'); + expect(syncs[0].url).includes(`gdpr_consent=${gdprConsent.consentString}`); + }); + + it('should not add GDPR consent params twice', function() { + const syncOptions = { + 'iframeEnabled': true, + 'pixelEnabled': true + }; + const gdprConsent = { + consentString: 'CO9rhBTO9rhBTAcABBENBCCsAP_AAH_AACiQHItf_X_fb3_j-_59_9t0eY1f9_7_v20zjgeds-8Nyd_X_L8X42M7vB36pq4KuR4Eu3LBIQdlHOHcTUmw6IkVqTPsbk2Mr7NKJ7PEinMbe2dYGH9_n9XTuZKY79_s___z__-__v__7_f_r-3_3_vp9V---3YHIgEmGpfARZiWOBJNGlUKIEIVxIdACACihGFomsICVwU7K4CP0EDABAagIwIgQYgoxZBAAAAAElEQEgB4IBEARAIAAQAqQEIACNAEFgBIGAQACgGhYARQBCBIQZHBUcpgQESLRQTyVgCUXexhhCGUUANAg4AA.YAAAAAAAAAAA', + vendorData: {}, + gdprApplies: true, + apiVersion: 2 + }; + const gdprConsent2 = { + consentString: 'CO9rhBTO9rhBTAcABBENBCCsAP_AAH_AACiQHItf_7_fb3_j-_59_9t0eY1f9_7_v20zjgeds-8Nyd_X_L8X42M7vB36pq4KuR4Eu3LBIQdlHOHcTUmw6IkVqTPsbk2Mr7NKJ7PEinMbe2dYGH9_n9XTuZKY79_s___z__-__v__7_f_r-3_3_vp9V---3YHIgEmGpfARZiWOBJNGlUKIEIVxIdACACihGFomsICVwU7K4CP0EDABAagIwIgQYgoxZBAAAAAElEQEgB4IBEARAIAAQAqQEIACNAEFgBIGAQACgGhYARQBCBIQZHBUcpgQESLRQTyVgCUXexhhCGUUANAg4AA.YAAAAAAAAAAA', + vendorData: {}, + gdprApplies: true, + apiVersion: 2 + }; + const serverResponse = {}; + let syncs = spec.getUserSyncs(syncOptions, serverResponse, gdprConsent); + syncs = spec.getUserSyncs(syncOptions, serverResponse, gdprConsent2); + expect(syncs[0].url.match(/gdpr/g).length).to.equal(2); // gdpr + gdpr_consent + expect(syncs[0].url.match(/gdpr_consent/g).length).to.equal(1); + }); + + it('should delete \'&\' symbol at the end of usersync URL', function() { + const syncOptions = { + 'iframeEnabled': true, + 'pixelEnabled': true + }; + const gdprConsent = { + consentString: 'CO9rhBTO9rhBTAcABBENBCCsAP_AAH_AACiQHItf_X_fb3_j-_59_9t0eY1f9_7_v20zjgeds-8Nyd_X_L8X42M7vB36pq4KuR4Eu3LBIQdlHOHcTUmw6IkVqTPsbk2Mr7NKJ7PEinMbe2dYGH9_n9XTuZKY79_s___z__-__v__7_f_r-3_3_vp9V---3YHIgEmGpfARZiWOBJNGlUKIEIVxIdACACihGFomsICVwU7K4CP0EDABAagIwIgQYgoxZBAAAAAElEQEgB4IBEARAIAAQAqQEIACNAEFgBIGAQACgGhYARQBCBIQZHBUcpgQESLRQTyVgCUXexhhCGUUANAg4AA.YAAAAAAAAAAA', + vendorData: {}, + gdprApplies: true, + apiVersion: 2 + }; + const serverResponse = {}; + let syncs = spec.getUserSyncs(syncOptions, serverResponse, gdprConsent); + expect(syncs[0].url.slice(-1)).to.not.equal('&'); + }); + }); +}); diff --git a/test/spec/modules/adagioBidAdapter_spec.js b/test/spec/modules/adagioBidAdapter_spec.js index 2cf97a1129b..0a585caaa1a 100644 --- a/test/spec/modules/adagioBidAdapter_spec.js +++ b/test/spec/modules/adagioBidAdapter_spec.js @@ -144,6 +144,19 @@ describe('Adagio bid adapter', () => { sinon.assert.callCount(utils.logWarn, 1); }); + it('should use adUnit code for adUnitElementId and placement params', function() { + const bid01 = new BidRequestBuilder({ params: { + organizationId: '1000', + site: 'site-name', + useAdUnitCodeAsPlacement: true, + useAdUnitCodeAsAdUnitElementId: true + }}).build(); + + expect(spec.isBidRequestValid(bid01)).to.equal(true); + expect(bid01.params.adUnitElementId).to.equal('adunit-code'); + expect(bid01.params.placement).to.equal('adunit-code'); + }) + it('should return false when a required param is missing', function() { const bid01 = new BidRequestBuilder({ params: { organizationId: '1000', @@ -229,7 +242,8 @@ describe('Adagio bid adapter', () => { placement: 'PAVE_ATF', site: 'SITE-NAME', adUnitElementId: 'gpt-adunit-code', - environment: 'desktop' + environment: 'desktop', + supportIObs: true } }], auctionId: '4fd1ca2d-846c-4211-b9e5-321dfe1709c9', @@ -249,7 +263,8 @@ describe('Adagio bid adapter', () => { placement: 'PAVE_ATF', site: 'SITE-NAME', adUnitElementId: 'gpt-adunit-code', - environment: 'desktop' + environment: 'desktop', + supportIObs: true } }], auctionId: '4fd1ca2d-846c-4211-b9e5-321dfe1709c9', @@ -260,6 +275,7 @@ describe('Adagio bid adapter', () => { it('should store bids config once by bid in window.top if it accessible', function() { sandbox.stub(adagio, 'getCurrentWindow').returns(window.top); + sandbox.stub(adagio, 'supportIObs').returns(true); // replace by the values defined in beforeEach window.top.ADAGIO = { @@ -274,8 +290,22 @@ describe('Adagio bid adapter', () => { expect(find(window.top.ADAGIO.pbjsAdUnits, aU => aU.code === 'adunit-code-02')).to.deep.eql(expected[1]); }); + it('should detect IntersectionObserver support', function() { + sandbox.stub(adagio, 'getCurrentWindow').returns(window.top); + sandbox.stub(adagio, 'supportIObs').returns(false); + + window.top.ADAGIO = { + ...window.ADAGIO + }; + + spec.isBidRequestValid(bid01); + const validBidReq = find(window.top.ADAGIO.pbjsAdUnits, aU => aU.code === 'adunit-code-01'); + expect(validBidReq.bids[0].params.supportIObs).to.equal(false); + }); + it('should store bids config once by bid in current window', function() { sandbox.stub(adagio, 'getCurrentWindow').returns(window.self); + sandbox.stub(adagio, 'supportIObs').returns(true); spec.isBidRequestValid(bid01); spec.isBidRequestValid(bid02); @@ -740,7 +770,8 @@ describe('Adagio bid adapter', () => { pagetype: 'ARTICLE', category: 'NEWS', subcategory: 'SPORT', - environment: 'desktop' + environment: 'desktop', + supportIObs: true }, adUnitCode: 'adunit-code', mediaTypes: { diff --git a/test/spec/modules/adhashBidAdapter_spec.js b/test/spec/modules/adhashBidAdapter_spec.js new file mode 100644 index 00000000000..ab4df84c093 --- /dev/null +++ b/test/spec/modules/adhashBidAdapter_spec.js @@ -0,0 +1,155 @@ +import { expect } from 'chai'; +import { spec } from 'modules/adhashBidAdapter.js'; + +describe('adhashBidAdapter', function () { + describe('isBidRequestValid', function () { + const validBid = { + bidder: 'adhash', + params: { + publisherId: '0xc3b09b27e9c6ef73957901aa729b9e69e5bbfbfb', + platformURL: 'https://adhash.org/p/struma/' + }, + mediaTypes: { + banner: { + sizes: [[300, 250]] + } + }, + adUnitCode: 'adunit-code', + sizes: [[300, 250]], + bidId: '12345678901234', + bidderRequestId: '98765432109876', + auctionId: '01234567891234', + }; + + it('should return true when all mandatory parameters are there', function () { + expect(spec.isBidRequestValid(validBid)).to.equal(true); + }); + + it('should return false when there are no params', function () { + const bid = { ...validBid }; + delete bid.params; + expect(spec.isBidRequestValid(bid)).to.equal(false); + }); + + it('should return false when unsupported media type is requested', function () { + const bid = { ...validBid }; + bid.mediaTypes = { native: { sizes: [[300, 250]] } }; + expect(spec.isBidRequestValid(bid)).to.equal(false); + }); + + it('should return false when publisherId is not a string', function () { + const bid = { ...validBid }; + bid.params.publisherId = 123; + expect(spec.isBidRequestValid(bid)).to.equal(false); + }); + + it('should return false when publisherId is not valid', function () { + const bid = { ...validBid }; + bid.params.publisherId = 'short string'; + expect(spec.isBidRequestValid(bid)).to.equal(false); + }); + + it('should return false when publisherId is not a string', function () { + const bid = { ...validBid }; + bid.params.platformURL = 123; + expect(spec.isBidRequestValid(bid)).to.equal(false); + }); + + it('should return false when publisherId is not valid', function () { + const bid = { ...validBid }; + bid.params.platformURL = 'https://'; + expect(spec.isBidRequestValid(bid)).to.equal(false); + }); + }); + + describe('buildRequests', function () { + const bidRequest = { + params: { + publisherId: '0xc3b09b27e9c6ef73957901aa729b9e69e5bbfbfb' + }, + sizes: [[300, 250]], + adUnitCode: 'adUnitCode' + }; + it('should build the request correctly', function () { + const result = spec.buildRequests( + [ bidRequest ], + { gdprConsent: true, refererInfo: { referer: 'http://example.com/' } } + ); + expect(result.length).to.equal(1); + expect(result[0].method).to.equal('POST'); + expect(result[0].url).to.equal('https://bidder.adhash.org/rtb?version=1.0&prebid=true'); + expect(result[0].bidRequest).to.equal(bidRequest); + expect(result[0].data).to.have.property('timezone'); + expect(result[0].data).to.have.property('location'); + expect(result[0].data).to.have.property('publisherId'); + expect(result[0].data).to.have.property('size'); + expect(result[0].data).to.have.property('navigator'); + expect(result[0].data).to.have.property('creatives'); + expect(result[0].data).to.have.property('blockedCreatives'); + expect(result[0].data).to.have.property('currentTimestamp'); + expect(result[0].data).to.have.property('recentAds'); + }); + it('should build the request correctly without referer', function () { + const result = spec.buildRequests([ bidRequest ], { gdprConsent: true }); + expect(result.length).to.equal(1); + expect(result[0].method).to.equal('POST'); + expect(result[0].url).to.equal('https://bidder.adhash.org/rtb?version=1.0&prebid=true'); + expect(result[0].bidRequest).to.equal(bidRequest); + expect(result[0].data).to.have.property('timezone'); + expect(result[0].data).to.have.property('location'); + expect(result[0].data).to.have.property('publisherId'); + expect(result[0].data).to.have.property('size'); + expect(result[0].data).to.have.property('navigator'); + expect(result[0].data).to.have.property('creatives'); + expect(result[0].data).to.have.property('blockedCreatives'); + expect(result[0].data).to.have.property('currentTimestamp'); + expect(result[0].data).to.have.property('recentAds'); + }); + }); + + describe('interpretResponse', function () { + const request = { + data: { some: 'data' }, + bidRequest: { + bidId: '12345678901234', + adUnitCode: 'adunit-code', + sizes: [[300, 250]], + params: { + platformURL: 'https://adhash.org/p/struma/' + } + } + }; + + it('should interpret the response correctly', function () { + const serverResponse = { + body: { + creatives: [{ + costEUR: 1.234 + }] + } + }; + const result = spec.interpretResponse(serverResponse, request); + expect(result.length).to.equal(1); + expect(result[0].requestId).to.equal('12345678901234'); + expect(result[0].cpm).to.equal(1.234); + expect(result[0].width).to.equal(300); + expect(result[0].height).to.equal(250); + expect(result[0].creativeId).to.equal('adunit-code'); + expect(result[0].netRevenue).to.equal(true); + expect(result[0].currency).to.equal('EUR'); + expect(result[0].ttl).to.equal(60); + }); + + it('should return empty array when there are no creatives returned', function () { + expect(spec.interpretResponse({body: {creatives: []}}, request).length).to.equal(0); + }); + + it('should return empty array when there is no creatives key in the response', function () { + expect(spec.interpretResponse({body: {}}, request).length).to.equal(0); + }); + + it('should return empty array when something is not right', function () { + expect(spec.interpretResponse(null, request).length).to.equal(0); + }); + }); +}); diff --git a/test/spec/modules/adheseBidAdapter_spec.js b/test/spec/modules/adheseBidAdapter_spec.js index 4d888db269d..2ce40fd7ffb 100644 --- a/test/spec/modules/adheseBidAdapter_spec.js +++ b/test/spec/modules/adheseBidAdapter_spec.js @@ -17,10 +17,9 @@ let minimalBid = function() { } }; -let bidWithParams = function(data, userId) { +let bidWithParams = function(data) { let bid = minimalBid(); bid.params.data = data; - bid.userId = userId; return bid; }; @@ -74,31 +73,33 @@ describe('AdheseAdapter', function () { it('should include requested slots', function () { let req = spec.buildRequests([ minimalBid() ], bidderRequest); - expect(JSON.parse(req.data).slots).to.deep.include({ 'slotname': '_main_page_-leaderboard' }); + expect(JSON.parse(req.data).slots[0].slotname).to.equal('_main_page_-leaderboard'); }); it('should include all extra bid params', function () { let req = spec.buildRequests([ bidWithParams({ 'ag': '25' }) ], bidderRequest); - expect(JSON.parse(req.data).parameters).to.deep.include({ 'ag': [ '25' ] }); + expect(JSON.parse(req.data).slots[0].parameters).to.deep.include({ 'ag': [ '25' ] }); }); - it('should include duplicate bid params once', function () { + it('should assign bid params per slot', function () { let req = spec.buildRequests([ bidWithParams({ 'ag': '25' }), bidWithParams({ 'ag': '25', 'ci': 'gent' }) ], bidderRequest); - expect(JSON.parse(req.data).parameters).to.deep.include({'ag': ['25']}).and.to.deep.include({ 'ci': [ 'gent' ] }); + expect(JSON.parse(req.data).slots[0].parameters).to.deep.include({ 'ag': [ '25' ] }).and.not.to.deep.include({ 'ci': [ 'gent' ] }); + expect(JSON.parse(req.data).slots[1].parameters).to.deep.include({ 'ag': [ '25' ] }).and.to.deep.include({ 'ci': [ 'gent' ] }); }); it('should split multiple target values', function () { let req = spec.buildRequests([ bidWithParams({ 'ci': 'london' }), bidWithParams({ 'ci': 'gent' }) ], bidderRequest); - expect(JSON.parse(req.data).parameters).to.deep.include({ 'ci': [ 'london', 'gent' ] }); + expect(JSON.parse(req.data).slots[0].parameters).to.deep.include({ 'ci': [ 'london' ] }); + expect(JSON.parse(req.data).slots[1].parameters).to.deep.include({ 'ci': [ 'gent' ] }); }); it('should filter out empty params', function () { let req = spec.buildRequests([ bidWithParams({ 'aa': [], 'bb': null, 'cc': '', 'dd': [ '', '' ], 'ee': [ 0, 1, null ], 'ff': 0, 'gg': [ 'x', 'y', '' ] }) ], bidderRequest); - let params = JSON.parse(req.data).parameters; + let params = JSON.parse(req.data).slots[0].parameters; expect(params).to.not.have.any.keys('aa', 'bb', 'cc', 'dd'); expect(params).to.deep.include({ 'ee': [ 0, 1 ], 'ff': [ 0 ], 'gg': [ 'x', 'y' ] }); }); @@ -115,10 +116,19 @@ describe('AdheseAdapter', function () { expect(JSON.parse(req.data).parameters).to.deep.include({ 'xf': [ 'aHR0cDovL3ByZWJpZC5vcmcvZGV2LWRvY3Mvc3ViamVjdHM_X2Q9MQ' ] }); }); - it('should include id5 id as /x5 param', function () { - let req = spec.buildRequests([ bidWithParams({}, { 'id5id': { 'uid': 'ID5-1234567890' } }) ], bidderRequest); + it('should include eids', function () { + let bid = minimalBid(); + bid.userIdAsEids = [{ source: 'id5-sync.com', uids: [{ id: 'ID5@59sigaS-...' }] }]; + + let req = spec.buildRequests([ bid ], bidderRequest); + + expect(JSON.parse(req.data).user.ext.eids).to.deep.equal(bid.userIdAsEids); + }); + + it('should not include eids field when userid module disabled', function () { + let req = spec.buildRequests([ minimalBid() ], bidderRequest); - expect(JSON.parse(req.data).parameters).to.deep.include({ 'x5': [ 'ID5-1234567890' ] }); + expect(JSON.parse(req.data)).to.not.have.key('eids'); }); it('should include bids', function () { diff --git a/test/spec/modules/adkernelBidAdapter_spec.js b/test/spec/modules/adkernelBidAdapter_spec.js index 4d3dca7f344..0d772423a22 100644 --- a/test/spec/modules/adkernelBidAdapter_spec.js +++ b/test/spec/modules/adkernelBidAdapter_spec.js @@ -556,7 +556,7 @@ describe('Adkernel adapter', function () { describe('adapter configuration', () => { it('should have aliases', () => { - expect(spec.aliases).to.have.lengthOf(9); + expect(spec.aliases).to.have.lengthOf(12); }); }); diff --git a/test/spec/modules/admixerBidAdapter_spec.js b/test/spec/modules/admixerBidAdapter_spec.js index dfadf1f95d5..1321d81d536 100644 --- a/test/spec/modules/admixerBidAdapter_spec.js +++ b/test/spec/modules/admixerBidAdapter_spec.js @@ -1,9 +1,11 @@ import {expect} from 'chai'; import {spec} from 'modules/admixerBidAdapter.js'; import {newBidder} from 'src/adapters/bidderFactory.js'; +import {config} from '../../../src/config.js'; const BIDDER_CODE = 'admixer'; const ENDPOINT_URL = 'https://inv-nets.admixer.net/prebid.1.1.aspx'; +const ENDPOINT_URL_CUSTOM = 'https://custom.admixer.net/prebid.aspx'; const ZONE_ID = '2eb6bd58-865c-47ce-af7f-a918108c3fd2'; describe('AdmixerAdapter', function () { @@ -57,6 +59,7 @@ describe('AdmixerAdapter', function () { } ]; let bidderRequest = { + bidderCode: BIDDER_CODE, refererInfo: { referer: 'https://example.com' } @@ -74,6 +77,16 @@ describe('AdmixerAdapter', function () { expect(request.url).to.equal(ENDPOINT_URL); expect(request.method).to.equal('GET'); }); + + it('sends bid request to CUSTOM_ENDPOINT via GET', function () { + config.setBidderConfig({ + bidders: [BIDDER_CODE], // one or more bidders + config: {[BIDDER_CODE]: {endpoint_url: ENDPOINT_URL_CUSTOM}} + }); + const request = config.runWithBidder(BIDDER_CODE, () => spec.buildRequests(validRequest, bidderRequest)); + expect(request.url).to.equal(ENDPOINT_URL_CUSTOM); + expect(request.method).to.equal('GET'); + }); }); describe('interpretResponse', function () { diff --git a/test/spec/modules/adprimeBidAdapter_spec.js b/test/spec/modules/adprimeBidAdapter_spec.js index fe05634baae..8627941dc80 100644 --- a/test/spec/modules/adprimeBidAdapter_spec.js +++ b/test/spec/modules/adprimeBidAdapter_spec.js @@ -284,4 +284,14 @@ describe('AdprimebBidAdapter', function () { expect(serverResponses).to.be.an('array').that.is.empty; }); }); + describe('getUserSyncs', function () { + let userSync = spec.getUserSyncs(); + it('Returns valid URL and type', function () { + expect(userSync).to.be.an('array').with.lengthOf(1); + expect(userSync[0].type).to.exist; + expect(userSync[0].url).to.exist; + expect(userSync[0].type).to.be.equal('image'); + expect(userSync[0].url).to.be.equal('https://delta.adprime.com/?c=rtb&m=sync'); + }); + }); }); diff --git a/test/spec/modules/adtelligentBidAdapter_spec.js b/test/spec/modules/adtelligentBidAdapter_spec.js index 62449771416..67372216e96 100644 --- a/test/spec/modules/adtelligentBidAdapter_spec.js +++ b/test/spec/modules/adtelligentBidAdapter_spec.js @@ -11,8 +11,14 @@ const EXPECTED_ENDPOINTS = [ 'https://ghb.adtelligent.com/v2/auction/' ]; const aliasEP = { - appaloosa: 'https://hb.appaloosa.media/v2/auction/' + appaloosa: 'https://ghb.hb.appaloosa.media/v2/auction/', + appaloosa_publisherSuffix: 'https://ghb.hb.appaloosa.media/v2/auction/', + onefiftytwomedia: 'https://ghb.ads.152media.com/v2/auction/', + mediafuse: 'https://ghb.hbmp.mediafuse.com/v2/auction/', + navelix: 'https://ghb.hb.navelix.com/v2/auction/', }; + +const DEFAULT_ADATPER_REQ = { bidderCode: 'adtelligent' }; const DISPLAY_REQUEST = { 'bidder': 'adtelligent', 'params': { @@ -244,10 +250,10 @@ describe('adtelligentBidAdapter', () => { let videoBidRequests = [VIDEO_REQUEST]; let displayBidRequests = [DISPLAY_REQUEST]; let videoAndDisplayBidRequests = [DISPLAY_REQUEST, VIDEO_REQUEST]; - const displayRequest = spec.buildRequests(displayBidRequests, {}); - const videoRequest = spec.buildRequests(videoBidRequests, {}); - const videoAndDisplayRequests = spec.buildRequests(videoAndDisplayBidRequests, {}); - const rotatingRequest = spec.buildRequests(displayBidRequests, {}); + const displayRequest = spec.buildRequests(displayBidRequests, DEFAULT_ADATPER_REQ); + const videoRequest = spec.buildRequests(videoBidRequests, DEFAULT_ADATPER_REQ); + const videoAndDisplayRequests = spec.buildRequests(videoAndDisplayBidRequests, DEFAULT_ADATPER_REQ); + const rotatingRequest = spec.buildRequests(displayBidRequests, DEFAULT_ADATPER_REQ); it('rotates endpoints', () => { const bidReqUrls = [displayRequest[0], videoRequest[0], videoAndDisplayRequests[0], rotatingRequest[0]].map(br => br.url); expect(bidReqUrls).to.deep.equal(EXPECTED_ENDPOINTS); @@ -276,7 +282,7 @@ describe('adtelligentBidAdapter', () => { expect(videoAndDisplayRequests.every(comparator)).to.be.true; }); it('forms correct ADPOD request', () => { - const pbBidReqData = spec.buildRequests([ADPOD_REQUEST], {})[0].data; + const pbBidReqData = spec.buildRequests([ADPOD_REQUEST], DEFAULT_ADATPER_REQ)[0].data; const impRequest = pbBidReqData.BidRequests[0] expect(impRequest.AdType).to.be.equal('video'); expect(impRequest.Adpod).to.be.a('object'); @@ -289,7 +295,8 @@ describe('adtelligentBidAdapter', () => { CallbackId: '84ab500420319d', AdType: 'video', Aid: 12345, - Sizes: '480x360,640x480' + Sizes: '480x360,640x480', + PlacementId: 'adunit-code' }; expect(data.BidRequests[0]).to.deep.equal(eq); }); @@ -301,7 +308,8 @@ describe('adtelligentBidAdapter', () => { CallbackId: '84ab500420319d', AdType: 'display', Aid: 12345, - Sizes: '300x250' + Sizes: '300x250', + PlacementId: 'adunit-code' }; expect(data.BidRequests[0]).to.deep.equal(eq); @@ -313,12 +321,14 @@ describe('adtelligentBidAdapter', () => { CallbackId: '84ab500420319d', AdType: 'display', Aid: 12345, - Sizes: '300x250' + Sizes: '300x250', + PlacementId: 'adunit-code' }, { CallbackId: '84ab500420319d', AdType: 'video', Aid: 12345, - Sizes: '480x360,640x480' + Sizes: '480x360,640x480', + PlacementId: 'adunit-code' }] expect(bidRequests.BidRequests).to.deep.equal(expectedBidReqs); diff --git a/test/spec/modules/adtrueBidAdapter_spec.js b/test/spec/modules/adtrueBidAdapter_spec.js new file mode 100644 index 00000000000..a7732d6aeb7 --- /dev/null +++ b/test/spec/modules/adtrueBidAdapter_spec.js @@ -0,0 +1,370 @@ +import {expect} from 'chai' +import {spec} from 'modules/adtrueBidAdapter.js' +import {newBidder} from 'src/adapters/bidderFactory.js' +import * as utils from '../../../src/utils.js'; +import {config} from 'src/config.js'; + +describe('AdTrueBidAdapter', function () { + const adapter = newBidder(spec) + let bidRequests; + let bidResponses; + beforeEach(function () { + bidRequests = [ + { + bidder: 'adtrue', + mediaTypes: { + banner: { + sizes: [[300, 250]] + } + }, + params: { + publisherId: '1212', + zoneId: '21423', + reserve: 0.2 + }, + placementCode: 'adunit-code-1', + sizes: [[300, 250]], + bidId: '23acc48ad47af5', + requestId: '0fb4905b-9456-4152-86be-c6f6d259ba99', + bidderRequestId: '1c56ad30b9b8ca8', + transactionId: '92489f71-1bf2-49a0-adf9-000cea934729', + schain: { + 'ver': '1.0', + 'complete': 1, + 'nodes': [ + { + 'asi': 'indirectseller.com', + 'sid': '00001', + 'hp': 1 + }, + + { + 'asi': 'indirectseller-2.com', + 'sid': '00002', + 'hp': 2 + } + ] + } + } + ]; + bidResponses = { + 'body': { + 'id': '1610681506302', + 'seatbid': [ + { + 'bid': [ + { + 'id': '1', + 'impid': '201fb513ca24e9', + 'price': 2.880000114440918, + 'burl': 'https://hb.adtrue.com/prebid/win-notify?impid=1610681506302&wp=${AUCTION_PRICE}', + 'adm': '', + 'adid': '1610681506302', + 'adomain': [ + 'adtrue.com' + ], + 'cid': 'f6l0r6n', + 'crid': 'abc77au4', + 'attr': [], + 'w': 300, + 'h': 250 + } + ], + 'seat': 'adtrue', + 'group': 0 + } + ], + 'bidid': '1610681506302', + 'cur': 'USD' + } + }; + }); + + describe('.code', function () { + it('should return a bidder code of adtrue', function () { + expect(spec.code).to.equal('adtrue') + }) + }) + + describe('inherited functions', function () { + it('should exist and be a function', function () { + expect(adapter.callBids).to.exist.and.to.be.a('function') + }) + }) + describe('implementation', function () { + describe('Bid validations', function () { + it('valid bid case', function () { + let validBid = { + bidder: 'adtrue', + params: { + zoneId: '21423', + publisherId: '1212' + } + }, + isValid = spec.isBidRequestValid(validBid); + expect(isValid).to.equal(true); + }); + it('invalid bid case: publisherId not passed', function () { + let validBid = { + bidder: 'adtrue', + params: { + zoneId: '21423' + } + }, + isValid = spec.isBidRequestValid(validBid); + expect(isValid).to.equal(false); + }); + it('valid bid case: zoneId is not passed', function () { + let validBid = { + bidder: 'adtrue', + params: { + publisherId: '1212' + } + }, + isValid = spec.isBidRequestValid(validBid); + expect(isValid).to.equal(false); + }); + it('should return false if there are no params', () => { + const bid = { + 'bidder': 'adtrue', + 'adUnitCode': 'adunit-code', + 'mediaTypes': { + banner: { + sizes: [[300, 250]] + } + }, + 'bidId': '30b31c1838de1e', + 'bidderRequestId': '22edbae2733bf6', + 'auctionId': '1d1a030790a475', + }; + expect(spec.isBidRequestValid(bid)).to.equal(false); + }); + it('should return false if there is no publisherId param', () => { + const bid = { + 'bidder': 'adtrue', + 'adUnitCode': 'adunit-code', + params: { + zoneId: '21423', + }, + 'mediaTypes': { + banner: { + sizes: [[300, 250]] + } + }, + 'bidId': '30b31c1838de1e', + 'bidderRequestId': '22edbae2733bf6', + 'auctionId': '1d1a030790a475', + }; + expect(spec.isBidRequestValid(bid)).to.equal(false); + }); + it('should return false if there is no zoneId param', () => { + const bid = { + 'bidder': 'adtrue', + 'adUnitCode': 'adunit-code', + params: { + publisherId: '1212', + }, + 'mediaTypes': { + banner: { + sizes: [[300, 250]] + } + }, + 'bidId': '30b31c1838de1e', + 'bidderRequestId': '22edbae2733bf6', + 'auctionId': '1d1a030790a475', + }; + expect(spec.isBidRequestValid(bid)).to.equal(false); + }); + it('should return true if the bid is valid', () => { + const bid = { + 'bidder': 'adtrue', + 'adUnitCode': 'adunit-code', + params: { + zoneId: '21423', + publisherId: '1212', + }, + 'mediaTypes': { + banner: { + sizes: [[300, 250]] + } + }, + 'bidId': '30b31c1838de1e', + 'bidderRequestId': '22edbae2733bf6', + 'auctionId': '1d1a030790a475', + }; + expect(spec.isBidRequestValid(bid)).to.equal(true); + }); + }); + describe('Request formation', function () { + it('buildRequests function should not modify original bidRequests object', function () { + let originalBidRequests = utils.deepClone(bidRequests); + let request = spec.buildRequests(bidRequests, { + auctionId: 'new-auction-id' + }); + expect(bidRequests).to.deep.equal(originalBidRequests); + }); + + it('Endpoint/method checking', function () { + let request = spec.buildRequests(bidRequests, { + auctionId: 'new-auction-id' + }); + expect(request.url).to.equal('https://hb.adtrue.com/prebid/auction'); + expect(request.method).to.equal('POST'); + }); + + it('test flag not sent when adtrueTest=true is absent in page url', function () { + let request = spec.buildRequests(bidRequests, { + auctionId: 'new-auction-id' + }); + let data = JSON.parse(request.data); + expect(data.test).to.equal(undefined); + }); + it('Request params check', function () { + let request = spec.buildRequests(bidRequests, { + auctionId: 'new-auction-id' + }); + let data = JSON.parse(request.data); + expect(data.at).to.equal(1); // auction type + expect(data.cur[0]).to.equal('USD'); // currency + expect(data.site.domain).to.be.a('string'); // domain should be set + expect(data.site.publisher.id).to.equal(bidRequests[0].params.publisherId); // publisher Id + expect(data.ext.wrapper.transactionId).to.equal(bidRequests[0].transactionId); // Prebid TransactionId + expect(data.source.tid).to.equal(bidRequests[0].transactionId); // Prebid TransactionId + expect(data.imp[0].id).to.equal(bidRequests[0].bidId); // Prebid bid id is passed as id + expect(data.imp[0].bidfloor).to.equal(bidRequests[0].params.reserve); // reverse + expect(data.imp[0].tagid).to.equal(bidRequests[0].params.zoneId); // zoneId + expect(data.imp[0].banner.w).to.equal(300); // width + expect(data.imp[0].banner.h).to.equal(250); // height + expect(data.source.ext.schain).to.deep.equal(bidRequests[0].schain); + }); + it('Request params check with GDPR Consent', function () { + let bidRequest = { + gdprConsent: { + consentString: 'kjfdniwjnifwenrif3', + gdprApplies: true + } + }; + let request = spec.buildRequests(bidRequests, bidRequest); + let data = JSON.parse(request.data); + expect(data.user.ext.consent).to.equal('kjfdniwjnifwenrif3'); + expect(data.at).to.equal(1); // auction type + expect(data.cur[0]).to.equal('USD'); // currency + expect(data.site.domain).to.be.a('string'); // domain should be set + expect(data.site.publisher.id).to.equal(bidRequests[0].params.publisherId); // publisher Id + expect(data.ext.wrapper.transactionId).to.equal(bidRequests[0].transactionId); // Prebid TransactionId + expect(data.source.tid).to.equal(bidRequests[0].transactionId); // Prebid TransactionId + expect(data.imp[0].id).to.equal(bidRequests[0].bidId); // Prebid bid id is passed as id + expect(data.imp[0].bidfloor).to.equal(parseFloat(bidRequests[0].params.reserve)); // reverse + expect(data.imp[0].tagid).to.equal(bidRequests[0].params.zoneId); // zoneId + expect(data.imp[0].banner.w).to.equal(300); // width + expect(data.imp[0].banner.h).to.equal(250); // height + expect(data.source.ext.schain).to.deep.equal(bidRequests[0].schain); + }); + it('Request params check with USP/CCPA Consent', function () { + let bidRequest = { + uspConsent: '1NYN' + }; + let request = spec.buildRequests(bidRequests, bidRequest); + let data = JSON.parse(request.data); + expect(data.regs.ext.us_privacy).to.equal('1NYN');// USP/CCPAs + expect(data.at).to.equal(1); // auction type + expect(data.cur[0]).to.equal('USD'); // currency + expect(data.site.domain).to.be.a('string'); // domain should be set + expect(data.site.publisher.id).to.equal(bidRequests[0].params.publisherId); // publisher Id + expect(data.ext.wrapper.transactionId).to.equal(bidRequests[0].transactionId); // Prebid TransactionId + expect(data.source.tid).to.equal(bidRequests[0].transactionId); // Prebid TransactionId + expect(data.imp[0].id).to.equal(bidRequests[0].bidId); // Prebid bid id is passed as id + expect(data.imp[0].bidfloor).to.equal(parseFloat(bidRequests[0].params.reserve)); // reverse + expect(data.imp[0].tagid).to.equal(bidRequests[0].params.zoneId); // zoneId + expect(data.imp[0].banner.w).to.equal(300); // width + expect(data.imp[0].banner.h).to.equal(250); // height + expect(data.source.ext.schain).to.deep.equal(bidRequests[0].schain); + }); + + it('should NOT include coppa flag in bid request if coppa config is not present', () => { + const request = spec.buildRequests(bidRequests, {}); + let data = JSON.parse(request.data); + if (data.regs) { + // in case GDPR is set then data.regs will exist + expect(data.regs.coppa).to.equal(undefined); + } else { + expect(data.regs).to.equal(undefined); + } + }); + it('should include coppa flag in bid request if coppa is set to true', () => { + let sandbox = sinon.sandbox.create(); + sandbox.stub(config, 'getConfig').callsFake(key => { + const config = { + 'coppa': true + }; + return config[key]; + }); + const request = spec.buildRequests(bidRequests, {}); + let data = JSON.parse(request.data); + expect(data.regs.coppa).to.equal(1); + sandbox.restore(); + }); + it('should NOT include coppa flag in bid request if coppa is set to false', () => { + let sandbox = sinon.sandbox.create(); + sandbox.stub(config, 'getConfig').callsFake(key => { + const config = { + 'coppa': false + }; + return config[key]; + }); + const request = spec.buildRequests(bidRequests, {}); + let data = JSON.parse(request.data); + if (data.regs) { + // in case GDPR is set then data.regs will exist + expect(data.regs.coppa).to.equal(undefined); + } else { + expect(data.regs).to.equal(undefined); + } + sandbox.restore(); + }); + }); + }); + describe('Response checking', function () { + it('should check for valid response values', function () { + let request = spec.buildRequests(bidRequests, { + auctionId: 'new-auction-id' + }); + let data = JSON.parse(request.data); + let response = spec.interpretResponse(bidResponses, request); + expect(response).to.be.an('array').with.length.above(0); + expect(response[0].requestId).to.equal(bidResponses.body.seatbid[0].bid[0].impid); + expect(response[0].width).to.equal(bidResponses.body.seatbid[0].bid[0].w); + expect(response[0].height).to.equal(bidResponses.body.seatbid[0].bid[0].h); + if (bidResponses.body.seatbid[0].bid[0].crid) { + expect(response[0].creativeId).to.equal(bidResponses.body.seatbid[0].bid[0].crid); + } else { + expect(response[0].creativeId).to.equal(bidResponses.body.seatbid[0].bid[0].id); + } + expect(response[0].currency).to.equal('USD'); + expect(response[0].ttl).to.equal(300); + expect(response[0].meta.clickUrl).to.equal('adtrue.com'); + expect(response[0].meta.advertiserDomains[0]).to.equal('adtrue.com'); + expect(response[0].ad).to.equal(bidResponses.body.seatbid[0].bid[0].adm); + expect(response[0].partnerImpId).to.equal(bidResponses.body.seatbid[0].bid[0].id); + }); + }); + describe('getUserSyncs', function () { + let USER_SYNC_URL_IFRAME = 'https://hb.adtrue.com/prebid/usersync?t=iframe&p=1212'; + let USER_SYNC_URL_IMAGE = 'https://hb.adtrue.com/prebid/usersync?t=img&p=1212'; + let sandbox; + beforeEach(function () { + sandbox = sinon.sandbox.create(); + }); + afterEach(function () { + sandbox.restore(); + }); + it('execute as per config', function () { + expect(spec.getUserSyncs({iframeEnabled: true}, {}, undefined, undefined)).to.deep.equal([{ + type: 'iframe', url: USER_SYNC_URL_IFRAME + }]); + expect(spec.getUserSyncs({iframeEnabled: false}, {}, undefined, undefined)).to.deep.equal([{ + type: 'image', url: USER_SYNC_URL_IMAGE + }]); + }); + }); +}); diff --git a/test/spec/modules/adyoulikeBidAdapter_spec.js b/test/spec/modules/adyoulikeBidAdapter_spec.js index ec8f2f00923..4c108ed9f1d 100644 --- a/test/spec/modules/adyoulikeBidAdapter_spec.js +++ b/test/spec/modules/adyoulikeBidAdapter_spec.js @@ -58,12 +58,83 @@ describe('Adyoulike Adapter', function () { 'mediaTypes': { 'banner': {'sizes': ['300x250'] + }, + 'native': + { 'image': { + 'required': true, + }, + 'title': { + 'required': true, + 'len': 80 + }, + 'cta': { + 'required': false + }, + 'sponsoredBy': { + 'required': true + }, + 'clickUrl': { + 'required': true + }, + 'privacyIcon': { + 'required': false + }, + 'privacyLink': { + 'required': false + }, + 'body': { + 'required': true + }, + 'icon': { + 'required': true, + 'sizes': [] } + }, }, 'transactionId': 'bid_id_0_transaction_id' } ]; + const sentBidNative = { + 'bid_id_0': { + 'PlacementID': 'e622af275681965d3095808561a1e510', + 'TransactionID': 'e8355240-d976-4cd5-a493-640656fe08e8', + 'AvailableSizes': '', + 'Native': { + 'image': { + 'required': true, + 'sizes': [] + }, + 'title': { + 'required': true, + 'len': 80 + }, + 'cta': { + 'required': false + }, + 'sponsoredBy': { + 'required': true + }, + 'clickUrl': { + 'required': true + }, + 'privacyIcon': { + 'required': false + }, + 'privacyLink': { + 'required': false + }, + 'body': { + 'required': true + }, + 'icon': { + 'required': true, + 'sizes': [] + } + } + } + }; + const bidRequestWithDCPlacement = [ { 'bidId': 'bid_id_0', @@ -165,11 +236,12 @@ describe('Adyoulike Adapter', function () { 'Placement': 'placement_0' } ]; + const admSample = "\u003cscript id=\"ayl-prebid-a11a121205932e75e622af275681965d\"\u003e\n(function(){\n\twindow.isPrebid = true\n\tvar prebidResults = /*PREBID*/{\"OnEvents\":{\"CLICK\":[{\"Kind\":\"PIXEL_URL\",\"Url\":\"https://testPixelCLICK.com/fake\"}],\"IMPRESSION\":[{\"Kind\":\"PIXEL_URL\",\"Url\":\"https://testPixelIMP.com/fake\"},{\"Kind\":\"JAVASCRIPT_URL\",\"Url\":\"https://testJsIMP.com/fake.js\"}]},\"Disabled\":false,\"Attempt\":\"a11a121205932e75e622af275681965d\",\"ApiPrefix\":\"https://fo-api.omnitagjs.com/fo-api\",\"TrackingPrefix\":\"https://tracking.omnitagjs.com/tracking\",\"DynamicPrefix\":\"https://tag-dyn.omnitagjs.com/fo-dyn\",\"StaticPrefix\":\"https://fo-static.omnitagjs.com/fo-static\",\"BlobPrefix\":\"https://fo-api.omnitagjs.com/fo-api/blobs\",\"SspPrefix\":\"https://fo-ssp.omnitagjs.com/fo-ssp\",\"VisitorPrefix\":\"https://visitor.omnitagjs.com/visitor\",\"Trusted\":true,\"Placement\":\"e622af275681965d3095808561a1e510\",\"PlacementAccess\":\"ALL\",\"Site\":\"6e2df7a92203c3c7a25561ed63f25a27\",\"Lang\":\"EN\",\"SiteLogo\":null,\"HasSponsorImage\":false,\"ResizeIframe\":true,\"IntegrationConfig\":{\"Kind\":\"WIDGET\",\"Widget\":{\"ExtraStyleSheet\":\"\",\"Placeholders\":{\"Body\":{\"Color\":{\"R\":77,\"G\":21,\"B\":82,\"A\":100},\"BackgroundColor\":{\"R\":255,\"G\":255,\"B\":255,\"A\":100},\"FontFamily\":\"Lato\",\"Width\":\"100%\",\"Align\":\"\",\"BoxShadow\":true},\"CallToAction\":{\"Color\":{\"R\":26,\"G\":157,\"B\":212,\"A\":100}},\"Description\":{\"Length\":130},\"Image\":{\"Width\":600,\"Height\":600,\"Lowres\":false,\"Raw\":false},\"Size\":{\"Height\":\"250px\",\"Width\":\"300px\"},\"Sponsor\":{\"Color\":{\"R\":35,\"G\":35,\"B\":35,\"A\":100},\"Label\":true,\"WithoutLogo\":false},\"Title\":{\"Color\":{\"R\":219,\"G\":181,\"B\":255,\"A\":100}}},\"Selector\":{\"Kind\":\"CSS\",\"Css\":\"#ayl-prebid-a11a121205932e75e622af275681965d\"},\"Insertion\":\"AFTER\",\"ClickFormat\":true,\"Creative20\":true,\"WidgetKind\":\"CREATIVE_TEMPLATE_4\"}},\"Legal\":\"Sponsored\",\"ForcedCampaign\":\"f1c80d4bb5643c222ae8de75e9b2f991\",\"ForcedTrack\":\"\",\"ForcedCreative\":\"\",\"ForcedSource\":\"\",\"DisplayMode\":\"DEFAULT\",\"Campaign\":\"f1c80d4bb5643c222ae8de75e9b2f991\",\"CampaignAccess\":\"ALL\",\"CampaignKind\":\"AD_TRAFFIC\",\"DataSource\":\"LOCAL\",\"DataSourceUrl\":\"\",\"DataSourceOnEventsIsolated\":false,\"DataSourceWithoutCookie\":false,\"Content\":{\"Preview\":{\"Thumbnail\":{\"Image\":{\"Kind\":\"EXTERNAL\",\"Data\":{\"External\":{\"Url\":\"https://tag-dyn.omnitagjs.com/fo-dyn/native/preview/image?key=fd4362d35bb174d6f1c80d4bb5643c22\\u0026kind=INTERNAL\\u0026ztop=0.000000\\u0026zleft=0.000000\\u0026zwidth=0.333333\\u0026zheight=1.000000\\u0026width=[width]\\u0026height=[height]\"}},\"ZoneTop\":0,\"ZoneLeft\":0,\"ZoneWidth\":1,\"ZoneHeight\":1,\"Smart\":false,\"NoTransform\":false,\"Quality\":\"NORMAL\"}},\"Text\":{\"CALLTOACTION\":\"Click here to learn more\",\"DESCRIPTION\":\"Considérant l'extrémité conjoncturelle, il serait bon d'anticiper toutes les voies de bon sens.\",\"SPONSOR\":\"Tested by\",\"TITLE\":\"Adserver Traffic Redirect Internal\"},\"Sponsor\":{\"Name\":\"QA Team\"},\"Credit\":{\"Logo\":{\"Resource\":{\"Kind\":\"EXTERNAL\",\"Data\":{\"External\":{\"Url\":\"https://fo-static.omnitagjs.com/fo-static/native/images/info-ayl.png\"}},\"ZoneTop\":0,\"ZoneLeft\":0,\"ZoneWidth\":1,\"ZoneHeight\":1,\"Smart\":false,\"NoTransform\":false,\"Quality\":\"NORMAL\"}},\"Url\":\"https://blobs.omnitagjs.com/adchoice/\"}},\"Landing\":{\"Url\":\"https://www.w3.org/People/mimasa/test/xhtml/entities/entities-11.xhtml#lat1\",\"LegacyTracking\":false},\"ViewButtons\":{\"Close\":{\"Skip\":6000}},\"InternalContentFields\":{\"AnimatedImage\":false}},\"AdDomain\":\"adyoulike.com\",\"Opener\":\"REDIRECT\",\"PerformUITriggers\":[\"CLICK\"],\"RedirectionTarget\":\"TAB\"}/*PREBID*/;\n\tvar insertAds = function insertAds() {\insertAds();\n\t}\n})();\n\u003c/script\u003e"; const responseWithSinglePlacement = [ { 'BidID': 'bid_id_0', 'Placement': 'placement_0', - 'Ad': 'placement_0', + 'Ad': admSample, 'Price': 0.5, 'Height': 600, } @@ -214,15 +286,34 @@ describe('Adyoulike Adapter', function () { 'transactionId': 'bid_id_1_transaction_id' }; + let nativeBid = { + 'bidId': 'bid_id_1', + 'bidder': 'adyoulike', + 'placementCode': 'adunit/hb-1', + 'params': { + 'placement': 'placement_1' + }, + mediaTypes: { + native: { + + } + }, + 'transactionId': 'bid_id_1_transaction_id' + }; + it('should return true when required params found', function () { - expect(spec.isBidRequestValid(bid)).to.equal(true); + expect(!!spec.isBidRequestValid(bid)).to.equal(true); + }); + + it('should return true when required params found for native ad', function () { + expect(!!spec.isBidRequestValid(nativeBid)).to.equal(true); }); it('should return false when required params are not passed', function () { let bid = Object.assign({}, bid); delete bid.size; - expect(spec.isBidRequestValid(bid)).to.equal(false); + expect(!!spec.isBidRequestValid(bid)).to.equal(false); }); it('should return false when required params are not passed', function () { @@ -231,7 +322,7 @@ describe('Adyoulike Adapter', function () { bid.params = { 'placement': 0 }; - expect(spec.isBidRequestValid(bid)).to.equal(false); + expect(!!spec.isBidRequestValid(bid)).to.equal(false); }); }); @@ -384,7 +475,7 @@ describe('Adyoulike Adapter', function () { expect(result.length).to.equal(1); expect(result[0].cpm).to.equal(0.5); - expect(result[0].ad).to.equal('placement_0'); + expect(result[0].ad).to.equal(admSample); expect(result[0].width).to.equal(300); expect(result[0].height).to.equal(600); }); @@ -405,5 +496,47 @@ describe('Adyoulike Adapter', function () { expect(result[1].width).to.equal(400); expect(result[1].height).to.equal(250); }); + + it('receive reponse with Native ad', function () { + serverResponse.body = responseWithSinglePlacement; + let result = spec.interpretResponse(serverResponse, {data: '{"Bids":' + JSON.stringify(sentBidNative) + '}'}); + + expect(result.length).to.equal(1); + + expect(result).to.deep.equal([{ + cpm: 0.5, + creativeId: undefined, + currency: 'USD', + netRevenue: true, + requestId: 'bid_id_0', + ttl: 3600, + mediaType: 'native', + native: { + body: 'Considérant l\'extrémité conjoncturelle, il serait bon d\'anticiper toutes les voies de bon sens.', + clickTrackers: [ + 'https://testPixelCLICK.com/fake' + ], + clickUrl: 'https://tracking.omnitagjs.com/tracking/ar?event_kind=CLICK&attempt=a11a121205932e75e622af275681965d&campaign=f1c80d4bb5643c222ae8de75e9b2f991&url=https%3A%2F%2Fwww.w3.org%2FPeople%2Fmimasa%2Ftest%2Fxhtml%2Fentities%2Fentities-11.xhtml%23lat1', + cta: 'Click here to learn more', + image: { + height: 600, + url: 'https://blobs.omnitagjs.com/blobs/f1/f1c80d4bb5643c22/fd4362d35bb174d6f1c80d4bb5643c22', + width: 300, + }, + impressionTrackers: [ + 'https://testPixelIMP.com/fake', + 'https://tracking.omnitagjs.com/tracking/pixel?event_kind=IMPRESSION&attempt=a11a121205932e75e622af275681965d&campaign=f1c80d4bb5643c222ae8de75e9b2f991' + ], + javascriptTrackers: [ + 'https://testJsIMP.com/fake.js' + ], + privacyIcon: 'https://fo-static.omnitagjs.com/fo-static/native/images/info-ayl.png', + privacyLink: 'https://blobs.omnitagjs.com/adchoice/', + sponsoredBy: 'QA Team', + title: 'Adserver Traffic Redirect Internal', + } + + }]); + }); }); }); diff --git a/test/spec/modules/aolBidAdapter_spec.js b/test/spec/modules/aolBidAdapter_spec.js index 11e1a317b70..8e74e19f420 100644 --- a/test/spec/modules/aolBidAdapter_spec.js +++ b/test/spec/modules/aolBidAdapter_spec.js @@ -93,7 +93,7 @@ describe('AolAdapter', function () { const USER_ID_DATA = { criteoId: SUPPORTED_USER_ID_SOURCES['criteo.com'], - vmuid: SUPPORTED_USER_ID_SOURCES['verizonmedia.com'], + connectid: SUPPORTED_USER_ID_SOURCES['verizonmedia.com'], idl_env: SUPPORTED_USER_ID_SOURCES['liveramp.com'], lipb: { lipbid: SUPPORTED_USER_ID_SOURCES['liveintent.com'], diff --git a/test/spec/modules/appnexusBidAdapter_spec.js b/test/spec/modules/appnexusBidAdapter_spec.js index 426259639e8..3a3f4effcb8 100644 --- a/test/spec/modules/appnexusBidAdapter_spec.js +++ b/test/spec/modules/appnexusBidAdapter_spec.js @@ -265,6 +265,7 @@ describe('AppNexusAdapter', function () { placementId: '10433394', user: { externalUid: '123', + segments: [123, { id: 987, value: 876 }], foobar: 'invalid' } } @@ -277,6 +278,7 @@ describe('AppNexusAdapter', function () { expect(payload.user).to.exist; expect(payload.user).to.deep.equal({ external_uid: '123', + segments: [{id: 123}, {id: 987, value: 876}] }); }); @@ -782,6 +784,18 @@ describe('AppNexusAdapter', function () { config.getConfig.restore(); }); + it('should set the X-Is-Test customHeader if test flag is enabled', function () { + let bidRequest = Object.assign({}, bidRequests[0]); + sinon.stub(config, 'getConfig') + .withArgs('apn_test') + .returns(true); + + const request = spec.buildRequests([bidRequest]); + expect(request.options.customHeaders).to.deep.equal({'X-Is-Test': 1}); + + config.getConfig.restore(); + }); + it('should set withCredentials to false if purpose 1 consent is not given', function () { let consentString = 'BOJ8RZsOJ8RZsABAB8AAAAAZ+A=='; let bidderRequest = { @@ -808,11 +822,13 @@ describe('AppNexusAdapter', function () { expect(request.options).to.deep.equal({withCredentials: false}); }); - it('should populate eids when ttd id and criteo is available', function () { + it('should populate eids when supported userIds are available', function () { const bidRequest = Object.assign({}, bidRequests[0], { userId: { tdid: 'sample-userid', - criteoId: 'sample-criteo-userid' + criteoId: 'sample-criteo-userid', + netId: 'sample-netId-userid', + idl_env: 'sample-idl-userid' } }); @@ -828,6 +844,16 @@ describe('AppNexusAdapter', function () { source: 'criteo.com', id: 'sample-criteo-userid', }); + + expect(payload.eids).to.deep.include({ + source: 'netid.de', + id: 'sample-netId-userid', + }); + + expect(payload.eids).to.deep.include({ + source: 'liveramp.com', + id: 'sample-idl-userid' + }) }); it('should populate iab_support object at the root level if omid support is detected', function () { diff --git a/test/spec/modules/astraoneBidAdapter_spec.js b/test/spec/modules/astraoneBidAdapter_spec.js index e422f64b570..0e545081869 100644 --- a/test/spec/modules/astraoneBidAdapter_spec.js +++ b/test/spec/modules/astraoneBidAdapter_spec.js @@ -14,7 +14,7 @@ function getSlotConfigs(mediaTypes, params) { describe('AstraOne Adapter', function() { describe('isBidRequestValid method', function() { - const PLACE_ID = '5af45ad34d506ee7acad0c26'; + const PLACE_ID = '5f477bf94d506ebe2c4240f3'; const IMAGE_URL = 'https://creative.astraone.io/files/default_image-1-600x400.jpg'; describe('returns true', function() { @@ -176,21 +176,23 @@ describe('AstraOne Adapter', function() { describe('the bid is a banner', function() { it('should return a banner bid', function() { const serverResponse = { - body: [ - { - bidId: '2df8c0733f284e', - price: 0.5, - currency: 'USD', - content: { - content: 'html', - actionUrls: {}, - seanceId: '123123' - }, - width: 100, - height: 100, - ttl: 360 - } - ] + body: { + bids: [ + { + bidId: '2df8c0733f284e', + price: 0.5, + currency: 'USD', + content: { + content: 'html', + actionUrls: {}, + seanceId: '123123' + }, + width: 100, + height: 100, + ttl: 360 + } + ] + } } const bids = spec.interpretResponse(serverResponse) expect(bids.length).to.equal(1) diff --git a/test/spec/modules/atsAnalyticsAdapter_spec.js b/test/spec/modules/atsAnalyticsAdapter_spec.js index 84206337fad..59b9105925a 100644 --- a/test/spec/modules/atsAnalyticsAdapter_spec.js +++ b/test/spec/modules/atsAnalyticsAdapter_spec.js @@ -2,27 +2,38 @@ import atsAnalyticsAdapter from '../../../modules/atsAnalyticsAdapter.js'; import { expect } from 'chai'; import adapterManager from 'src/adapterManager.js'; import {server} from '../../mocks/xhr.js'; -import {checkUserBrowser, browserIsChrome, browserIsEdge, browserIsSafari, browserIsFirefox} from '../../../modules/atsAnalyticsAdapter.js'; +import {parseBrowser} from '../../../modules/atsAnalyticsAdapter.js'; +import {getStorageManager} from '../../../src/storageManager.js'; +import {analyticsUrl} from '../../../modules/atsAnalyticsAdapter.js'; + let events = require('src/events'); let constants = require('src/constants.json'); +export const storage = getStorageManager(); + describe('ats analytics adapter', function () { beforeEach(function () { sinon.stub(events, 'getEvents').returns([]); + storage.setCookie('_lr_env_src_ats', 'true', 'Thu, 01 Jan 1970 00:00:01 GMT'); }); afterEach(function () { events.getEvents.restore(); + atsAnalyticsAdapter.getUserAgent.restore(); atsAnalyticsAdapter.disableAnalytics(); }); describe('track', function () { it('builds and sends request and response data', function () { sinon.stub(atsAnalyticsAdapter, 'shouldFireRequest').returns(true); + sinon.stub(atsAnalyticsAdapter, 'getUserAgent').returns('Mozilla/5.0 (Macintosh; Intel Mac OS X 10_7_5) AppleWebKit/536.25 (KHTML, like Gecko) Version/6.0 Safari/536.25'); + let now = new Date(); + now.setTime(now.getTime() + 3600000); + storage.setCookie('_lr_env_src_ats', 'true', now.toUTCString()); + storage.setCookie('_lr_sampling_rate', '10', now.toUTCString()); let initOptions = { - pid: '10433394', - host: 'https://example.com/dev', + pid: '10433394' }; let auctionTimestamp = 1496510254326; @@ -74,13 +85,15 @@ describe('ats analytics adapter', function () { let expectedAfterBid = { 'Data': [{ 'has_envelope': true, + 'adapter_version': 1, 'bidder': 'appnexus', 'bid_id': '30c77d079cdf17', 'auction_id': 'a5b849e5-87d7-4205-8300-d063084fcfb7', - 'user_browser': checkUserBrowser(), + 'user_browser': parseBrowser(), 'user_platform': navigator.platform, 'auction_start': '2020-02-03T14:14:25.161Z', 'domain': window.location.hostname, + 'envelope_source': true, 'pid': '10433394', 'response_time_stamp': '2020-02-03T14:23:11.978Z', 'currency': 'USD', @@ -132,76 +145,37 @@ describe('ats analytics adapter', function () { events.emit(constants.EVENTS.AUCTION_END, {}); let requests = server.requests.filter(req => { - return req.url.indexOf(initOptions.host) > -1; + return req.url.indexOf(analyticsUrl) > -1; }); + expect(requests.length).to.equal(1); let realAfterBid = JSON.parse(requests[0].requestBody); - // Step 6: assert real data after bid and expected data expect(realAfterBid['Data']).to.deep.equal(expectedAfterBid['Data']); - // check that the host and publisher ID is configured via options - expect(atsAnalyticsAdapter.context.host).to.equal(initOptions.host); + // check that the publisher ID is configured via options expect(atsAnalyticsAdapter.context.pid).to.equal(initOptions.pid); }) - it('check browser is not safari', function () { - window.safari = undefined; - let browser = browserIsSafari(); - expect(browser).to.equal(false); - }) it('check browser is safari', function () { - window.safari = {}; - let browser = browserIsSafari(); + sinon.stub(atsAnalyticsAdapter, 'getUserAgent').returns('Mozilla/5.0 (Macintosh; Intel Mac OS X 10_7_5) AppleWebKit/536.25 (KHTML, like Gecko) Version/6.0 Safari/536.25'); + let browser = parseBrowser(); expect(browser).to.equal('Safari'); }) - it('check browser is not chrome', function () { - window.chrome = { - app: undefined, - webstore: undefined, - runtime: undefined - }; - let browser = browserIsChrome(); - expect(browser).to.equal(false); - }) it('check browser is chrome', function () { - window.chrome = { - app: {}, - webstore: {}, - runtime: {} - }; - let browser = browserIsChrome(); + sinon.stub(atsAnalyticsAdapter, 'getUserAgent').returns('Mozilla/5.0 (iPhone; CPU iPhone OS 13_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) CriOS/80.0.3987.95 Mobile/15E148 Safari/604.1'); + let browser = parseBrowser(); expect(browser).to.equal('Chrome'); }) it('check browser is edge', function () { - Object.defineProperty(window, 'StyleMedia', { - value: {}, - writable: true - }); - Object.defineProperty(document, 'documentMode', { - value: undefined, - writable: true - }); - let browser = browserIsEdge(); - expect(browser).to.equal('Edge'); - }) - it('check browser is not edge', function () { - Object.defineProperty(document, 'documentMode', { - value: {}, - writable: true - }); - let browser = browserIsEdge(); - expect(browser).to.equal(false); + sinon.stub(atsAnalyticsAdapter, 'getUserAgent').returns('Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.74 Safari/537.36 Edg/79.0.309.43'); + let browser = parseBrowser(); + expect(browser).to.equal('Microsoft Edge'); }) it('check browser is firefox', function () { - global.InstallTrigger = {}; - let browser = browserIsFirefox(); + sinon.stub(atsAnalyticsAdapter, 'getUserAgent').returns('Mozilla/5.0 (iPhone; CPU OS 13_3_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) FxiOS/23.0 Mobile/15E148 Safari/605.1.15'); + let browser = parseBrowser(); expect(browser).to.equal('Firefox'); }) - it('check browser is not firefox', function () { - global.InstallTrigger = undefined; - let browser = browserIsFirefox(); - expect(browser).to.equal(false); - }) }) }) diff --git a/test/spec/modules/axonixBidAdapter_spec.js b/test/spec/modules/axonixBidAdapter_spec.js new file mode 100644 index 00000000000..632e080f83d --- /dev/null +++ b/test/spec/modules/axonixBidAdapter_spec.js @@ -0,0 +1,372 @@ +import { expect } from 'chai'; +import { spec } from 'modules/axonixBidAdapter'; +import { newBidder } from 'src/adapters/bidderFactory'; +import * as utils from 'src/utils'; + +describe('AxonixBidAdapter', function () { + const adapter = newBidder(spec); + + const SUPPLY_ID_1 = '91fd110a-5685-11eb-8db6-a7e0eeefbbc7'; + const SUPPLY_ID_2 = '22de2092-568b-11eb-bae3-cfa975dc72aa'; + const REGION_1 = 'us-east-1'; + const REGION_2 = 'eu-west-1'; + + const BANNER_REQUEST = { + adUnitCode: 'ad_code', + bidId: 'abcd1234', + mediaTypes: { + banner: { + sizes: [ + [300, 250], + [300, 200] + ] + } + }, + bidder: 'axonix', + params: { + supplyId: SUPPLY_ID_1, + region: REGION_1 + }, + requestId: 'q4owht8ofqi3ulwsd', + transactionId: 'fvpq3oireansdwo' + }; + + const VIDEO_REQUEST = { + adUnitCode: 'ad_code', + bidId: 'abcd1234', + mediaTypes: { + video: { + context: 'outstream', + mimes: ['video/mp4'], + playerSize: [400, 300], + renderer: { + url: 'https://url.com', + backupOnly: true, + render: () => true + }, + } + }, + bidder: 'axonix', + params: { + supplyId: SUPPLY_ID_1, + region: REGION_1 + }, + requestId: 'q4owht8ofqi3ulwsd', + transactionId: 'fvpq3oireansdwo' + }; + + const BIDDER_REQUEST = { + bidderCode: 'axonix', + auctionId: '18fd8b8b0bd757', + bidderRequestId: '418b37f85e772c', + timeout: 3000, + gdprConsent: { + consentString: 'BOKAVy4OKAVy4ABAB8AAAAAZ+A==', + gdprApplies: true + }, + refererInfo: { + referer: 'https://www.prebid.org', + canonicalUrl: 'https://www.prebid.org/the/link/to/the/page' + } + }; + + const BANNER_RESPONSE = { + requestId: 'f08b3a8dcff747eabada295dcf94eee0', + cpm: 6, + currency: 'USD', + width: 300, + height: 250, + ad: '', + creativeId: 'abc', + netRevenue: false, + meta: { + networkId: 'nid', + advertiserDomains: [ + 'https://the.url' + ], + secondaryCatIds: [ + 'IAB1' + ], + mediaType: 'banner' + }, + nurl: 'https://win.url' + }; + + const VIDEO_RESPONSE = { + requestId: 'f08b3a8dcff747eabada295dcf94eee0', + cpm: 6, + currency: 'USD', + width: 300, + height: 250, + ad: '', + creativeId: 'abc', + netRevenue: false, + meta: { + networkId: 'nid', + advertiserDomains: [ + 'https://the.url' + ], + secondaryCatIds: [ + 'IAB1' + ], + mediaType: 'video' + }, + nurl: 'https://win.url' + }; + + describe('inherited functions', function () { + it('exists and is a function', function () { + expect(adapter.callBids).to.exist.and.to.be.a('function'); + }); + }); + + describe('isBidRequestValid', function () { + let validBids = [ + { + bidder: 'axonix', + params: { + supplyId: SUPPLY_ID_1, + region: REGION_1 + }, + }, + { + bidder: 'axonix', + params: { + supplyId: SUPPLY_ID_2, + region: REGION_2 + }, + future_parameter: { + future: 'ididid' + } + }, + ]; + + let invalidBids = [ + { + bidder: 'axonix', + params: {}, + }, + { + bidder: 'axonix', + }, + ]; + + it('should accept valid bids', function () { + for (let bid of validBids) { + expect(spec.isBidRequestValid(bid)).to.equal(true); + } + }); + + it('should reject invalid bids', function () { + for (let bid of invalidBids) { + expect(spec.isBidRequestValid(bid)).to.equal(false); + } + }); + }); + + describe('buildRequests: can handle banner ad requests', function () { + it('creates ServerRequests with the correct data', function () { + const [request] = spec.buildRequests([BANNER_REQUEST], BIDDER_REQUEST); + + expect(request).to.have.property('url', `https://openrtb-${REGION_1}.axonix.com/supply/prebid/${SUPPLY_ID_1}`); + expect(request).to.have.property('method', 'POST'); + expect(request).to.have.property('data'); + + const { data } = request; + expect(data.app).to.be.undefined; + + expect(data).to.have.property('site'); + expect(data.site).to.have.property('page', 'https://www.prebid.org'); + + expect(data).to.have.property('validBidRequest', BANNER_REQUEST); + expect(data).to.have.property('connectiontype').to.exist; + expect(data).to.have.property('devicetype', 2); + expect(data).to.have.property('bidfloor', 0); + expect(data).to.have.property('dnt', 0); + expect(data).to.have.property('language').to.be.a('string'); + expect(data).to.have.property('prebidVersion').to.be.a('string'); + expect(data).to.have.property('screenHeight').to.be.a('number'); + expect(data).to.have.property('screenWidth').to.be.a('number'); + expect(data).to.have.property('tmax').to.be.a('number'); + expect(data).to.have.property('ua').to.be.a('string'); + }); + + it('creates ServerRequests pointing to the correct region and endpoint if it changes', function () { + const bannerRequests = [utils.deepClone(BANNER_REQUEST), utils.deepClone(BANNER_REQUEST)]; + bannerRequests[0].params.endpoint = 'https://the.url'; + bannerRequests[1].params.endpoint = 'https://the.other.url'; + + const requests = spec.buildRequests(bannerRequests, BIDDER_REQUEST); + + requests.forEach((request, index) => { + expect(request).to.have.property('url', bannerRequests[index].params.endpoint); + }); + }); + + it('creates ServerRequests pointing to default endpoint if missing', function () { + const bannerRequests = [utils.deepClone(BANNER_REQUEST), utils.deepClone(BANNER_REQUEST)]; + bannerRequests[1].params.supplyId = SUPPLY_ID_2; + bannerRequests[1].params.region = REGION_2; + + const requests = spec.buildRequests(bannerRequests, BIDDER_REQUEST); + expect(requests[0]).to.have.property('url', `https://openrtb-${REGION_1}.axonix.com/supply/prebid/${SUPPLY_ID_1}`); + expect(requests[1]).to.have.property('url', `https://openrtb-${REGION_2}.axonix.com/supply/prebid/${SUPPLY_ID_2}`); + }); + + it('creates ServerRequests pointing to default region if missing', function () { + const bannerRequest = utils.deepClone(BANNER_REQUEST); + delete bannerRequest.params.region; + + const requests = spec.buildRequests([bannerRequest], BIDDER_REQUEST); + expect(requests[0]).to.have.property('url', `https://openrtb-${REGION_1}.axonix.com/supply/prebid/${SUPPLY_ID_1}`); + }); + }); + + describe('buildRequests: can handle video ad requests', function () { + it('creates ServerRequests with the correct data', function () { + const [request] = spec.buildRequests([VIDEO_REQUEST], BIDDER_REQUEST); + + expect(request).to.have.property('url', `https://openrtb-${REGION_1}.axonix.com/supply/prebid/${SUPPLY_ID_1}`); + expect(request).to.have.property('method', 'POST'); + expect(request).to.have.property('data'); + + const { data } = request; + expect(data.app).to.be.undefined; + + expect(data).to.have.property('site'); + expect(data.site).to.have.property('page', 'https://www.prebid.org'); + + expect(data).to.have.property('validBidRequest', VIDEO_REQUEST); + expect(data).to.have.property('connectiontype').to.exist; + expect(data).to.have.property('devicetype', 2); + expect(data).to.have.property('bidfloor', 0); + expect(data).to.have.property('dnt', 0); + expect(data).to.have.property('language').to.be.a('string'); + expect(data).to.have.property('prebidVersion').to.be.a('string'); + expect(data).to.have.property('screenHeight').to.be.a('number'); + expect(data).to.have.property('screenWidth').to.be.a('number'); + expect(data).to.have.property('tmax').to.be.a('number'); + expect(data).to.have.property('ua').to.be.a('string'); + }); + + it('creates ServerRequests pointing to the correct region and endpoint if it changes', function () { + const videoRequests = [utils.deepClone(VIDEO_REQUEST), utils.deepClone(VIDEO_REQUEST)]; + videoRequests[0].params.endpoint = 'https://the.url'; + videoRequests[1].params.endpoint = 'https://the.other.url'; + + const requests = spec.buildRequests(videoRequests, BIDDER_REQUEST); + + requests.forEach((request, index) => { + expect(request).to.have.property('url', videoRequests[index].params.endpoint); + }); + }); + + it('creates ServerRequests pointing to default endpoint if missing', function () { + const videoRequests = [utils.deepClone(VIDEO_REQUEST), utils.deepClone(VIDEO_REQUEST)]; + videoRequests[1].params.supplyId = SUPPLY_ID_2; + videoRequests[1].params.region = REGION_2; + + const requests = spec.buildRequests(videoRequests, BIDDER_REQUEST); + expect(requests[0]).to.have.property('url', `https://openrtb-${REGION_1}.axonix.com/supply/prebid/${SUPPLY_ID_1}`); + expect(requests[1]).to.have.property('url', `https://openrtb-${REGION_2}.axonix.com/supply/prebid/${SUPPLY_ID_2}`); + }); + + it('creates ServerRequests pointing to default region if missing', function () { + const videoRequest = utils.deepClone(VIDEO_REQUEST); + delete videoRequest.params.region; + + const requests = spec.buildRequests([videoRequest], BIDDER_REQUEST); + expect(requests[0]).to.have.property('url', `https://openrtb-${REGION_1}.axonix.com/supply/prebid/${SUPPLY_ID_1}`); + }); + }); + + describe.skip('buildRequests: can handle native ad requests', function () { + it('creates ServerRequests pointing to the correct region and endpoint if it changes', function () { + // loop: + // set supply id + // set region/endpoint in ssp config + // call buildRequests, validate request (url, method, supply id) + expect.fail('Not implemented'); + }); + + it('creates ServerRequests pointing to default endpoint if missing', function () { + // no endpoint in config means default value openrtb.axonix.com + expect.fail('Not implemented'); + }); + + it('creates ServerRequests pointing to default region if missing', function () { + // no region in config means default value us-east-1 + expect.fail('Not implemented'); + }); + }); + + describe('interpretResponse', function () { + it('considers corner cases', function() { + expect(spec.interpretResponse(null)).to.be.an('array').that.is.empty; + expect(spec.interpretResponse()).to.be.an('array').that.is.empty; + }); + + it('ignores unparseable responses', function() { + expect(spec.interpretResponse('invalid')).to.be.an('array').that.is.empty; + expect(spec.interpretResponse(['invalid'])).to.be.an('array').that.is.empty; + expect(spec.interpretResponse([{ invalid: 'object' }])).to.be.an('array').that.is.empty; + }); + + it('parses banner responses', function () { + const response = spec.interpretResponse([BANNER_RESPONSE]); + + expect(response).to.be.an('array').that.is.not.empty; + expect(response[0]).to.equal(BANNER_RESPONSE); + }); + + it('parses 1 video responses', function () { + const response = spec.interpretResponse([VIDEO_RESPONSE]); + + expect(response).to.be.an('array').that.is.not.empty; + expect(response[0]).to.equal(VIDEO_RESPONSE); + }); + + it.skip('parses 1 native responses', function () { + // passing 1 valid native in a response generates an array with 1 correct prebid response + // examine mediaType:native, native element + // check nativeBidIsValid from {@link file://./../../../src/native.js} + expect.fail('Not implemented'); + }); + }); + + describe('onBidWon', function () { + beforeEach(function () { + sinon.stub(utils, 'triggerPixel'); + }); + + afterEach(function () { + utils.triggerPixel.restore(); + }); + + it('called once', function () { + spec.onBidWon(spec.interpretResponse([BANNER_RESPONSE])); + expect(utils.triggerPixel.calledOnce).to.equal(true); + }); + + it('called false', function () { + spec.onBidWon([{ cpm: '2.21' }]); + expect(utils.triggerPixel.called).to.equal(false); + }); + + it('when there is no notification expected server side, none is called', function () { + var response = spec.onBidWon([]); + expect(utils.triggerPixel.called).to.equal(false); + expect(response).to.be.an('undefined') + }); + }); + + describe('onTimeout', function () { + it('banner response', () => { + spec.onTimeout(spec.interpretResponse([BANNER_RESPONSE])); + }); + + it('video response', () => { + spec.onTimeout(spec.interpretResponse([VIDEO_RESPONSE])); + }); + }); +}); diff --git a/test/spec/modules/beachfrontBidAdapter_spec.js b/test/spec/modules/beachfrontBidAdapter_spec.js index 661780ffac0..43c71dd6349 100644 --- a/test/spec/modules/beachfrontBidAdapter_spec.js +++ b/test/spec/modules/beachfrontBidAdapter_spec.js @@ -336,6 +336,7 @@ describe('BeachfrontAdapter', function () { const width = 300; const height = 250; const bidRequest = bidRequests[0]; + bidRequest.params.tagid = '7cd7a7b4-ef3f-4aeb-9565-3627f255fa10'; bidRequest.mediaTypes = { banner: { sizes: [ width, height ] @@ -354,6 +355,7 @@ describe('BeachfrontAdapter', function () { slot: bidRequest.adUnitCode, id: bidRequest.params.appId, bidfloor: bidRequest.params.bidfloor, + tagid: bidRequest.params.tagid, sizes: [{ w: width, h: height }] } ]); diff --git a/test/spec/modules/betweenBidAdapter_spec.js b/test/spec/modules/betweenBidAdapter_spec.js index 94db41ba014..44a8d2cc733 100644 --- a/test/spec/modules/betweenBidAdapter_spec.js +++ b/test/spec/modules/betweenBidAdapter_spec.js @@ -20,7 +20,7 @@ describe('betweenBidAdapterTests', function () { sizes: [[240, 400]] }] let request = spec.buildRequests(bidRequestData); - let req_data = request[0].data; + let req_data = JSON.parse(request.data)[0].data; expect(req_data.bidid).to.equal('bid1234'); }); it('validate itu param', function() { @@ -37,7 +37,7 @@ describe('betweenBidAdapterTests', function () { }]; let request = spec.buildRequests(bidRequestData); - let req_data = request[0].data; + let req_data = JSON.parse(request.data)[0].data; expect(req_data.itu).to.equal('https://something.url'); }); @@ -55,7 +55,7 @@ describe('betweenBidAdapterTests', function () { }]; let request = spec.buildRequests(bidRequestData); - let req_data = request[0].data; + let req_data = JSON.parse(request.data)[0].data; expect(req_data.cur).to.equal('THX'); }); @@ -73,7 +73,7 @@ describe('betweenBidAdapterTests', function () { }]; let request = spec.buildRequests(bidRequestData); - let req_data = request[0].data; + let req_data = JSON.parse(request.data)[0].data; expect(req_data.subid).to.equal(1138); }); @@ -91,7 +91,7 @@ describe('betweenBidAdapterTests', function () { }]; let request = spec.buildRequests(bidRequestData); - let req_data = request[0].data; + let req_data = JSON.parse(request.data)[0].data; expect(req_data.click3rd).to.equal('https://something.url'); }); @@ -111,7 +111,7 @@ describe('betweenBidAdapterTests', function () { }]; let request = spec.buildRequests(bidRequestData); - let req_data = request[0].data; + let req_data = JSON.parse(request.data)[0].data; expect(req_data['pubside_macro[param]']).to.equal('%26test%3Dtset'); }); @@ -134,7 +134,7 @@ describe('betweenBidAdapterTests', function () { } let request = spec.buildRequests(bidRequestData, bidderRequest); - let req_data = request[0].data; + let req_data = JSON.parse(request.data)[0].data; expect(req_data.gdprApplies).to.equal(bidderRequest.gdprConsent.gdprApplies); expect(req_data.consentString).to.equal(bidderRequest.gdprConsent.consentString); @@ -217,8 +217,8 @@ describe('betweenBidAdapterTests', function () { }]; let request = spec.buildRequests(bidRequestData); - let req_data = request[0].data; + let req_data = JSON.parse(request.data)[0].data; - expect(req_data.sizes).to.deep.equal('970x250%2C240x400%2C728x90'); + expect(req_data.sizes).to.deep.equal(['970x250', '240x400', '728x90']) }); }); diff --git a/test/spec/modules/bidViewability_spec.js b/test/spec/modules/bidViewability_spec.js new file mode 100644 index 00000000000..211dec090a5 --- /dev/null +++ b/test/spec/modules/bidViewability_spec.js @@ -0,0 +1,297 @@ +import * as bidViewability from 'modules/bidViewability.js'; +import { config } from 'src/config.js'; +import * as events from 'src/events.js'; +import * as utils from 'src/utils.js'; +import * as sinon from 'sinon'; +import {expect, spy} from 'chai'; +import * as prebidGlobal from 'src/prebidGlobal.js'; +import { EVENTS } from 'src/constants.json'; +import adapterManager, { gdprDataHandler, uspDataHandler } from 'src/adapterManager.js'; +import parse from 'url-parse'; + +const GPT_SLOT = { + getAdUnitPath() { + return '/harshad/Jan/2021/'; + }, + + getSlotElementId() { + return 'DIV-1'; + } +}; + +const PBJS_WINNING_BID = { + 'adUnitCode': '/harshad/Jan/2021/', + 'bidderCode': 'pubmatic', + 'bidder': 'pubmatic', + 'width': 300, + 'height': 250, + 'statusMessage': 'Bid available', + 'adId': 'id', + 'requestId': 1024, + 'source': 'client', + 'no_bid': false, + 'cpm': '1.1495', + 'ttl': 180, + 'creativeId': 'id', + 'netRevenue': true, + 'currency': 'USD', + 'vurls': [ + 'https://domain-1.com/end-point?a=1', + 'https://domain-2.com/end-point/', + 'https://domain-3.com/end-point?a=1' + ] +}; + +describe('#bidViewability', function() { + let gptSlot; + let pbjsWinningBid; + + beforeEach(function() { + gptSlot = Object.assign({}, GPT_SLOT); + pbjsWinningBid = Object.assign({}, PBJS_WINNING_BID); + }); + + describe('isBidAdUnitCodeMatchingSlot', function() { + it('match found by GPT Slot getAdUnitPath', function() { + expect(bidViewability.isBidAdUnitCodeMatchingSlot(pbjsWinningBid, gptSlot)).to.equal(true); + }); + + it('match found by GPT Slot getSlotElementId', function() { + pbjsWinningBid.adUnitCode = 'DIV-1'; + expect(bidViewability.isBidAdUnitCodeMatchingSlot(pbjsWinningBid, gptSlot)).to.equal(true); + }); + + it('match not found', function() { + pbjsWinningBid.adUnitCode = 'DIV-10'; + expect(bidViewability.isBidAdUnitCodeMatchingSlot(pbjsWinningBid, gptSlot)).to.equal(false); + }); + }); + + describe('getMatchingWinningBidForGPTSlot', function() { + let winningBidsArray; + let sandbox + beforeEach(function() { + sandbox = sinon.sandbox.create(); + // mocking winningBidsArray + winningBidsArray = []; + sandbox.stub(prebidGlobal, 'getGlobal').returns({ + getAllWinningBids: function (number) { + return winningBidsArray; + } + }); + }); + + afterEach(function() { + sandbox.restore(); + }) + + it('should find a match by using customMatchFunction provided in config', function() { + // Needs config to be passed with customMatchFunction + let bidViewabilityConfig = { + customMatchFunction(bid, slot) { + return ('AD-' + slot.getAdUnitPath()) === bid.adUnitCode; + } + }; + let newWinningBid = Object.assign({}, PBJS_WINNING_BID, {adUnitCode: 'AD-' + PBJS_WINNING_BID.adUnitCode}); + // Needs pbjs.getWinningBids to be implemented with match + winningBidsArray.push(newWinningBid); + let wb = bidViewability.getMatchingWinningBidForGPTSlot(bidViewabilityConfig, gptSlot); + expect(wb).to.deep.equal(newWinningBid); + }); + + it('should NOT find a match by using customMatchFunction provided in config', function() { + // Needs config to be passed with customMatchFunction + let bidViewabilityConfig = { + customMatchFunction(bid, slot) { + return ('AD-' + slot.getAdUnitPath()) === bid.adUnitCode; + } + }; + // Needs pbjs.getWinningBids to be implemented without match; winningBidsArray is set to empty in beforeEach + let wb = bidViewability.getMatchingWinningBidForGPTSlot(bidViewabilityConfig, gptSlot); + expect(wb).to.equal(null); + }); + + it('should find a match by using default matching function', function() { + // Needs config to be passed without customMatchFunction + // Needs pbjs.getWinningBids to be implemented with match + winningBidsArray.push(PBJS_WINNING_BID); + let wb = bidViewability.getMatchingWinningBidForGPTSlot({}, gptSlot); + expect(wb).to.deep.equal(PBJS_WINNING_BID); + }); + + it('should NOT find a match by using default matching function', function() { + // Needs config to be passed without customMatchFunction + // Needs pbjs.getWinningBids to be implemented without match; winningBidsArray is set to empty in beforeEach + let wb = bidViewability.getMatchingWinningBidForGPTSlot({}, gptSlot); + expect(wb).to.equal(null); + }); + }); + + describe('fireViewabilityPixels', function() { + let sandbox; + let triggerPixelSpy; + + beforeEach(function() { + sandbox = sinon.sandbox.create(); + triggerPixelSpy = sandbox.spy(utils, ['triggerPixel']); + }); + + afterEach(function() { + sandbox.restore(); + }); + + it('DO NOT fire pixels if NOT mentioned in module config', function() { + let moduleConfig = {}; + bidViewability.fireViewabilityPixels(moduleConfig, PBJS_WINNING_BID); + expect(triggerPixelSpy.callCount).to.equal(0); + }); + + it('fire pixels if mentioned in module config', function() { + let moduleConfig = {firePixels: true}; + bidViewability.fireViewabilityPixels(moduleConfig, PBJS_WINNING_BID); + PBJS_WINNING_BID.vurls.forEach((url, i) => { + let call = triggerPixelSpy.getCall(i); + expect(call.args[0]).to.equal(url); + }); + }); + + it('USP: should include the us_privacy key when USP Consent is available', function () { + let uspDataHandlerStub = sinon.stub(uspDataHandler, 'getConsentData'); + uspDataHandlerStub.returns('1YYY'); + let moduleConfig = {firePixels: true}; + bidViewability.fireViewabilityPixels(moduleConfig, PBJS_WINNING_BID); + PBJS_WINNING_BID.vurls.forEach((url, i) => { + let call = triggerPixelSpy.getCall(i); + expect(call.args[0].indexOf(url)).to.equal(0); + const testurl = parse(call.args[0]); + const queryObject = utils.parseQS(testurl.query); + expect(queryObject.us_privacy).to.equal('1YYY'); + }); + uspDataHandlerStub.restore(); + }); + + it('USP: should not include the us_privacy key when USP Consent is not available', function () { + let moduleConfig = {firePixels: true}; + bidViewability.fireViewabilityPixels(moduleConfig, PBJS_WINNING_BID); + PBJS_WINNING_BID.vurls.forEach((url, i) => { + let call = triggerPixelSpy.getCall(i); + expect(call.args[0].indexOf(url)).to.equal(0); + const testurl = parse(call.args[0]); + const queryObject = utils.parseQS(testurl.query); + expect(queryObject.us_privacy).to.equal(undefined); + }); + }); + + it('GDPR: should include the GDPR keys when GDPR Consent is available', function() { + let gdprDataHandlerStub = sinon.stub(gdprDataHandler, 'getConsentData'); + gdprDataHandlerStub.returns({ + gdprApplies: true, + consentString: 'consent', + addtlConsent: 'moreConsent' + }); + let moduleConfig = {firePixels: true}; + bidViewability.fireViewabilityPixels(moduleConfig, PBJS_WINNING_BID); + PBJS_WINNING_BID.vurls.forEach((url, i) => { + let call = triggerPixelSpy.getCall(i); + expect(call.args[0].indexOf(url)).to.equal(0); + const testurl = parse(call.args[0]); + const queryObject = utils.parseQS(testurl.query); + expect(queryObject.gdpr).to.equal('1'); + expect(queryObject.gdpr_consent).to.equal('consent'); + expect(queryObject.addtl_consent).to.equal('moreConsent'); + }); + gdprDataHandlerStub.restore(); + }); + + it('GDPR: should not include the GDPR keys when GDPR Consent is not available', function () { + let moduleConfig = {firePixels: true}; + bidViewability.fireViewabilityPixels(moduleConfig, PBJS_WINNING_BID); + PBJS_WINNING_BID.vurls.forEach((url, i) => { + let call = triggerPixelSpy.getCall(i); + expect(call.args[0].indexOf(url)).to.equal(0); + const testurl = parse(call.args[0]); + const queryObject = utils.parseQS(testurl.query); + expect(queryObject.gdpr).to.equal(undefined); + expect(queryObject.gdpr_consent).to.equal(undefined); + expect(queryObject.addtl_consent).to.equal(undefined); + }); + }); + + it('GDPR: should only include the GDPR keys for GDPR Consent fields with values', function () { + let gdprDataHandlerStub = sinon.stub(gdprDataHandler, 'getConsentData'); + gdprDataHandlerStub.returns({ + gdprApplies: true, + consentString: 'consent' + }); + let moduleConfig = {firePixels: true}; + bidViewability.fireViewabilityPixels(moduleConfig, PBJS_WINNING_BID); + PBJS_WINNING_BID.vurls.forEach((url, i) => { + let call = triggerPixelSpy.getCall(i); + expect(call.args[0].indexOf(url)).to.equal(0); + const testurl = parse(call.args[0]); + const queryObject = utils.parseQS(testurl.query); + expect(queryObject.gdpr).to.equal('1'); + expect(queryObject.gdpr_consent).to.equal('consent'); + expect(queryObject.addtl_consent).to.equal(undefined); + }); + gdprDataHandlerStub.restore(); + }) + }); + + describe('impressionViewableHandler', function() { + let sandbox; + let triggerPixelSpy; + let eventsEmitSpy; + let logWinningBidNotFoundSpy; + let callBidViewableBidderSpy; + let winningBidsArray; + + beforeEach(function() { + sandbox = sinon.sandbox.create(); + triggerPixelSpy = sandbox.spy(utils, ['triggerPixel']); + eventsEmitSpy = sandbox.spy(events, ['emit']); + callBidViewableBidderSpy = sandbox.spy(adapterManager, ['callBidViewableBidder']); + // mocking winningBidsArray + winningBidsArray = []; + sandbox.stub(prebidGlobal, 'getGlobal').returns({ + getAllWinningBids: function (number) { + return winningBidsArray; + } + }); + }); + + afterEach(function() { + sandbox.restore(); + }) + + it('matching winning bid is found', function() { + let moduleConfig = { + firePixels: true + }; + winningBidsArray.push(PBJS_WINNING_BID); + bidViewability.impressionViewableHandler(moduleConfig, GPT_SLOT, null); + // fire pixels should be called + PBJS_WINNING_BID.vurls.forEach((url, i) => { + let call = triggerPixelSpy.getCall(i); + expect(call.args[0]).to.equal(url); + }); + // adapterManager.callBidViewableBidder is called with required args + let call = callBidViewableBidderSpy.getCall(0); + expect(call.args[0]).to.equal(PBJS_WINNING_BID.bidder); + expect(call.args[1]).to.deep.equal(PBJS_WINNING_BID); + // EVENTS.BID_VIEWABLE is triggered + call = eventsEmitSpy.getCall(0); + expect(call.args[0]).to.equal(EVENTS.BID_VIEWABLE); + expect(call.args[1]).to.deep.equal(PBJS_WINNING_BID); + }); + + it('matching winning bid is NOT found', function() { + // fire pixels should NOT be called + expect(triggerPixelSpy.callCount).to.equal(0); + // adapterManager.callBidViewableBidder is NOT called + expect(callBidViewableBidderSpy.callCount).to.equal(0); + // EVENTS.BID_VIEWABLE is NOT triggered + expect(eventsEmitSpy.callCount).to.equal(0); + }); + }); +}); diff --git a/test/spec/modules/colossussspBidAdapter_spec.js b/test/spec/modules/colossussspBidAdapter_spec.js index a10a7590677..f6e24d07c63 100644 --- a/test/spec/modules/colossussspBidAdapter_spec.js +++ b/test/spec/modules/colossussspBidAdapter_spec.js @@ -88,12 +88,13 @@ describe('ColossussspAdapter', function () { let placements = data['placements']; for (let i = 0; i < placements.length; i++) { let placement = placements[i]; - expect(placement).to.have.all.keys('placementId', 'eids', 'bidId', 'traffic', 'sizes', 'schain'); + expect(placement).to.have.all.keys('placementId', 'eids', 'bidId', 'traffic', 'sizes', 'schain', 'floor'); expect(placement.schain).to.be.an('object') expect(placement.placementId).to.be.a('number'); expect(placement.bidId).to.be.a('string'); expect(placement.traffic).to.be.a('string'); expect(placement.sizes).to.be.an('array'); + expect(placement.floor).to.be.an('object'); } }); it('Returns empty data if no valid requests are passed', function () { @@ -126,7 +127,6 @@ describe('ColossussspAdapter', function () { expect(v.uids).to.be.an('array'); expect(v.uids.length).to.be.equal(1) expect(v.uids[0]).to.have.property('id') - expect(v.uids[0].id).to.be.oneOf(['britepoolid123', 'idl_env123', 'tdid123', 'id5id123']) } } }); diff --git a/test/spec/modules/consentManagement_spec.js b/test/spec/modules/consentManagement_spec.js index cf5c578502f..5e9b0f07f46 100644 --- a/test/spec/modules/consentManagement_spec.js +++ b/test/spec/modules/consentManagement_spec.js @@ -537,6 +537,15 @@ describe('consentManagement', function () { // from CMP window postMessage listener. testIFramedPage('with/JSON response', false, 'encoded_consent_data_via_post_message', 1); testIFramedPage('with/String response', true, 'encoded_consent_data_via_post_message', 1); + + it('should contain correct V1 CMP definition', (done) => { + setConsentConfig(goodConfigWithAllowAuction); + requestBidsHook(() => { + const nbArguments = window.__cmp.toString().split('\n')[0].split(', ').length; + expect(nbArguments).to.equal(3); + done(); + }, {}); + }); }); describe('v2 CMP workflow for iframe pages:', function () { @@ -562,6 +571,15 @@ describe('consentManagement', function () { testIFramedPage('with/JSON response', false, 'abc12345234', 2); testIFramedPage('with/String response', true, 'abc12345234', 2); + + it('should contain correct v2 CMP definition', (done) => { + setConsentConfig(goodConfigWithAllowAuction); + requestBidsHook(() => { + const nbArguments = window.__tcfapi.toString().split('\n')[0].split(', ').length; + expect(nbArguments).to.equal(4); + done(); + }, {}); + }); }); }); diff --git a/test/spec/modules/craftBidAdapter_spec.js b/test/spec/modules/craftBidAdapter_spec.js index ef7dd7c3232..3f4bc977016 100644 --- a/test/spec/modules/craftBidAdapter_spec.js +++ b/test/spec/modules/craftBidAdapter_spec.js @@ -81,7 +81,7 @@ describe('craftAdapter', function () { it('sends bid request to ENDPOINT via POST', function () { let request = spec.buildRequests(bidRequests, bidderRequest); expect(request.method).to.equal('POST'); - expect(request.url).to.equal('https://gacraft.jp/prebid-v3'); + expect(request.url).to.equal('https://gacraft.jp/prebid-v3/craft-prebid-example'); let data = JSON.parse(request.data); expect(data.tags).to.deep.equals([{ sitekey: 'craft-prebid-example', diff --git a/test/spec/modules/criteoBidAdapter_spec.js b/test/spec/modules/criteoBidAdapter_spec.js index c5068d11d31..e91d3b10abb 100755 --- a/test/spec/modules/criteoBidAdapter_spec.js +++ b/test/spec/modules/criteoBidAdapter_spec.js @@ -992,6 +992,7 @@ describe('The Criteo bidding adapter', function () { zoneid: 123, native: { 'products': [{ + 'sendTargetingKeys': false, 'title': 'Product title', 'description': 'Product desc', 'price': '100', @@ -1027,7 +1028,6 @@ describe('The Criteo bidding adapter', function () { native: true, }] }; - config.setConfig({'enableSendAllBids': false}); const bids = spec.interpretResponse(response, request); expect(bids).to.have.lengthOf(1); expect(bids[0].requestId).to.equal('test-bidId'); @@ -1036,61 +1036,87 @@ describe('The Criteo bidding adapter', function () { expect(bids[0].mediaType).to.equal(NATIVE); }); - it('should not parse bid response with native when enableSendAllBids is true', function () { - const response = { - body: { - slots: [{ - impid: 'test-requestId', - bidId: 'abc123', - cpm: 1.23, - width: 728, - height: 90, - zoneid: 123, - native: {} - }], - }, - }; - const request = { - bidRequests: [{ - adUnitCode: 'test-requestId', - bidId: 'test-bidId', + it('should warn only once if sendTargetingKeys set to true on required fields for native bidRequest', () => { + const bidderRequest = { }; + const bidRequests = [ + { + bidder: 'criteo', + adUnitCode: 'bid-123', + transactionId: 'transaction-123', + sizes: [[728, 90]], params: { zoneId: 123, + publisherSubId: '123', + nativeCallback: function() {} }, - native: true, - }] - }; - config.setConfig({'enableSendAllBids': true}); - const bids = spec.interpretResponse(response, request); - expect(bids).to.have.lengthOf(0); - }); - - it('should not parse bid response with native when enableSendAllBids is not set', function () { - const response = { - body: { - slots: [{ - impid: 'test-requestId', - bidId: 'abc123', - cpm: 1.23, - width: 728, - height: 90, - zoneid: 123, - native: {} - }], }, - }; - const request = { - bidRequests: [{ - adUnitCode: 'test-requestId', - bidId: 'test-bidId', + { + bidder: 'criteo', + adUnitCode: 'bid-456', + transactionId: 'transaction-456', + sizes: [[728, 90]], params: { - zoneId: 123, + zoneId: 456, + publisherSubId: '456', + nativeCallback: function() {} }, - native: true, - }] - }; - const bids = spec.interpretResponse(response, request); - expect(bids).to.have.lengthOf(0); + }, + ]; + + const nativeParamsWithSendTargetingKeys = [ + { + nativeParams: { + image: { + sendTargetingKeys: true + }, + } + }, + { + nativeParams: { + icon: { + sendTargetingKeys: true + }, + } + }, + { + nativeParams: { + clickUrl: { + sendTargetingKeys: true + }, + } + }, + { + nativeParams: { + displayUrl: { + sendTargetingKeys: true + }, + } + }, + { + nativeParams: { + privacyLink: { + sendTargetingKeys: true + }, + } + }, + { + nativeParams: { + privacyIcon: { + sendTargetingKeys: true + }, + } + } + ]; + + utilsMock.expects('logWarn') + .withArgs('Criteo: all native assets containing URL should be sent as placeholders with sendId(icon, image, clickUrl, displayUrl, privacyLink, privacyIcon)') + .exactly(nativeParamsWithSendTargetingKeys.length * bidRequests.length); + nativeParamsWithSendTargetingKeys.forEach(nativeParams => { + let transformedBidRequests = {...bidRequests}; + transformedBidRequests = [Object.assign(transformedBidRequests[0], nativeParams), Object.assign(transformedBidRequests[1], nativeParams)]; + spec.buildRequests(transformedBidRequests, bidderRequest); + }); + utilsMock.verify(); }); it('should properly parse a bid response with a zoneId passed as a string', function () { diff --git a/test/spec/modules/dfpAdServerVideo_spec.js b/test/spec/modules/dfpAdServerVideo_spec.js index c0ecb9cad5e..eaffca01e06 100644 --- a/test/spec/modules/dfpAdServerVideo_spec.js +++ b/test/spec/modules/dfpAdServerVideo_spec.js @@ -7,6 +7,7 @@ import * as utils from 'src/utils.js'; import { config } from 'src/config.js'; import { targeting } from 'src/targeting.js'; import { auctionManager } from 'src/auctionManager.js'; +import { gdprDataHandler, uspDataHandler } from 'src/adapterManager.js'; import * as adpod from 'modules/adpod.js'; import { server } from 'test/mocks/xhr.js'; @@ -115,6 +116,116 @@ describe('The DFP video support module', function () { expect(customParams).to.have.property('hb_cache_id', bid.videoCacheKey); }); + it('should include the us_privacy key when USP Consent is available', function () { + let uspDataHandlerStub = sinon.stub(uspDataHandler, 'getConsentData'); + uspDataHandlerStub.returns('1YYY'); + + const bidCopy = utils.deepClone(bid); + bidCopy.adserverTargeting = Object.assign(bidCopy.adserverTargeting, { + hb_adid: 'ad_id', + }); + + const url = parse(buildDfpVideoUrl({ + adUnit: adUnit, + bid: bidCopy, + params: { + 'iu': 'my/adUnit' + } + })); + const queryObject = utils.parseQS(url.query); + expect(queryObject.us_privacy).to.equal('1YYY'); + uspDataHandlerStub.restore(); + }); + + it('should not include the us_privacy key when USP Consent is not available', function () { + const bidCopy = utils.deepClone(bid); + bidCopy.adserverTargeting = Object.assign(bidCopy.adserverTargeting, { + hb_adid: 'ad_id', + }); + + const url = parse(buildDfpVideoUrl({ + adUnit: adUnit, + bid: bidCopy, + params: { + 'iu': 'my/adUnit' + } + })); + const queryObject = utils.parseQS(url.query); + expect(queryObject.us_privacy).to.equal(undefined); + }); + + it('should include the GDPR keys when GDPR Consent is available', function () { + let gdprDataHandlerStub = sinon.stub(gdprDataHandler, 'getConsentData'); + gdprDataHandlerStub.returns({ + gdprApplies: true, + consentString: 'consent', + addtlConsent: 'moreConsent' + }); + + const bidCopy = utils.deepClone(bid); + bidCopy.adserverTargeting = Object.assign(bidCopy.adserverTargeting, { + hb_adid: 'ad_id', + }); + + const url = parse(buildDfpVideoUrl({ + adUnit: adUnit, + bid: bidCopy, + params: { + 'iu': 'my/adUnit' + } + })); + const queryObject = utils.parseQS(url.query); + expect(queryObject.gdpr).to.equal('1'); + expect(queryObject.gdpr_consent).to.equal('consent'); + expect(queryObject.addtl_consent).to.equal('moreConsent'); + gdprDataHandlerStub.restore(); + }); + + it('should not include the GDPR keys when GDPR Consent is not available', function () { + const bidCopy = utils.deepClone(bid); + bidCopy.adserverTargeting = Object.assign(bidCopy.adserverTargeting, { + hb_adid: 'ad_id', + }); + + const url = parse(buildDfpVideoUrl({ + adUnit: adUnit, + bid: bidCopy, + params: { + 'iu': 'my/adUnit' + } + })); + const queryObject = utils.parseQS(url.query); + expect(queryObject.gdpr).to.equal(undefined); + expect(queryObject.gdpr_consent).to.equal(undefined); + expect(queryObject.addtl_consent).to.equal(undefined); + }); + + it('should only include the GDPR keys for GDPR Consent fields with values', function () { + let gdprDataHandlerStub = sinon.stub(gdprDataHandler, 'getConsentData'); + gdprDataHandlerStub.returns({ + gdprApplies: true, + consentString: 'consent', + }); + + const bidCopy = utils.deepClone(bid); + bidCopy.adserverTargeting = Object.assign(bidCopy.adserverTargeting, { + hb_adid: 'ad_id', + }); + + const url = parse(buildDfpVideoUrl({ + adUnit: adUnit, + bid: bidCopy, + params: { + 'iu': 'my/adUnit' + } + })); + const queryObject = utils.parseQS(url.query); + expect(queryObject.gdpr).to.equal('1'); + expect(queryObject.gdpr_consent).to.equal('consent'); + expect(queryObject.addtl_consent).to.equal(undefined); + gdprDataHandlerStub.restore(); + }); + describe('special targeting unit test', function () { const allTargetingData = { 'hb_format': 'video', @@ -350,6 +461,14 @@ describe('The DFP video support module', function () { it('should return masterTag url', function() { amStub.returns(getBidsReceived()); + let uspDataHandlerStub = sinon.stub(uspDataHandler, 'getConsentData'); + uspDataHandlerStub.returns('1YYY'); + let gdprDataHandlerStub = sinon.stub(gdprDataHandler, 'getConsentData'); + gdprDataHandlerStub.returns({ + gdprApplies: true, + consentString: 'consent', + addtlConsent: 'moreConsent' + }); let url; parse(buildAdpodVideoUrl({ code: 'adUnitCode-1', @@ -380,10 +499,16 @@ describe('The DFP video support module', function () { expect(queryParams).to.have.property('unviewed_position_start', '1'); expect(queryParams).to.have.property('url'); expect(queryParams).to.have.property('cust_params'); + expect(queryParams).to.have.property('us_privacy', '1YYY'); + expect(queryParams).to.have.property('gdpr', '1'); + expect(queryParams).to.have.property('gdpr_consent', 'consent'); + expect(queryParams).to.have.property('addtl_consent', 'moreConsent'); const custParams = utils.parseQS(decodeURIComponent(queryParams.cust_params)); expect(custParams).to.have.property('hb_cache_id', '123'); expect(custParams).to.have.property('hb_pb_cat_dur', '15.00_395_15s,15.00_406_30s,10.00_395_15s'); + uspDataHandlerStub.restore(); + gdprDataHandlerStub.restore(); } }); diff --git a/test/spec/modules/districtmDmxBidAdapter_spec.js b/test/spec/modules/districtmDmxBidAdapter_spec.js index 5d1f299dad5..90c18c6a84f 100644 --- a/test/spec/modules/districtmDmxBidAdapter_spec.js +++ b/test/spec/modules/districtmDmxBidAdapter_spec.js @@ -687,17 +687,16 @@ describe('DistrictM Adaptor', function () { describe(`isBidRequestValid test response`, function () { let params = { - dmxid: 10001, + dmxid: 10001, // optional memberid: 10003, }; it(`function should return true`, function () { expect(districtm.isBidRequestValid({ params })).to.be.equal(true); }); it(`function should return false`, function () { - expect(districtm.isBidRequestValid({ params: { memberid: 12345 } })).to.be.equal(false); + expect(districtm.isBidRequestValid({ params: {} })).to.be.equal(false); }); - it(`expect to have two property available dmxid and memberid`, function () { - expect(params).to.have.property('dmxid'); + it(`expect to have memberid`, function () { expect(params).to.have.property('memberid'); }); }); diff --git a/test/spec/modules/eids_spec.js b/test/spec/modules/eids_spec.js index 26dbad2f153..659e5257e90 100644 --- a/test/spec/modules/eids_spec.js +++ b/test/spec/modules/eids_spec.js @@ -57,10 +57,13 @@ describe('eids array generation for known sub-modules', function() { expect(newEids.length).to.equal(1); expect(newEids[0]).to.deep.equal({ source: 'id5-sync.com', - uids: [{ id: 'some-random-id-value', atype: 1 }], - ext: { - linkType: 0 - } + uids: [{ + id: 'some-random-id-value', + atype: 1, + ext: { + linkType: 0 + } + }] }); }); }); @@ -169,6 +172,18 @@ describe('eids array generation for known sub-modules', function() { }); }); + it('tapadId', function() { + const userId = { + tapadId: 'some-random-id-value' + }; + const newEids = createEidsArray(userId); + expect(newEids.length).to.equal(1); + expect(newEids[0]).to.deep.equal({ + source: 'tapad.com', + uids: [{id: 'some-random-id-value', atype: 1}] + }); + }); + it('NetId', function() { const userId = { netId: 'some-random-id-value' diff --git a/test/spec/modules/emx_digitalBidAdapter_spec.js b/test/spec/modules/emx_digitalBidAdapter_spec.js index 39e56638ece..855ffa0a0b7 100644 --- a/test/spec/modules/emx_digitalBidAdapter_spec.js +++ b/test/spec/modules/emx_digitalBidAdapter_spec.js @@ -370,7 +370,7 @@ describe('emx_digital Adapter', function () { it('should add schain object to request', function() { const schainBidderRequest = utils.deepClone(bidderRequest); - schainBidderRequest.schain = { + schainBidderRequest.bids[0].schain = { 'complete': 1, 'ver': '1.0', 'nodes': [ @@ -386,7 +386,7 @@ describe('emx_digital Adapter', function () { expect(request.source.ext.schain).to.exist; expect(request.source.ext.schain).to.have.property('complete', 1); expect(request.source.ext.schain).to.have.property('ver', '1.0'); - expect(request.source.ext.schain.nodes[0].asi).to.equal(schainBidderRequest.schain.nodes[0].asi); + expect(request.source.ext.schain.nodes[0].asi).to.equal(schainBidderRequest.bids[0].schain.nodes[0].asi); }); }); diff --git a/test/spec/modules/engageyaBidAdapter_spec.js b/test/spec/modules/engageyaBidAdapter_spec.js new file mode 100644 index 00000000000..ad411fc9350 --- /dev/null +++ b/test/spec/modules/engageyaBidAdapter_spec.js @@ -0,0 +1,161 @@ +import {expect} from 'chai'; +import {spec} from 'modules/engageyaBidAdapter.js'; +import * as utils from 'src/utils.js'; + +const ENDPOINT_URL = 'https://recs.engageya.com/rec-api/getrecs.json'; + +export const _getUrlVars = function(url) { + var hash; + var myJson = {}; + var hashes = url.slice(url.indexOf('?') + 1).split('&'); + for (var i = 0; i < hashes.length; i++) { + hash = hashes[i].split('='); + myJson[hash[0]] = hash[1]; + } + return myJson; +} + +describe('engageya adapter', function() { + let bidRequests; + let nativeBidRequests; + + beforeEach(function() { + bidRequests = [ + { + bidder: 'engageya', + params: { + widgetId: 85610, + websiteId: 91140, + pageUrl: '[PAGE_URL]' + } + } + ] + + nativeBidRequests = [ + { + bidder: 'engageya', + params: { + widgetId: 85610, + websiteId: 91140, + pageUrl: '[PAGE_URL]' + }, + nativeParams: { + title: { + required: true, + len: 80 + }, + image: { + required: true, + sizes: [150, 50] + }, + sponsoredBy: { + required: true + } + } + } + ] + }) + describe('isBidRequestValid', function () { + it('valid bid case', function () { + let validBid = { + bidder: 'engageya', + params: { + widgetId: 85610, + websiteId: 91140, + pageUrl: '[PAGE_URL]' + } + } + let isValid = spec.isBidRequestValid(validBid); + expect(isValid).to.equal(true); + }); + + it('invalid bid case: widgetId and websiteId is not passed', function() { + let validBid = { + bidder: 'engageya', + params: { + } + } + let isValid = spec.isBidRequestValid(validBid); + expect(isValid).to.equal(false); + }) + + it('invalid bid case: widget id must be number', function() { + let invalidBid = { + bidder: 'engageya', + params: { + widgetId: '157746a', + websiteId: 91140, + pageUrl: '[PAGE_URL]' + } + } + let isValid = spec.isBidRequestValid(invalidBid); + expect(isValid).to.equal(false); + }) + }) + + describe('buildRequests', function () { + it('sends bid request to ENDPOINT via GET', function () { + const request = spec.buildRequests(bidRequests)[0]; + expect(request.url).to.include(ENDPOINT_URL); + expect(request.method).to.equal('GET'); + }); + + it('buildRequests function should not modify original bidRequests object', function () { + let originalBidRequests = utils.deepClone(bidRequests); + let request = spec.buildRequests(bidRequests); + expect(bidRequests).to.deep.equal(originalBidRequests); + }); + + it('buildRequests function should not modify original nativeBidRequests object', function () { + let originalBidRequests = utils.deepClone(nativeBidRequests); + let request = spec.buildRequests(nativeBidRequests); + expect(nativeBidRequests).to.deep.equal(originalBidRequests); + }); + + it('Request params check', function() { + let request = spec.buildRequests(bidRequests)[0]; + const data = _getUrlVars(request.url) + expect(parseInt(data.wid)).to.exist.and.to.equal(bidRequests[0].params.widgetId); + expect(parseInt(data.webid)).to.exist.and.to.equal(bidRequests[0].params.websiteId); + }) + }) + + describe('interpretResponse', function () { + let response = {recs: [ + { + 'ecpm': 0.0920, + 'postId': '', + 'ad': '', + 'thumbnail_path': 'https://engageya.live/wp-content/uploads/2019/05/images.png' + } + ], + imageWidth: 300, + imageHeight: 250, + ireqId: '1d236f7890b', + pbtypeId: 2}; + + it('should get correct bid response', function () { + let expectedResponse = [ + { + 'requestId': '1d236f7890b', + 'cpm': 0.0920, + 'width': 300, + 'height': 250, + 'netRevenue': false, + 'currency': 'USD', + 'creativeId': '', + 'ttl': 700, + 'ad': '' + } + ]; + let request = spec.buildRequests(bidRequests)[0]; + let result = spec.interpretResponse({body: response}, request); + expect(Object.keys(result[0])).to.have.members(Object.keys(expectedResponse[0])); + expect(result[0].cpm).to.not.equal(null); + expect(result[0].creativeId).to.not.equal(null); + expect(result[0].ad).to.not.equal(null); + expect(result[0].currency).to.equal('USD'); + expect(result[0].netRevenue).to.equal(false); + }); + }) +}) diff --git a/test/spec/modules/eplanningBidAdapter_spec.js b/test/spec/modules/eplanningBidAdapter_spec.js index f267680b46b..5969777f4da 100644 --- a/test/spec/modules/eplanningBidAdapter_spec.js +++ b/test/spec/modules/eplanningBidAdapter_spec.js @@ -1,6 +1,7 @@ import { expect } from 'chai'; import { spec, storage } from 'modules/eplanningBidAdapter.js'; import { newBidder } from 'src/adapters/bidderFactory.js'; +import * as utils from 'src/utils.js'; describe('E-Planning Adapter', function () { const adapter = newBidder('spec'); @@ -32,6 +33,15 @@ describe('E-Planning Adapter', function () { 'adUnitCode': ADUNIT_CODE, 'sizes': [[300, 250], [300, 600]], }; + const validBid2 = { + 'bidder': 'eplanning', + 'bidId': BID_ID2, + 'params': { + 'ci': CI, + }, + 'adUnitCode': ADUNIT_CODE2, + 'sizes': [[300, 250], [300, 600]], + }; const ML = '1'; const validBidMappingLinear = { 'bidder': 'eplanning', @@ -43,13 +53,14 @@ describe('E-Planning Adapter', function () { 'adUnitCode': ADUNIT_CODE, 'sizes': [[300, 250], [300, 600]], }; - const validBid2 = { + const SN = 'spaceName'; + const validBidSpaceName = { 'bidder': 'eplanning', - 'bidId': BID_ID2, + 'bidId': BID_ID, 'params': { 'ci': CI, + 'sn': SN, }, - 'adUnitCode': ADUNIT_CODE2, 'sizes': [[300, 250], [300, 600]], }; const validBidView = { @@ -95,6 +106,39 @@ describe('E-Planning Adapter', function () { 'adUnitCode': 'adunit-code', 'sizes': [[300, 250], [300, 600]], }; + const validBidExistingSizesInPriorityListForMobile = { + 'bidder': 'eplanning', + 'bidId': BID_ID, + 'params': { + 'ci': CI, + }, + 'adUnitCode': ADUNIT_CODE, + 'sizes': [[970, 250], [320, 50], [300, 50]], + }; + const validBidSizesNotExistingInPriorityListForMobile = { + 'bidder': 'eplanning', + 'bidId': BID_ID, + 'params': { + 'ci': CI, + }, + 'adUnitCode': ADUNIT_CODE, + 'sizes': [[970, 250], [300, 70], [160, 600]], + }; + const validBidExistingSizesInPriorityListForDesktop = { + 'bidder': 'eplanning', + 'bidId': BID_ID, + 'params': { + 'ci': CI, + }, + 'adUnitCode': ADUNIT_CODE, + 'sizes': [[970, 250], [300, 600], [300, 250]], + 'ext': { + 'screen': { + 'w': 1025, + 'h': 1025 + } + } + }; const response = { body: { 'sI': { @@ -247,6 +291,27 @@ describe('E-Planning Adapter', function () { describe('buildRequests', function () { let bidRequests = [validBid]; + let sandbox; + beforeEach(() => { + sandbox = sinon.sandbox.create(); + }); + + afterEach(() => { + sandbox.restore(); + }); + + const createWindow = () => { + const win = {}; + win.self = win; + win.innerWidth = 1025; + return win; + }; + + function setupSingleWindow(sandBox) { + const win = createWindow(); + sandBox.stub(utils, 'getWindowSelf').returns(win); + } + it('should create the url correctly', function () { const url = spec.buildRequests(bidRequests, bidderRequest).url; expect(url).to.equal('https://ads.us.e-planning.net/hb/1/' + CI + '/1/localhost/ROS'); @@ -278,6 +343,12 @@ describe('E-Planning Adapter', function () { expect(e).to.equal(CLEAN_ADUNIT_CODE_ML + ':300x250,300x600'); }); + it('should return e parameter with space name attribute with value according to the adunit sizes', function () { + let bidRequestsSN = [validBidSpaceName]; + const e = spec.buildRequests(bidRequestsSN, bidderRequest).data.e; + expect(e).to.equal(SN + ':300x250,300x600'); + }); + it('should return correct e parameter with more than one adunit', function () { const NEW_CODE = ADUNIT_CODE + '2'; const CLEAN_NEW_CODE = CLEAN_ADUNIT_CODE + '2'; @@ -314,6 +385,23 @@ describe('E-Planning Adapter', function () { expect(e).to.equal(CLEAN_ADUNIT_CODE_ML + ':300x250,300x600+' + CLEAN_NEW_CODE + ':100x100'); }); + it('should return correct e parameter with space name attribute with more than one adunit', function () { + let bidRequestsSN = [validBidSpaceName]; + const NEW_SN = 'anotherNameSpace'; + const anotherBid = { + 'bidder': 'eplanning', + 'params': { + 'ci': CI, + 'sn': NEW_SN, + }, + 'sizes': [[100, 100]], + }; + bidRequestsSN.push(anotherBid); + + const e = spec.buildRequests(bidRequestsSN, bidderRequest).data.e; + expect(e).to.equal(SN + ':300x250,300x600+' + NEW_SN + ':100x100'); + }); + it('should return correct e parameter when the adunit has no size', function () { const noSizeBid = { 'bidder': 'eplanning', @@ -388,6 +476,25 @@ describe('E-Planning Adapter', function () { const dataRequest = request.data; expect(dataRequest.ccpa).to.equal('consentCcpa'); }); + + it('should return the e parameter with a value according to the sizes in order corresponding to the mobile priority list of the ad units', function () { + let bidRequestsPrioritySizes = [validBidExistingSizesInPriorityListForMobile]; + const e = spec.buildRequests(bidRequestsPrioritySizes, bidderRequest).data.e; + expect(e).to.equal('320x50_0:320x50,300x50,970x250'); + }); + + it('should return the e parameter with a value according to the sizes in order corresponding to the desktop priority list of the ad units', function () { + let bidRequestsPrioritySizes = [validBidExistingSizesInPriorityListForDesktop]; + setupSingleWindow(sandbox); + const e = spec.buildRequests(bidRequestsPrioritySizes, bidderRequest).data.e; + expect(e).to.equal('300x250_0:300x250,300x600,970x250'); + }); + + it('should return the e parameter with a value according to the sizes in order as they are sent from the ad units', function () { + let bidRequestsPrioritySizes2 = [validBidSizesNotExistingInPriorityListForMobile]; + const e = spec.buildRequests(bidRequestsPrioritySizes2, bidderRequest).data.e; + expect(e).to.equal('970x250_0:970x250,300x70,160x600'); + }); }); describe('interpretResponse', function () { diff --git a/test/spec/modules/fabrickIdSystem_spec.js b/test/spec/modules/fabrickIdSystem_spec.js index cbd538816ab..c250c8e5e8b 100644 --- a/test/spec/modules/fabrickIdSystem_spec.js +++ b/test/spec/modules/fabrickIdSystem_spec.js @@ -1,7 +1,7 @@ import * as utils from '../../../src/utils.js'; import {server} from '../../mocks/xhr.js'; -import * as fabrickIdSystem from 'modules/fabrickIdSystem.js'; +import {fabrickIdSubmodule, appendUrl} from 'modules/fabrickIdSystem.js'; const defaultConfigParams = { apiKey: '123', @@ -10,26 +10,25 @@ const defaultConfigParams = { url: 'http://localhost:9999/test/mocks/fabrickId.json?' }; const responseHeader = {'Content-Type': 'application/json'} -const fabrickIdSubmodule = fabrickIdSystem.fabrickIdSubmodule; describe('Fabrick ID System', function() { let logErrorStub; beforeEach(function () { - logErrorStub = sinon.stub(utils, 'logError'); }); afterEach(function () { - logErrorStub.restore(); - fabrickIdSubmodule.getRefererInfoOverride = null; }); it('should log an error if no configParams were passed into getId', function () { + logErrorStub = sinon.stub(utils, 'logError'); fabrickIdSubmodule.getId(); expect(logErrorStub.calledOnce).to.be.true; + logErrorStub.restore(); }); it('should error on json parsing', function() { + logErrorStub = sinon.stub(utils, 'logError'); let submoduleCallback = fabrickIdSubmodule.getId({ name: 'fabrickId', params: defaultConfigParams @@ -44,11 +43,12 @@ describe('Fabrick ID System', function() { ); expect(callBackSpy.calledOnce).to.be.true; expect(logErrorStub.calledOnce).to.be.true; + logErrorStub.restore(); }); it('should truncate the params', function() { let r = ''; - for (let i = 0; i < 300; i++) { + for (let i = 0; i < 1500; i++) { r += 'r'; } let configParams = Object.assign({}, defaultConfigParams, { @@ -66,7 +66,7 @@ describe('Fabrick ID System', function() { submoduleCallback(callBackSpy); let request = server.requests[0]; r = ''; - for (let i = 0; i < 200; i++) { + for (let i = 0; i < 1000 - 3; i++) { r += 'r'; } expect(request.url).to.match(new RegExp(`r=${r}&r=`)); @@ -76,7 +76,6 @@ describe('Fabrick ID System', function() { JSON.stringify({}) ); expect(callBackSpy.calledOnce).to.be.true; - expect(logErrorStub.calledOnce).to.be.false; }); it('should complete successfully', function() { @@ -101,6 +100,35 @@ describe('Fabrick ID System', function() { JSON.stringify({}) ); expect(callBackSpy.calledOnce).to.be.true; - expect(logErrorStub.calledOnce).to.be.false; + }); + + it('should truncate 2', function() { + let configParams = { + maxUrlLen: 10, + maxRefLen: 5, + maxSpaceAvailable: 2 + }; + + let url = appendUrl('', 'r', '123', configParams); + expect(url).to.equal('&r=12'); + + url = appendUrl('12345', 'r', '678', configParams); + expect(url).to.equal('12345&r=67'); + + url = appendUrl('12345678', 'r', '9', configParams); + expect(url).to.equal('12345678'); + + configParams.maxRefLen = 8; + url = appendUrl('', 'r', '1234&', configParams); + expect(url).to.equal('&r=1234'); + + url = appendUrl('', 'r', '123&', configParams); + expect(url).to.equal('&r=123'); + + url = appendUrl('', 'r', '12&', configParams); + expect(url).to.equal('&r=12%26'); + + url = appendUrl('', 'r', '1&&', configParams); + expect(url).to.equal('&r=1%26'); }); }); diff --git a/test/spec/modules/freewheel-sspBidAdapter_spec.js b/test/spec/modules/freewheel-sspBidAdapter_spec.js index c44d7908ba8..e416bd1c26b 100644 --- a/test/spec/modules/freewheel-sspBidAdapter_spec.js +++ b/test/spec/modules/freewheel-sspBidAdapter_spec.js @@ -323,7 +323,10 @@ describe('freewheelSSP BidAdapter Test', () => { ' ' + ' Adswizz' + ' ' + - ' https://ads.stickyadstv.com/auto-user-sync?dealId=NRJ-PRO-00008' + + ' https://ads.stickyadstv.com/auto-user-sync?dealId=NRJ-PRO-12008' + + ' ' + + ' ' + + ' ' + ' ' + ' ' + ' ' + @@ -358,6 +361,8 @@ describe('freewheelSSP BidAdapter Test', () => { netRevenue: true, ttl: 360, dealId: 'NRJ-PRO-00008', + campaignId: 'SMF-WOW-55555', + bannerId: '12345', ad: ad } ]; @@ -365,6 +370,8 @@ describe('freewheelSSP BidAdapter Test', () => { let result = spec.interpretResponse(response, request[0]); expect(Object.keys(result[0])).to.deep.equal(Object.keys(expectedResponse[0])); expect(result[0].dealId).to.equal('NRJ-PRO-00008'); + expect(result[0].campaignId).to.equal('SMF-WOW-55555'); + expect(result[0].bannerId).to.equal('12345'); }); it('should get correct bid response with formated ad', () => { @@ -381,6 +388,8 @@ describe('freewheelSSP BidAdapter Test', () => { netRevenue: true, ttl: 360, dealId: 'NRJ-PRO-00008', + campaignId: 'SMF-WOW-55555', + bannerId: '12345', ad: formattedAd } ]; @@ -388,6 +397,8 @@ describe('freewheelSSP BidAdapter Test', () => { let result = spec.interpretResponse(response, request[0]); expect(Object.keys(result[0])).to.deep.equal(Object.keys(expectedResponse[0])); expect(result[0].dealId).to.equal('NRJ-PRO-00008'); + expect(result[0].campaignId).to.equal('SMF-WOW-55555'); + expect(result[0].bannerId).to.equal('12345'); }); it('handles nobid responses', () => { @@ -398,6 +409,7 @@ describe('freewheelSSP BidAdapter Test', () => { expect(result.length).to.equal(0); }); }); + describe('interpretResponseForVideo', () => { let bidRequests = [ { @@ -461,6 +473,12 @@ describe('freewheelSSP BidAdapter Test', () => { ' Adswizz' + ' ' + ' https://ads.stickyadstv.com/auto-user-sync?dealId=NRJ-PRO-00008' + + ' ' + + ' ' + + ' ' + + ' ' + + ' ' + + ' ' + ' ' + ' ' + ' ' + @@ -495,6 +513,8 @@ describe('freewheelSSP BidAdapter Test', () => { netRevenue: true, ttl: 360, dealId: 'NRJ-PRO-00008', + campaignId: 'SMF-WOW-55555', + bannerId: '12345', vastXml: response, mediaType: 'video', ad: ad @@ -504,6 +524,8 @@ describe('freewheelSSP BidAdapter Test', () => { let result = spec.interpretResponse(response, request[0]); expect(Object.keys(result[0])).to.deep.equal(Object.keys(expectedResponse[0])); expect(result[0].dealId).to.equal('NRJ-PRO-00008'); + expect(result[0].campaignId).to.equal('SMF-WOW-55555'); + expect(result[0].bannerId).to.equal('12345'); }); it('should get correct bid response with formated ad', () => { @@ -520,6 +542,8 @@ describe('freewheelSSP BidAdapter Test', () => { netRevenue: true, ttl: 360, dealId: 'NRJ-PRO-00008', + campaignId: 'SMF-WOW-55555', + bannerId: '12345', vastXml: response, mediaType: 'video', ad: formattedAd @@ -529,6 +553,8 @@ describe('freewheelSSP BidAdapter Test', () => { let result = spec.interpretResponse(response, request[0]); expect(Object.keys(result[0])).to.deep.equal(Object.keys(expectedResponse[0])); expect(result[0].dealId).to.equal('NRJ-PRO-00008'); + expect(result[0].campaignId).to.equal('SMF-WOW-55555'); + expect(result[0].bannerId).to.equal('12345'); }); it('handles nobid responses', () => { diff --git a/test/spec/modules/gjirafaBidAdapter_spec.js b/test/spec/modules/gjirafaBidAdapter_spec.js index db9b82e0a10..f0fb01f4398 100644 --- a/test/spec/modules/gjirafaBidAdapter_spec.js +++ b/test/spec/modules/gjirafaBidAdapter_spec.js @@ -34,9 +34,7 @@ describe('gjirafaAdapterTest', () => { it('bidRequest without propertyId or placementId', () => { expect(spec.isBidRequestValid({ bidder: 'gjirafa', - params: { - propertyId: '{propertyId}', - } + params: {} })).to.equal(false); }); }); @@ -46,7 +44,17 @@ describe('gjirafaAdapterTest', () => { 'bidder': 'gjirafa', 'params': { 'propertyId': '{propertyId}', - 'placementId': '{placementId}' + 'placementId': '{placementId}', + 'data': { + 'catalogs': [{ + 'catalogId': 1, + 'items': ['1', '2', '3'] + }], + 'inventory': { + 'category': ['category1', 'category2'], + 'query': ['query'] + } + } }, 'adUnitCode': 'hb-leaderboard', 'transactionId': 'b6b889bb-776c-48fd-bc7b-d11a1cf0425e', @@ -86,6 +94,20 @@ describe('gjirafaAdapterTest', () => { expect(requestItem.data.placements[0].sizes).to.equal('728x90'); }); }); + + it('bidRequest data param', () => { + const requests = spec.buildRequests(bidRequests); + requests.forEach((requestItem) => { + expect(requestItem.data.data).to.exist; + expect(requestItem.data.data.catalogs).to.exist; + expect(requestItem.data.data.inventory).to.exist; + expect(requestItem.data.data.catalogs.length).to.equal(1); + expect(requestItem.data.data.catalogs[0].items.length).to.equal(3); + expect(Object.keys(requestItem.data.data.inventory).length).to.equal(2); + expect(requestItem.data.data.inventory.category.length).to.equal(2); + expect(requestItem.data.data.inventory.query.length).to.equal(1); + }); + }); }); describe('interpretResponse', () => { diff --git a/test/spec/modules/glomexBidAdapter_spec.js b/test/spec/modules/glomexBidAdapter_spec.js new file mode 100644 index 00000000000..b43623928f7 --- /dev/null +++ b/test/spec/modules/glomexBidAdapter_spec.js @@ -0,0 +1,133 @@ +import { expect } from 'chai' +import { spec } from 'modules/glomexBidAdapter.js' +import { newBidder } from 'src/adapters/bidderFactory.js' + +const REQUEST = { + bidder: 'glomex', + params: { + integrationId: 'abcdefg', + playlistId: 'defghjk' + }, + bidderRequestId: '143346cf0f1732', + auctionId: '2e41f65424c87c', + adUnitCode: 'adunit-code', + bidId: '2d925f27f5079f', + sizes: [640, 360] +} + +const BIDDER_REQUEST = { + auctionId: '2d921234f5079f', + refererInfo: { + isAmp: true, + numIframes: 0, + reachedTop: true, + referer: 'https://glomex.com' + }, + gdprConsent: { + gdprApplies: true, + consentString: 'CO5asbJO5asbJE-AAAENAACAAAAAAAAAAAYgAAAAAAAA.IAAA' + } +} + +const RESPONSE = { + bids: [ + { + id: '2d925f27f5079f', + cpm: 3.5, + width: 640, + height: 360, + creativeId: 'creative-1j75x4ln1kk6m1ius', + dealId: 'deal-1j75x4ln1kk6m1iut', + currency: 'EUR', + netRevenue: true, + ttl: 300, + ad: '' + } + ] +} +describe('glomexBidAdapter', function () { + const adapter = newBidder(spec) + + describe('inherited functions', function () { + it('exists and is a function', function () { + expect(adapter.callBids).to.exist.and.to.be.a('function') + }) + }) + + describe('isBidRequestValid', function () { + it('should return true when required params found', function () { + const request = { + 'params': { + 'integrationId': 'abcdefg' + } + } + expect(spec.isBidRequestValid(request)).to.equal(true) + }) + + it('should return false when required params are not passed', function () { + expect(spec.isBidRequestValid({})).to.equal(false) + }) + }) + + describe('buildRequests', function () { + const bidRequests = [REQUEST] + const request = spec.buildRequests(bidRequests, BIDDER_REQUEST) + + it('sends bid request to ENDPOINT via POST', function () { + expect(request.method).to.equal('POST') + }) + + it('returns a list of valid requests', function () { + expect(request.validBidRequests).to.eql([REQUEST]) + }) + + it('sends params.integrationId', function () { + expect(request.validBidRequests[0].params.integrationId).to.eql(REQUEST.params.integrationId) + }) + + it('sends params.playlistId', function () { + expect(request.validBidRequests[0].params.playlistId).to.eql(REQUEST.params.playlistId) + }) + + it('sends refererInfo', function () { + expect(request.data.refererInfo).to.eql(BIDDER_REQUEST.refererInfo) + }) + + it('sends gdprConsent', function () { + expect(request.data.gdprConsent).to.eql(BIDDER_REQUEST.gdprConsent) + }) + + it('sends the auctionId', function () { + expect(request.data.auctionId).to.eql(BIDDER_REQUEST.auctionId) + }) + }) + + describe('interpretResponse', function () { + it('handles nobid responses', function () { + expect(spec.interpretResponse({body: {}}, {validBidRequests: []}).length).to.equal(0) + expect(spec.interpretResponse({body: []}, {validBidRequests: []}).length).to.equal(0) + }) + + it('handles the server response', function () { + const result = spec.interpretResponse( + { + body: RESPONSE + }, + { + validBidRequests: [REQUEST] + } + ) + + expect(result[0].requestId).to.equal('2d925f27f5079f') + expect(result[0].cpm).to.equal(3.5) + expect(result[0].width).to.equal(640) + expect(result[0].height).to.equal(360) + expect(result[0].creativeId).to.equal('creative-1j75x4ln1kk6m1ius') + expect(result[0].dealId).to.equal('deal-1j75x4ln1kk6m1iut') + expect(result[0].currency).to.equal('EUR') + expect(result[0].netRevenue).to.equal(true) + expect(result[0].ttl).to.equal(300) + expect(result[0].ad).to.equal('') + }) + }) +}) diff --git a/test/spec/modules/gmosspBidAdapter_spec.js b/test/spec/modules/gmosspBidAdapter_spec.js index 5de3db623c5..52bb8460caa 100644 --- a/test/spec/modules/gmosspBidAdapter_spec.js +++ b/test/spec/modules/gmosspBidAdapter_spec.js @@ -54,18 +54,30 @@ describe('GmosspAdapter', function () { } ]; - const bidderRequest = { - refererInfo: { - referer: 'https://hoge.com' - } - }; - it('sends bid request to ENDPOINT via GET', function () { + const bidderRequest = { + refererInfo: { + referer: 'https://hoge.com' + } + }; + const requests = spec.buildRequests(bidRequests, bidderRequest); expect(requests[0].url).to.equal(ENDPOINT); expect(requests[0].method).to.equal('GET'); expect(requests[0].data).to.equal('tid=791e9d84-af92-4903-94da-24c7426d9d0c&bid=2b84475b5b636e&ver=$prebid.version$&sid=123456&url=https%3A%2F%2Fhoge.com&cur=JPY&dnt=0&'); }); + + it('should use fallback if refererInfo.referer in bid request is empty', function () { + const bidderRequest = { + refererInfo: { + referer: '' + } + }; + + const requests = spec.buildRequests(bidRequests, bidderRequest); + const result = 'tid=791e9d84-af92-4903-94da-24c7426d9d0c&bid=2b84475b5b636e&ver=$prebid.version$&sid=123456&url=' + encodeURIComponent(window.top.location.href) + '&cur=JPY&dnt=0&'; + expect(requests[0].data).to.equal(result); + }); }); describe('interpretResponse', function () { diff --git a/test/spec/modules/gothamadsBidAdapter_spec.js b/test/spec/modules/gothamadsBidAdapter_spec.js new file mode 100644 index 00000000000..c638e7a0e3e --- /dev/null +++ b/test/spec/modules/gothamadsBidAdapter_spec.js @@ -0,0 +1,369 @@ +import { expect } from 'chai'; +import { spec } from 'modules/gothamadsBidAdapter.js'; + +const NATIVE_BID_REQUEST = { + code: 'native_example', + mediaTypes: { + native: { + title: { + required: true, + len: 800 + }, + image: { + required: true, + len: 80 + }, + sponsoredBy: { + required: true + }, + clickUrl: { + required: true + }, + privacyLink: { + required: false + }, + body: { + required: true + }, + icon: { + required: true, + sizes: [50, 50] + } + } + }, + bidder: 'gothamads', + params: { + placementId: 'hash', + accountId: 'accountId' + }, + timeout: 1000 + +}; + +const BANNER_BID_REQUEST = { + code: 'banner_example', + mediaTypes: { + banner: { + sizes: [[300, 250], [300, 600]] + } + }, + bidder: 'gothamads', + params: { + placementId: 'hash', + accountId: 'accountId' + }, + timeout: 1000, + +} + +const bidRequest = { + refererInfo: { + referer: 'test.com' + } +} + +const VIDEO_BID_REQUEST = { + code: 'video1', + sizes: [640, 480], + mediaTypes: { video: { + minduration: 0, + maxduration: 999, + boxingallowed: 1, + skip: 0, + mimes: [ + 'application/javascript', + 'video/mp4' + ], + w: 1920, + h: 1080, + protocols: [ + 2 + ], + linearity: 1, + api: [ + 1, + 2 + ] + } + }, + + bidder: 'gothamads', + params: { + placementId: 'hash', + accountId: 'accountId' + }, + timeout: 1000 + +} + +const BANNER_BID_RESPONSE = { + id: 'request_id', + bidid: 'request_imp_id', + seatbid: [{ + bid: [{ + id: 'bid_id', + impid: 'request_imp_id', + price: 5, + adomain: ['example.com'], + adm: 'admcode', + crid: 'crid', + ext: { + mediaType: 'banner' + } + }], + }], +}; + +const VIDEO_BID_RESPONSE = { + id: 'request_id', + bidid: 'request_imp_id', + seatbid: [{ + bid: [{ + id: 'bid_id', + impid: 'request_imp_id', + price: 5, + adomain: ['example.com'], + adm: 'admcode', + crid: 'crid', + ext: { + mediaType: 'video', + vastUrl: 'http://example.vast', + } + }], + }], +}; + +let imgData = { + url: `https://example.com/image`, + w: 1200, + h: 627 +}; + +const NATIVE_BID_RESPONSE = { + id: 'request_id', + bidid: 'request_imp_id', + seatbid: [{ + bid: [{ + id: 'bid_id', + impid: 'request_imp_id', + price: 5, + adomain: ['example.com'], + adm: { native: + { + assets: [ + {id: 0, title: 'dummyText'}, + {id: 3, image: imgData}, + { + id: 5, + data: {value: 'organization.name'} + } + ], + link: {url: 'example.com'}, + imptrackers: ['tracker1.com', 'tracker2.com', 'tracker3.com'], + jstracker: 'tracker1.com' + } + }, + crid: 'crid', + ext: { + mediaType: 'native' + } + }], + }], +}; + +describe('GothamAdsAdapter', function() { + describe('isBidRequestValid', function() { + it('should return true when required params found', function () { + expect(spec.isBidRequestValid(NATIVE_BID_REQUEST)).to.equal(true); + }); + + it('should return false when required params are not passed', function () { + let bid = Object.assign({}, NATIVE_BID_REQUEST); + delete bid.params; + bid.params = { + 'IncorrectParam': 0 + }; + expect(spec.isBidRequestValid(bid)).to.equal(false); + }); + }); + + describe('build Native Request', function () { + const request = spec.buildRequests([NATIVE_BID_REQUEST], bidRequest); + + it('Creates a ServerRequest object with method, URL and data', function () { + expect(request).to.exist; + expect(request.method).to.exist; + expect(request.url).to.exist; + expect(request.data).to.exist; + }); + + it('sends bid request to our endpoint via POST', function () { + expect(request.method).to.equal('POST'); + }); + + it('Returns valid URL', function () { + expect(request.url).to.equal('https://us-e-node1.gothamads.com/bid?pass=accountId&integration=prebidjs'); + }); + + it('Returns empty data if no valid requests are passed', function () { + let serverRequest = spec.buildRequests([]); + expect(serverRequest).to.be.an('array').that.is.empty; + }); + }); + + describe('build Banner Request', function () { + const request = spec.buildRequests([BANNER_BID_REQUEST]); + + it('Creates a ServerRequest object with method, URL and data', function () { + expect(request).to.exist; + expect(request.method).to.exist; + expect(request.url).to.exist; + expect(request.data).to.exist; + }); + + it('sends bid request to our endpoint via POST', function () { + expect(request.method).to.equal('POST'); + }); + + it('Returns valid URL', function () { + expect(request.url).to.equal('https://us-e-node1.gothamads.com/bid?pass=accountId&integration=prebidjs'); + }); + }); + + describe('build Video Request', function () { + const request = spec.buildRequests([VIDEO_BID_REQUEST]); + + it('Creates a ServerRequest object with method, URL and data', function () { + expect(request).to.exist; + expect(request.method).to.exist; + expect(request.url).to.exist; + expect(request.data).to.exist; + }); + + it('sends bid request to our endpoint via POST', function () { + expect(request.method).to.equal('POST'); + }); + + it('Returns valid URL', function () { + expect(request.url).to.equal('https://us-e-node1.gothamads.com/bid?pass=accountId&integration=prebidjs'); + }); + }); + + describe('interpretResponse', function () { + it('Empty response must return empty array', function() { + const emptyResponse = null; + let response = spec.interpretResponse(emptyResponse); + + expect(response).to.be.an('array').that.is.empty; + }) + + it('Should interpret banner response', function () { + const bannerResponse = { + body: [BANNER_BID_RESPONSE] + } + + const expectedBidResponse = { + requestId: BANNER_BID_RESPONSE.id, + cpm: BANNER_BID_RESPONSE.seatbid[0].bid[0].price, + width: BANNER_BID_RESPONSE.seatbid[0].bid[0].w, + height: BANNER_BID_RESPONSE.seatbid[0].bid[0].h, + ttl: BANNER_BID_RESPONSE.ttl || 1200, + currency: BANNER_BID_RESPONSE.cur || 'USD', + netRevenue: true, + creativeId: BANNER_BID_RESPONSE.seatbid[0].bid[0].crid, + dealId: BANNER_BID_RESPONSE.seatbid[0].bid[0].dealid, + mediaType: 'banner', + ad: BANNER_BID_RESPONSE.seatbid[0].bid[0].adm + } + + let bannerResponses = spec.interpretResponse(bannerResponse); + + expect(bannerResponses).to.be.an('array').that.is.not.empty; + let dataItem = bannerResponses[0]; + expect(dataItem).to.have.all.keys('requestId', 'cpm', 'width', 'height', 'ad', 'ttl', 'creativeId', + 'netRevenue', 'currency', 'dealId', 'mediaType'); + expect(dataItem.requestId).to.equal(expectedBidResponse.requestId); + expect(dataItem.cpm).to.equal(expectedBidResponse.cpm); + expect(dataItem.ad).to.equal(expectedBidResponse.ad); + expect(dataItem.ttl).to.equal(expectedBidResponse.ttl); + expect(dataItem.creativeId).to.equal(expectedBidResponse.creativeId); + expect(dataItem.netRevenue).to.be.true; + expect(dataItem.currency).to.equal(expectedBidResponse.currency); + expect(dataItem.width).to.equal(expectedBidResponse.width); + expect(dataItem.height).to.equal(expectedBidResponse.height); + }); + + it('Should interpret video response', function () { + const videoResponse = { + body: [VIDEO_BID_RESPONSE] + } + + const expectedBidResponse = { + requestId: VIDEO_BID_RESPONSE.id, + cpm: VIDEO_BID_RESPONSE.seatbid[0].bid[0].price, + width: VIDEO_BID_RESPONSE.seatbid[0].bid[0].w, + height: VIDEO_BID_RESPONSE.seatbid[0].bid[0].h, + ttl: VIDEO_BID_RESPONSE.ttl || 1200, + currency: VIDEO_BID_RESPONSE.cur || 'USD', + netRevenue: true, + creativeId: VIDEO_BID_RESPONSE.seatbid[0].bid[0].crid, + dealId: VIDEO_BID_RESPONSE.seatbid[0].bid[0].dealid, + mediaType: 'video', + vastXml: VIDEO_BID_RESPONSE.seatbid[0].bid[0].adm, + vastUrl: VIDEO_BID_RESPONSE.seatbid[0].bid[0].ext.vastUrl + } + + let videoResponses = spec.interpretResponse(videoResponse); + + expect(videoResponses).to.be.an('array').that.is.not.empty; + let dataItem = videoResponses[0]; + expect(dataItem).to.have.all.keys('requestId', 'cpm', 'width', 'height', 'vastXml', 'vastUrl', 'ttl', 'creativeId', + 'netRevenue', 'currency', 'dealId', 'mediaType'); + expect(dataItem.requestId).to.equal(expectedBidResponse.requestId); + expect(dataItem.cpm).to.equal(expectedBidResponse.cpm); + expect(dataItem.vastXml).to.equal(expectedBidResponse.vastXml) + expect(dataItem.ttl).to.equal(expectedBidResponse.ttl); + expect(dataItem.creativeId).to.equal(expectedBidResponse.creativeId); + expect(dataItem.netRevenue).to.be.true; + expect(dataItem.currency).to.equal(expectedBidResponse.currency); + expect(dataItem.width).to.equal(expectedBidResponse.width); + expect(dataItem.height).to.equal(expectedBidResponse.height); + }); + + it('Should interpret native response', function () { + const nativeResponse = { + body: [NATIVE_BID_RESPONSE] + } + + const expectedBidResponse = { + requestId: NATIVE_BID_RESPONSE.id, + cpm: NATIVE_BID_RESPONSE.seatbid[0].bid[0].price, + width: NATIVE_BID_RESPONSE.seatbid[0].bid[0].w, + height: NATIVE_BID_RESPONSE.seatbid[0].bid[0].h, + ttl: NATIVE_BID_RESPONSE.ttl || 1200, + currency: NATIVE_BID_RESPONSE.cur || 'USD', + netRevenue: true, + creativeId: NATIVE_BID_RESPONSE.seatbid[0].bid[0].crid, + dealId: NATIVE_BID_RESPONSE.seatbid[0].bid[0].dealid, + mediaType: 'native', + native: {clickUrl: NATIVE_BID_RESPONSE.seatbid[0].bid[0].adm.native.link.url} + } + + let nativeResponses = spec.interpretResponse(nativeResponse); + + expect(nativeResponses).to.be.an('array').that.is.not.empty; + let dataItem = nativeResponses[0]; + expect(dataItem).to.have.all.keys('requestId', 'cpm', 'width', 'height', 'native', 'ttl', 'creativeId', + 'netRevenue', 'currency', 'dealId', 'mediaType'); + expect(dataItem.requestId).to.equal(expectedBidResponse.requestId); + expect(dataItem.cpm).to.equal(expectedBidResponse.cpm); + expect(dataItem.native.clickUrl).to.equal(expectedBidResponse.native.clickUrl) + expect(dataItem.ttl).to.equal(expectedBidResponse.ttl); + expect(dataItem.creativeId).to.equal(expectedBidResponse.creativeId); + expect(dataItem.netRevenue).to.be.true; + expect(dataItem.currency).to.equal(expectedBidResponse.currency); + expect(dataItem.width).to.equal(expectedBidResponse.width); + expect(dataItem.height).to.equal(expectedBidResponse.height); + }); + }); +}) diff --git a/test/spec/modules/gridBidAdapter_spec.js b/test/spec/modules/gridBidAdapter_spec.js index e884df40c5e..df119bb689d 100644 --- a/test/spec/modules/gridBidAdapter_spec.js +++ b/test/spec/modules/gridBidAdapter_spec.js @@ -105,7 +105,8 @@ describe('TheMediaGrid Adapter', function () { 'sizes': [[728, 90]], 'mediaTypes': { 'video': { - 'playerSize': [[400, 600]] + 'playerSize': [[400, 600]], + 'protocols': [1, 2, 3] }, 'banner': { 'sizes': [[728, 90]] @@ -281,7 +282,8 @@ describe('TheMediaGrid Adapter', function () { }, 'video': { 'w': 400, - 'h': 600 + 'h': 600, + 'protocols': [1, 2, 3] } }] }); @@ -311,68 +313,36 @@ describe('TheMediaGrid Adapter', function () { }); it('if userId is present payload must have user.ext param with right keys', function () { - const bidRequestsWithUserIds = bidRequests.map((bid) => { - return Object.assign({ - userId: { - id5id: { uid: 'id5id_1', ext: { linkType: 2 } }, - tdid: 'tdid_1', - digitrustid: {data: {id: 'DTID', keyv: 4, privacy: {optout: false}, producer: 'ABC', version: 2}}, - lipb: {lipbid: 'lipb_1'}, - idl_env: 'idl_env_1', - criteoId: 'criteoId_1' - } - }, bid); - }); - const request = spec.buildRequests(bidRequestsWithUserIds, bidderRequest); - expect(request.data).to.be.an('string'); - const payload = parseRequest(request.data); - expect(payload).to.have.property('user'); - expect(payload.user).to.have.property('ext'); - expect(payload.user.ext.digitrust).to.deep.equal({ - id: 'DTID', - keyv: 4, - privacy: { - optout: false + const eids = [ + { + source: 'pubcid.org', + uids: [{ + id: 'some-random-id-value', + atype: 1 + }] }, - producer: 'ABC', - version: 2 - }); - expect(payload.user.ext.eids).to.deep.equal([ { source: 'adserver.org', uids: [{ - id: 'tdid_1', + id: 'some-random-id-value', + atype: 1, ext: { rtiPartner: 'TDID' } }] - }, - { - source: 'id5-sync.com', - uids: [{ - id: 'id5id_1' - }], - ext: { linkType: 2 } - }, - { - source: 'liveintent.com', - uids: [{ - id: 'lipb_1' - }] - }, - { - source: 'identityLink', - uids: [{ - id: 'idl_env_1' - }] - }, - { - source: 'criteo.com', - uids: [{ - id: 'criteoId_1' - }] } - ]); + ]; + const bidRequestsWithUserIds = bidRequests.map((bid) => { + return Object.assign({ + userIdAsEids: eids + }, bid); + }); + const request = spec.buildRequests(bidRequestsWithUserIds, bidderRequest); + expect(request.data).to.be.an('string'); + const payload = parseRequest(request.data); + expect(payload).to.have.property('user'); + expect(payload.user).to.have.property('ext'); + expect(payload.user.ext.eids).to.deep.equal(eids); }); it('if schain is present payload must have source.ext.schain param', function () { @@ -403,7 +373,7 @@ describe('TheMediaGrid Adapter', function () { it('if content and segment is present in jwTargeting, payload must have right params', function () { const jsContent = {id: 'test_jw_content_id'}; const jsSegments = ['test_seg_1', 'test_seg_2']; - const bidRequestsWithUserIds = bidRequests.map((bid) => { + const bidRequestsWithJwTargeting = bidRequests.map((bid) => { return Object.assign({ rtd: { jwplayer: { @@ -415,7 +385,7 @@ describe('TheMediaGrid Adapter', function () { } }, bid); }); - const request = spec.buildRequests(bidRequestsWithUserIds, bidderRequest); + const request = spec.buildRequests(bidRequestsWithJwTargeting, bidderRequest); expect(request.data).to.be.an('string'); const payload = parseRequest(request.data); expect(payload).to.have.property('user'); @@ -521,7 +491,6 @@ describe('TheMediaGrid Adapter', function () { 'width': 300, 'height': 250, 'ad': '
test content 1
', - 'bidderCode': 'grid', 'currency': 'USD', 'mediaType': 'banner', 'netRevenue': false, @@ -579,7 +548,6 @@ describe('TheMediaGrid Adapter', function () { 'width': 300, 'height': 250, 'ad': '
test content 1
', - 'bidderCode': 'grid', 'currency': 'USD', 'mediaType': 'banner', 'netRevenue': false, @@ -593,7 +561,6 @@ describe('TheMediaGrid Adapter', function () { 'width': 300, 'height': 600, 'ad': '
test content 2
', - 'bidderCode': 'grid', 'currency': 'USD', 'mediaType': 'banner', 'netRevenue': false, @@ -607,7 +574,6 @@ describe('TheMediaGrid Adapter', function () { 'width': 728, 'height': 90, 'ad': '
test content 3
', - 'bidderCode': 'grid', 'currency': 'USD', 'mediaType': 'banner', 'netRevenue': false, @@ -667,7 +633,6 @@ describe('TheMediaGrid Adapter', function () { 'dealId': undefined, 'width': 300, 'height': 600, - 'bidderCode': 'grid', 'currency': 'USD', 'mediaType': 'video', 'netRevenue': false, @@ -684,7 +649,6 @@ describe('TheMediaGrid Adapter', function () { 'dealId': undefined, 'width': undefined, 'height': undefined, - 'bidderCode': 'grid', 'currency': 'USD', 'mediaType': 'video', 'netRevenue': false, @@ -816,7 +780,6 @@ describe('TheMediaGrid Adapter', function () { 'width': 300, 'height': 250, 'ad': '
test content 1
', - 'bidderCode': 'grid', 'currency': 'USD', 'mediaType': 'banner', 'netRevenue': false, @@ -830,7 +793,6 @@ describe('TheMediaGrid Adapter', function () { 'width': 300, 'height': 600, 'ad': '
test content 2
', - 'bidderCode': 'grid', 'currency': 'USD', 'mediaType': 'banner', 'netRevenue': false, @@ -844,7 +806,6 @@ describe('TheMediaGrid Adapter', function () { 'width': 728, 'height': 90, 'ad': '
test content 3
', - 'bidderCode': 'grid', 'currency': 'USD', 'mediaType': 'banner', 'netRevenue': false, @@ -858,7 +819,6 @@ describe('TheMediaGrid Adapter', function () { 'width': 300, 'height': 600, 'ad': '
test content 4
', - 'bidderCode': 'grid', 'currency': 'USD', 'mediaType': 'banner', 'netRevenue': false, diff --git a/test/spec/modules/gumgumBidAdapter_spec.js b/test/spec/modules/gumgumBidAdapter_spec.js index 701ce9a7e81..0d37c8b1d25 100644 --- a/test/spec/modules/gumgumBidAdapter_spec.js +++ b/test/spec/modules/gumgumBidAdapter_spec.js @@ -1,3 +1,5 @@ +import { BANNER, VIDEO } from 'src/mediaTypes.js'; + import { expect } from 'chai'; import { newBidder } from 'src/adapters/bidderFactory.js'; import { spec } from 'modules/gumgumBidAdapter.js'; @@ -34,7 +36,7 @@ describe('gumgumAdapter', function () { it('should return true when required params found', function () { const zoneBid = { ...bid, params: { 'zone': '123' } }; - const pubIdBid = { ...bid, params: { 'pubId': '123' } }; + const pubIdBid = { ...bid, params: { 'pubId': 123 } }; expect(spec.isBidRequestValid(bid)).to.equal(true); expect(spec.isBidRequestValid(zoneBid)).to.equal(true); expect(spec.isBidRequestValid(pubIdBid)).to.equal(true); @@ -142,23 +144,54 @@ describe('gumgumAdapter', function () { protocols: [1, 2] } }; + const zoneParam = { 'zone': '123a' }; + const pubIdParam = { 'pubId': 123 }; - describe('zone param', function () { - const zoneParam = { 'zone': '123a' }; + it('should set pubId param if found', function () { + const request = { ...bidRequests[0], params: pubIdParam }; + const bidRequest = spec.buildRequests([request])[0]; + expect(bidRequest.data.pubId).to.equal(pubIdParam.pubId); + }); - it('should set t and pi param', function () { - const request = { ...bidRequests[0], params: zoneParam }; - const bidRequest = spec.buildRequests([request])[0]; - expect(bidRequest.data.t).to.equal(zoneParam.zone); - expect(bidRequest.data.pi).to.equal(2); - }); - it('should set the correct pi param if slot param is found', function () { - const request = { ...bidRequests[0], params: { ...zoneParam, 'slot': 1 } }; - const bidRequest = spec.buildRequests([request])[0]; - expect(bidRequest.data.pi).to.equal(3); - }); + it('should set t param when zone param is found', function () { + const request = { ...bidRequests[0], params: zoneParam }; + const bidRequest = spec.buildRequests([request])[0]; + expect(bidRequest.data.t).to.equal(zoneParam.zone); + }); + + it('should set the iriscat param when found', function () { + const request = { ...bidRequests[0], params: { iriscat: 'abc123' } } + const bidRequest = spec.buildRequests([request])[0]; + expect(bidRequest.data).to.have.property('iriscat'); + }); + + it('should not set the iriscat param when not found', function () { + const request = { ...bidRequests[0] } + const bidRequest = spec.buildRequests([request])[0]; + expect(bidRequest.data).to.not.have.property('iriscat'); + }); + + it('should set the irisid param when found', function () { + const request = { ...bidRequests[0], params: { irisid: 'abc123' } } + const bidRequest = spec.buildRequests([request])[0]; + expect(bidRequest.data).to.have.property('irisid'); + }); + + it('should not set the irisid param when not found', function () { + const request = { ...bidRequests[0] } + const bidRequest = spec.buildRequests([request])[0]; + expect(bidRequest.data).to.not.have.property('irisid'); + }); + + it('should not set the irisid param when not of type string', function () { + const request = { ...bidRequests[0], params: { irisid: 123456 } } + const bidRequest = spec.buildRequests([request])[0]; + expect(bidRequest.data).to.not.have.property('irisid'); + }); + + describe('product id', function () { it('should set the correct pi param if native param is found', function () { - const request = { ...bidRequests[0], params: { ...zoneParam, 'native': 2 } }; + const request = { ...bidRequests[0], params: { ...zoneParam, native: 2 } }; const bidRequest = spec.buildRequests([request])[0]; expect(bidRequest.data.pi).to.equal(5); }); @@ -173,25 +206,18 @@ describe('gumgumAdapter', function () { const bidRequest = spec.buildRequests([request])[0]; expect(bidRequest.data.pi).to.equal(6); }); - }); - - describe('pubId zone', function () { - const pubIdParam = { 'pubId': 'abc' }; - - it('should set t param', function () { - const request = { ...bidRequests[0], params: pubIdParam }; + it('should set the correct pi param if slot param is found', function () { + const request = { ...bidRequests[0], params: { ...zoneParam, slot: '123s' } }; const bidRequest = spec.buildRequests([request])[0]; - expect(bidRequest.data.pubId).to.equal(pubIdParam.pubId); + expect(bidRequest.data.pi).to.equal(3); }); - - it('should set the correct pi depending on what is found in mediaTypes', function () { - const request = { ...bidRequests[0], params: pubIdParam }; - const bidRequest = spec.buildRequests([request])[0]; - const vidRequest = { ...bidRequests[0], mediaTypes: vidMediaTypes, params: { 'videoPubID': 123 } }; - const vidBidRequest = spec.buildRequests([vidRequest])[0]; - - expect(bidRequest.data.pi).to.equal(2); - expect(vidBidRequest.data.pi).to.equal(7); + it('should default the pi param to 2 if only zone or pubId param is found', function () { + const zoneRequest = { ...bidRequests[0], params: zoneParam }; + const pubIdRequest = { ...bidRequests[0], params: pubIdParam }; + const zoneBidRequest = spec.buildRequests([zoneRequest])[0]; + const pubIdBidRequest = spec.buildRequests([pubIdRequest])[0]; + expect(zoneBidRequest.data.pi).to.equal(2); + expect(pubIdBidRequest.data.pi).to.equal(2); }); }); @@ -462,16 +488,15 @@ describe('gumgumAdapter', function () { pi: 3 } let expectedResponse = { - 'ad': '

I am an ad

', - 'cpm': 0, - 'creativeId': 29593, - 'currency': 'USD', - 'height': '250', - 'netRevenue': true, - 'requestId': 12345, - 'width': '300', - // dealId: DEAL_ID, - // referrer: REFERER, + ad: '

I am an ad

', + cpm: 0, + creativeId: 29593, + currency: 'USD', + height: '250', + netRevenue: true, + requestId: 12345, + width: '300', + mediaType: BANNER, ttl: 60 }; @@ -552,6 +577,20 @@ describe('gumgumAdapter', function () { const decodedResponse = JSON.parse(atob(bidResponse)); expect(decodedResponse.jcsi).to.eql(JCSI); }); + + it('sets the correct mediaType depending on product', function () { + const bannerBidResponse = spec.interpretResponse({ body: serverResponse }, bidRequest)[0]; + const invideoBidResponse = spec.interpretResponse({ body: serverResponse }, { ...bidRequest, data: { pi: 6 } })[0]; + const videoBidResponse = spec.interpretResponse({ body: serverResponse }, { ...bidRequest, data: { pi: 7 } })[0]; + expect(bannerBidResponse.mediaType).to.equal(BANNER); + expect(invideoBidResponse.mediaType).to.equal(VIDEO); + expect(videoBidResponse.mediaType).to.equal(VIDEO); + }); + + it('sets a vastXml property if mediaType is video', function () { + const videoBidResponse = spec.interpretResponse({ body: serverResponse }, { ...bidRequest, data: { pi: 7 } })[0]; + expect(videoBidResponse.vastXml).to.exist; + }); }) describe('getUserSyncs', function () { const syncOptions = { diff --git a/test/spec/modules/id5IdSystem_spec.js b/test/spec/modules/id5IdSystem_spec.js index 845cf7fa010..7f104ff403d 100644 --- a/test/spec/modules/id5IdSystem_spec.js +++ b/test/spec/modules/id5IdSystem_spec.js @@ -1,12 +1,14 @@ import { id5IdSubmodule, ID5_STORAGE_NAME, + ID5_PRIVACY_STORAGE_NAME, getFromLocalStorage, storeInLocalStorage, expDaysStr, nbCacheName, getNbFromCache, - storeNbInCache + storeNbInCache, + isInControlGroup } from 'modules/id5IdSystem.js'; import { init, requestBidsHook, setSubmoduleRegistry, coreStorage } from 'modules/userId/index.js'; import { config } from 'src/config.js'; @@ -26,16 +28,19 @@ describe('ID5 ID System', function() { const ID5_NB_STORAGE_NAME = nbCacheName(ID5_TEST_PARTNER_ID); const ID5_STORED_ID = 'storedid5id'; const ID5_STORED_SIGNATURE = '123456'; + const ID5_STORED_LINK_TYPE = 1; const ID5_STORED_OBJ = { 'universal_uid': ID5_STORED_ID, - 'signature': ID5_STORED_SIGNATURE + 'signature': ID5_STORED_SIGNATURE, + 'link_type': ID5_STORED_LINK_TYPE }; const ID5_RESPONSE_ID = 'newid5id'; const ID5_RESPONSE_SIGNATURE = 'abcdef'; + const ID5_RESPONSE_LINK_TYPE = 2; const ID5_JSON_RESPONSE = { 'universal_uid': ID5_RESPONSE_ID, 'signature': ID5_RESPONSE_SIGNATURE, - 'link_type': 0 + 'link_type': ID5_RESPONSE_LINK_TYPE }; function getId5FetchConfig(storageName = ID5_STORAGE_NAME, storageType = 'html5') { @@ -177,10 +182,10 @@ describe('ID5 ID System', function() { it('should call the ID5 server with pd field when pd config is set', function () { const pubData = 'b50ca08271795a8e7e4012813f23d505193d75c0f2e2bb99baa63aa822f66ed3'; - let config = getId5FetchConfig(); - config.params.pd = pubData; + let id5Config = getId5FetchConfig(); + id5Config.params.pd = pubData; - let submoduleCallback = id5IdSubmodule.getId(config, undefined, ID5_STORED_OBJ).callback; + let submoduleCallback = id5IdSubmodule.getId(id5Config, undefined, ID5_STORED_OBJ).callback; submoduleCallback(callbackSpy); let request = server.requests[0]; @@ -191,10 +196,10 @@ describe('ID5 ID System', function() { }); it('should call the ID5 server with empty pd field when pd config is not set', function () { - let config = getId5FetchConfig(); - config.params.pd = undefined; + let id5Config = getId5FetchConfig(); + id5Config.params.pd = undefined; - let submoduleCallback = id5IdSubmodule.getId(config, undefined, ID5_STORED_OBJ).callback; + let submoduleCallback = id5IdSubmodule.getId(id5Config, undefined, ID5_STORED_OBJ).callback; submoduleCallback(callbackSpy); let request = server.requests[0]; @@ -233,6 +238,74 @@ describe('ID5 ID System', function() { expect(getNbFromCache(ID5_TEST_PARTNER_ID)).to.be.eq(0); }); + + it('should call the ID5 server with ab feature = 1 when abTesting is turned on', function () { + let id5Config = getId5FetchConfig(); + id5Config.params.abTesting = { enabled: true, controlGroupPct: 10 } + + let submoduleCallback = id5IdSubmodule.getId(id5Config, undefined, ID5_STORED_OBJ).callback; + submoduleCallback(callbackSpy); + + let request = server.requests[0]; + let requestBody = JSON.parse(request.requestBody); + expect(requestBody.features.ab).to.eq(1); + + request.respond(200, responseHeader, JSON.stringify(ID5_JSON_RESPONSE)); + }); + + it('should call the ID5 server without ab feature when abTesting is turned off', function () { + let id5Config = getId5FetchConfig(); + id5Config.params.abTesting = { enabled: false, controlGroupPct: 10 } + + let submoduleCallback = id5IdSubmodule.getId(id5Config, undefined, ID5_STORED_OBJ).callback; + submoduleCallback(callbackSpy); + + let request = server.requests[0]; + let requestBody = JSON.parse(request.requestBody); + expect(requestBody.features).to.be.undefined; + + request.respond(200, responseHeader, JSON.stringify(ID5_JSON_RESPONSE)); + }); + + it('should call the ID5 server without ab feature when when abTesting is not set', function () { + let id5Config = getId5FetchConfig(); + + let submoduleCallback = id5IdSubmodule.getId(id5Config, undefined, ID5_STORED_OBJ).callback; + submoduleCallback(callbackSpy); + + let request = server.requests[0]; + let requestBody = JSON.parse(request.requestBody); + expect(requestBody.features).to.be.undefined; + + request.respond(200, responseHeader, JSON.stringify(ID5_JSON_RESPONSE)); + }); + + it('should store the privacy object from the ID5 server response', function () { + let submoduleCallback = id5IdSubmodule.getId(getId5FetchConfig(), undefined, ID5_STORED_OBJ).callback; + submoduleCallback(callbackSpy); + + let request = server.requests[0]; + + let responseObject = utils.deepClone(ID5_JSON_RESPONSE); + responseObject.privacy = { + jurisdiction: 'gdpr', + id5_consent: true + }; + request.respond(200, responseHeader, JSON.stringify(responseObject)); + expect(getFromLocalStorage(ID5_PRIVACY_STORAGE_NAME)).to.be.eq(JSON.stringify(responseObject.privacy)); + coreStorage.removeDataFromLocalStorage(ID5_PRIVACY_STORAGE_NAME); + }); + + it('should not store a privacy object if not part of ID5 server response', function () { + coreStorage.removeDataFromLocalStorage(ID5_PRIVACY_STORAGE_NAME); + let submoduleCallback = id5IdSubmodule.getId(getId5FetchConfig(), undefined, ID5_STORED_OBJ).callback; + submoduleCallback(callbackSpy); + + let request = server.requests[0]; + + request.respond(200, responseHeader, JSON.stringify(ID5_JSON_RESPONSE)); + expect(getFromLocalStorage(ID5_PRIVACY_STORAGE_NAME)).to.be.null; + }); }); describe('Request Bids Hook', function() { @@ -266,10 +339,13 @@ describe('ID5 ID System', function() { expect(bid.userId.id5id.uid).to.equal(ID5_STORED_ID); expect(bid.userIdAsEids[0]).to.deep.equal({ source: ID5_SOURCE, - uids: [{ id: ID5_STORED_ID, atype: 1 }], - ext: { - linkType: 0 - } + uids: [{ + id: ID5_STORED_ID, + atype: 1, + ext: { + linkType: ID5_STORED_LINK_TYPE + } + }] }); }); }); @@ -306,7 +382,7 @@ describe('ID5 ID System', function() { config.setConfig(getFetchLocalStorageConfig()); let innerAdUnits; - requestBidsHook((config) => { innerAdUnits = config.adUnits }, {adUnits}); + requestBidsHook((adUnitConfig) => { innerAdUnits = adUnitConfig.adUnits }, {adUnits}); expect(getNbFromCache(ID5_TEST_PARTNER_ID)).to.be.eq(1); }); @@ -320,7 +396,7 @@ describe('ID5 ID System', function() { config.setConfig(getFetchLocalStorageConfig()); let innerAdUnits; - requestBidsHook((config) => { innerAdUnits = config.adUnits }, {adUnits}); + requestBidsHook((adUnitConfig) => { innerAdUnits = adUnitConfig.adUnits }, {adUnits}); expect(getNbFromCache(ID5_TEST_PARTNER_ID)).to.be.eq(2); }); @@ -338,7 +414,7 @@ describe('ID5 ID System', function() { config.setConfig(id5Config); let innerAdUnits; - requestBidsHook((config) => { innerAdUnits = config.adUnits }, {adUnits}); + requestBidsHook((adUnitConfig) => { innerAdUnits = adUnitConfig.adUnits }, {adUnits}); expect(getNbFromCache(ID5_TEST_PARTNER_ID)).to.be.eq(2); @@ -360,13 +436,199 @@ describe('ID5 ID System', function() { }); describe('Decode stored object', function() { - const expectedDecodedObject = { id5id: { uid: ID5_STORED_ID, ext: { linkType: 0 } } }; + const expectedDecodedObject = { id5id: { uid: ID5_STORED_ID, ext: { linkType: ID5_STORED_LINK_TYPE } } }; it('should properly decode from a stored object', function() { - expect(id5IdSubmodule.decode(ID5_STORED_OBJ)).to.deep.equal(expectedDecodedObject); + expect(id5IdSubmodule.decode(ID5_STORED_OBJ, getId5FetchConfig())).to.deep.equal(expectedDecodedObject); }); it('should return undefined if passed a string', function() { - expect(id5IdSubmodule.decode('somestring')).to.eq(undefined); + expect(id5IdSubmodule.decode('somestring', getId5FetchConfig())).to.eq(undefined); + }); + }); + + describe('A/B Testing', function() { + const expectedDecodedObjectWithIdAbOff = { id5id: { uid: ID5_STORED_ID, ext: { linkType: ID5_STORED_LINK_TYPE } } }; + const expectedDecodedObjectWithIdAbOn = { id5id: { uid: ID5_STORED_ID, ext: { linkType: ID5_STORED_LINK_TYPE, abTestingControlGroup: false } } }; + const expectedDecodedObjectWithoutIdAbOn = { id5id: { uid: 0, ext: { linkType: 0, abTestingControlGroup: true } } }; + let testConfig; + + beforeEach(function() { + testConfig = getId5FetchConfig(); + }); + + describe('Configuration Validation', function() { + let logErrorSpy; + let logInfoSpy; + + beforeEach(function() { + logErrorSpy = sinon.spy(utils, 'logError'); + logInfoSpy = sinon.spy(utils, 'logInfo'); + }); + afterEach(function() { + logErrorSpy.restore(); + logInfoSpy.restore(); + }); + + // A/B Testing ON, but invalid config + let testInvalidAbTestingConfigsWithError = [ + { enabled: true }, + { enabled: true, controlGroupPct: 2 }, + { enabled: true, controlGroupPct: -1 }, + { enabled: true, controlGroupPct: 'a' }, + { enabled: true, controlGroupPct: true } + ]; + testInvalidAbTestingConfigsWithError.forEach((testAbTestingConfig) => { + it('should be undefined if ratio is invalid', () => { + expect(isInControlGroup('userId', testAbTestingConfig.controlGroupPct)).to.be.undefined; + }); + it('should error if config is invalid, and always return an ID', function () { + testConfig.params.abTesting = testAbTestingConfig; + let decoded = id5IdSubmodule.decode(ID5_STORED_OBJ, testConfig); + expect(decoded).to.deep.equal(expectedDecodedObjectWithIdAbOn); + sinon.assert.calledOnce(logErrorSpy); + }); + }); + + // A/B Testing OFF, with invalid config (ignore) + let testInvalidAbTestingConfigsWithoutError = [ + { enabled: false, controlGroupPct: -1 }, + { enabled: false, controlGroupPct: 2 }, + { enabled: false, controlGroupPct: 'a' }, + { enabled: false, controlGroupPct: true } + ]; + testInvalidAbTestingConfigsWithoutError.forEach((testAbTestingConfig) => { + it('should be undefined if ratio is invalid', () => { + expect(isInControlGroup('userId', testAbTestingConfig.controlGroupPct)).to.be.undefined; + }); + it('should not error if config is invalid but A/B testing is off, and always return an ID', function () { + testConfig.params.abTesting = testAbTestingConfig; + let decoded = id5IdSubmodule.decode(ID5_STORED_OBJ, testConfig); + expect(decoded).to.deep.equal(expectedDecodedObjectWithIdAbOff); + sinon.assert.notCalled(logErrorSpy); + sinon.assert.notCalled(logInfoSpy); + }); + }); + + // A/B Testing ON, with valid config + let testValidConfigs = [ + { enabled: true, controlGroupPct: 0 }, + { enabled: true, controlGroupPct: 0.5 }, + { enabled: true, controlGroupPct: 1 } + ]; + testValidConfigs.forEach((testAbTestingConfig) => { + it('should not be undefined if ratio is valid', () => { + expect(isInControlGroup('userId', testAbTestingConfig.controlGroupPct)).to.not.be.undefined; + }); + it('should not error if config is valid', function () { + testConfig.params.abTesting = testAbTestingConfig; + id5IdSubmodule.decode(ID5_STORED_OBJ, testConfig); + sinon.assert.notCalled(logErrorSpy); + sinon.assert.calledOnce(logInfoSpy); + }); + }); + }); + + describe('A/B Testing Config is not Set', function() { + let randStub; + + beforeEach(function() { + randStub = sinon.stub(Math, 'random').callsFake(function() { + return 0; + }); + }); + afterEach(function () { + randStub.restore(); + }); + + it('should expose ID when A/B config is not set', function () { + let decoded = id5IdSubmodule.decode(ID5_STORED_OBJ, testConfig); + expect(decoded).to.deep.equal(expectedDecodedObjectWithIdAbOff); + }); + + it('should expose ID when A/B config is empty', function () { + testConfig.params.abTesting = { }; + + let decoded = id5IdSubmodule.decode(ID5_STORED_OBJ, testConfig); + expect(decoded).to.deep.equal(expectedDecodedObjectWithIdAbOff); + }); + }); + + describe('A/B Testing Config is Set', function() { + let randStub; + + beforeEach(function() { + randStub = sinon.stub(Math, 'random').callsFake(function() { + return 0.25; + }); + }); + afterEach(function () { + randStub.restore(); + }); + + describe('IsInControlGroup', function () { + it('Nobody is in a 0% control group', function () { + expect(isInControlGroup('dsdndskhsdks', 0)).to.be.false; + expect(isInControlGroup('3erfghyuijkm', 0)).to.be.false; + expect(isInControlGroup('', 0)).to.be.false; + expect(isInControlGroup(undefined, 0)).to.be.false; + }); + + it('Everybody is in a 100% control group', function () { + expect(isInControlGroup('dsdndskhsdks', 1)).to.be.true; + expect(isInControlGroup('3erfghyuijkm', 1)).to.be.true; + expect(isInControlGroup('', 1)).to.be.true; + expect(isInControlGroup(undefined, 1)).to.be.true; + }); + + it('Being in the control group must be consistant', function () { + const inControlGroup = isInControlGroup('dsdndskhsdks', 0.5); + expect(inControlGroup === isInControlGroup('dsdndskhsdks', 0.5)).to.be.true; + expect(inControlGroup === isInControlGroup('dsdndskhsdks', 0.5)).to.be.true; + expect(inControlGroup === isInControlGroup('dsdndskhsdks', 0.5)).to.be.true; + }); + + it('Control group ratio must be within a 10% error on a large sample', function () { + let nbInControlGroup = 0; + const sampleSize = 100; + for (let i = 0; i < sampleSize; i++) { + nbInControlGroup = nbInControlGroup + (isInControlGroup('R$*df' + i, 0.5) ? 1 : 0); + } + expect(nbInControlGroup).to.be.greaterThan(sampleSize / 2 - sampleSize / 10); + expect(nbInControlGroup).to.be.lessThan(sampleSize / 2 + sampleSize / 10); + }); + }); + + describe('Decode', function() { + it('should expose ID when A/B testing is off', function () { + testConfig.params.abTesting = { + enabled: false, + controlGroupPct: 0.5 + }; + + let decoded = id5IdSubmodule.decode(ID5_STORED_OBJ, testConfig); + expect(decoded).to.deep.equal(expectedDecodedObjectWithIdAbOff); + }); + + it('should expose ID when no one is in control group', function () { + testConfig.params.abTesting = { + enabled: true, + controlGroupPct: 0 + }; + + let decoded = id5IdSubmodule.decode(ID5_STORED_OBJ, testConfig); + expect(decoded).to.deep.equal(expectedDecodedObjectWithIdAbOn); + }); + + it('should not expose ID when everyone is in control group', function () { + testConfig.params.abTesting = { + enabled: true, + controlGroupPct: 1 + }; + + let decoded = id5IdSubmodule.decode(ID5_STORED_OBJ, testConfig); + expect(decoded).to.deep.equal(expectedDecodedObjectWithoutIdAbOn); + }); + }); }); }); }); diff --git a/test/spec/modules/idLibrary_spec.js b/test/spec/modules/idImportLibrary_spec.js similarity index 79% rename from test/spec/modules/idLibrary_spec.js rename to test/spec/modules/idImportLibrary_spec.js index da61850f29b..699c2c43a94 100644 --- a/test/spec/modules/idLibrary_spec.js +++ b/test/spec/modules/idImportLibrary_spec.js @@ -1,5 +1,5 @@ import * as utils from 'src/utils.js'; -import * as idlibrary from 'modules/idLibrary.js'; +import * as idImportlibrary from 'modules/idImportLibrary.js'; var expect = require('chai').expect; @@ -20,7 +20,7 @@ describe('currency', function () { utils.logInfo.restore(); utils.logError.restore(); fakeCurrencyFileServer.restore(); - idlibrary.setConfig({}); + idImportlibrary.setConfig({}); }); describe('setConfig', function () { @@ -35,27 +35,27 @@ describe('currency', function () { }); it('results when no config available', function () { - idlibrary.setConfig({}); + idImportlibrary.setConfig({}); sinon.assert.called(utils.logError); }); it('results with config available', function () { - idlibrary.setConfig({ 'url': 'URL' }); + idImportlibrary.setConfig({ 'url': 'URL' }); sinon.assert.called(utils.logInfo); }); it('results with config default debounce ', function () { let config = { 'url': 'URL' } - idlibrary.setConfig(config); + idImportlibrary.setConfig(config); expect(config.debounce).to.be.equal(250); }); it('results with config default fullscan ', function () { let config = { 'url': 'URL' } - idlibrary.setConfig(config); - expect(config.fullscan).to.be.equal(true); + idImportlibrary.setConfig(config); + expect(config.fullscan).to.be.equal(false); }); it('results with config fullscan ', function () { - let config = { 'url': 'URL', 'fullscan': false } - idlibrary.setConfig(config); - expect(config.fullscan).to.be.equal(false); + let config = { 'url': 'URL', 'fullscan': true } + idImportlibrary.setConfig(config); + expect(config.fullscan).to.be.equal(true); }); }); }); diff --git a/test/spec/modules/improvedigitalBidAdapter_spec.js b/test/spec/modules/improvedigitalBidAdapter_spec.js index 89ec5aed8c3..4d0da695d95 100644 --- a/test/spec/modules/improvedigitalBidAdapter_spec.js +++ b/test/spec/modules/improvedigitalBidAdapter_spec.js @@ -28,6 +28,12 @@ describe('Improve Digital Adapter Tests', function () { sizes: [[300, 250], [160, 600], ['blah', 150], [-1, 300], [300, -5]] }; + const videoParams = { + skip: 1, + skipmin: 5, + skipafter: 30 + } + const instreamBidRequest = utils.deepClone(simpleBidRequest); instreamBidRequest.mediaTypes = { video: { @@ -280,14 +286,23 @@ describe('Improve Digital Adapter Tests', function () { expect(params.bid_request.referrer).to.equal('https://blah.com/test.html'); }); + it('should not add video params for banner', function () { + const bidRequest = JSON.parse(JSON.stringify(simpleBidRequest)); + bidRequest.params.video = videoParams; + const request = spec.buildRequests([bidRequest], bidderRequest)[0]; + const params = JSON.parse(decodeURIComponent(request.data.substring(PARAM_PREFIX.length))); + expect(params.bid_request.imp[0].video).to.not.exist; + }); + it('should add ad type for instream video', function () { - let bidRequest = Object.assign({}, simpleBidRequest); + let bidRequest = JSON.parse(JSON.stringify(simpleBidRequest)); bidRequest.mediaType = 'video'; let request = spec.buildRequests([bidRequest], bidderRequest)[0]; let params = JSON.parse(decodeURIComponent(request.data.substring(PARAM_PREFIX.length))); expect(params.bid_request.imp[0].ad_types).to.deep.equal(['video']); + expect(params.bid_request.imp[0].video).to.not.exist; - bidRequest = Object.assign({}, simpleBidRequest); + bidRequest = JSON.parse(JSON.stringify(simpleBidRequest)); bidRequest.mediaTypes = { video: { context: 'instream', @@ -297,18 +312,89 @@ describe('Improve Digital Adapter Tests', function () { request = spec.buildRequests([bidRequest], bidderRequest)[0]; params = JSON.parse(decodeURIComponent(request.data.substring(PARAM_PREFIX.length))); expect(params.bid_request.imp[0].ad_types).to.deep.equal(['video']); + expect(params.bid_request.imp[0].video).to.not.exist; }); it('should not set ad type for outstream video', function() { const request = spec.buildRequests([outstreamBidRequest])[0]; const params = JSON.parse(decodeURIComponent(request.data.substring(PARAM_PREFIX.length))); expect(params.bid_request.imp[0].ad_types).to.not.exist; + expect(params.bid_request.imp[0].video).to.not.exist; }); it('should not set ad type for multi-format bids', function() { const request = spec.buildRequests([multiFormatBidRequest], bidderRequest)[0]; const params = JSON.parse(decodeURIComponent(request.data.substring(PARAM_PREFIX.length))); expect(params.bid_request.imp[0].ad_types).to.not.exist; + expect(params.bid_request.imp[0].video).to.not.exist; + }); + + it('should set video params for instream', function() { + const bidRequest = JSON.parse(JSON.stringify(instreamBidRequest)); + bidRequest.params.video = videoParams; + const request = spec.buildRequests([bidRequest])[0]; + const params = JSON.parse(decodeURIComponent(request.data.substring(PARAM_PREFIX.length))); + expect(params.bid_request.imp[0].video).to.deep.equal(videoParams); + }); + + it('should set skip params only if skip=1', function() { + const bidRequest = JSON.parse(JSON.stringify(instreamBidRequest)); + // 1 + const videoTest = { + skip: 1, + skipmin: 5, + skipafter: 30 + } + bidRequest.params.video = videoTest; + let request = spec.buildRequests([bidRequest])[0]; + let params = JSON.parse(decodeURIComponent(request.data.substring(PARAM_PREFIX.length))); + expect(params.bid_request.imp[0].video).to.deep.equal(videoTest); + + // 0 - leave out skipmin and skipafter + videoTest.skip = 0; + bidRequest.params.video = videoTest; + request = spec.buildRequests([bidRequest])[0]; + params = JSON.parse(decodeURIComponent(request.data.substring(PARAM_PREFIX.length))); + expect(params.bid_request.imp[0].video).to.deep.equal({ skip: 0 }); + + // other + videoTest.skip = 'blah'; + bidRequest.params.video = videoTest; + request = spec.buildRequests([bidRequest])[0]; + params = JSON.parse(decodeURIComponent(request.data.substring(PARAM_PREFIX.length))); + expect(params.bid_request.imp[0].video).to.not.exist; + }); + + it('should ignore invalid/unexpected video params', function() { + const bidRequest = JSON.parse(JSON.stringify(instreamBidRequest)); + // 1 + const videoTest = { + skip: 1, + skipmin: 5, + skipafter: 30 + } + const videoTestInvParam = Object.assign({}, videoTest); + videoTestInvParam.blah = 1; + bidRequest.params.video = videoTestInvParam; + let request = spec.buildRequests([bidRequest])[0]; + let params = JSON.parse(decodeURIComponent(request.data.substring(PARAM_PREFIX.length))); + expect(params.bid_request.imp[0].video).to.deep.equal(videoTest); + }); + + it('should set video params for outstream', function() { + const bidRequest = JSON.parse(JSON.stringify(outstreamBidRequest)); + bidRequest.params.video = videoParams; + const request = spec.buildRequests([bidRequest])[0]; + const params = JSON.parse(decodeURIComponent(request.data.substring(PARAM_PREFIX.length))); + expect(params.bid_request.imp[0].video).to.deep.equal(videoParams); + }); + + it('should set video params for multi-format', function() { + const bidRequest = JSON.parse(JSON.stringify(multiFormatBidRequest)); + bidRequest.params.video = videoParams; + const request = spec.buildRequests([bidRequest])[0]; + const params = JSON.parse(decodeURIComponent(request.data.substring(PARAM_PREFIX.length))); + expect(params.bid_request.imp[0].video).to.deep.equal(videoParams); }); it('should not set Prebid sizes in bid request for instream video', function () { diff --git a/test/spec/modules/inmarBidAdapter_spec.js b/test/spec/modules/inmarBidAdapter_spec.js index 86b7ab3a8af..998fe20d369 100644 --- a/test/spec/modules/inmarBidAdapter_spec.js +++ b/test/spec/modules/inmarBidAdapter_spec.js @@ -17,7 +17,6 @@ describe('Inmar adapter tests', function () { }, bidder: 'inmar', params: { - adnetId: 'ADb1f40rmi', partnerId: 12345 }, auctionId: '0cb3144c-d084-4686-b0d6-f5dbe917c563', @@ -39,7 +38,6 @@ describe('Inmar adapter tests', function () { }, bidder: 'inmar', params: { - adnetId: 'ADb1f40rmi', partnerId: 12345 }, auctionId: '0cb3144c-d084-4686-b0d6-f5dbe917c563', @@ -60,7 +58,6 @@ describe('Inmar adapter tests', function () { ], bidder: 'inmar', params: { - adnetId: 'ADb1f40rmi', partnerId: 12345, }, auctionId: '851adee7-d843-48f9-a7e9-9ff00573fcbf', @@ -118,7 +115,6 @@ describe('Inmar adapter tests', function () { expect(request).to.have.property('method').and.to.equal('POST'); const requestContent = JSON.parse(request.data); - expect(requestContent.bidRequests[0].params).to.have.property('adnetId').and.to.equal('ADb1f40rmi'); expect(requestContent.bidRequests[0].params).to.have.property('partnerId').and.to.equal(12345); expect(requestContent.bidRequests[0]).to.have.property('auctionId').and.to.equal('0cb3144c-d084-4686-b0d6-f5dbe917c563'); expect(requestContent.bidRequests[0]).to.have.property('bidId').and.to.equal('2c7c8e9c900244'); @@ -198,19 +194,12 @@ describe('Inmar adapter tests', function () { })).to.equal(false); expect(spec.isBidRequestValid({ params: { - adnetId: 'ADb1f40rmi' } })).to.equal(false); expect(spec.isBidRequestValid({ params: { partnerId: 12345 } - })).to.equal(false); - expect(spec.isBidRequestValid({ - params: { - adnetId: 'ADb1f40rmi', - partnerId: 12345 - } })).to.equal(true); }); diff --git a/test/spec/modules/inskinBidAdapter_spec.js b/test/spec/modules/inskinBidAdapter_spec.js index e817b3e3b81..cedaff2a0cf 100644 --- a/test/spec/modules/inskinBidAdapter_spec.js +++ b/test/spec/modules/inskinBidAdapter_spec.js @@ -250,7 +250,7 @@ describe('InSkin BidAdapter', function () { const payload = JSON.parse(request.data); expect(payload.keywords).to.be.an('array').that.is.empty; - expect(payload.placements[0].properties).to.be.undefined; + expect(payload.placements[0].properties.restrictions).to.be.undefined; }); it('should add keywords if TCF v2 purposes are not granted', function () { diff --git a/test/spec/modules/ipromBidAdapter_spec.js b/test/spec/modules/ipromBidAdapter_spec.js new file mode 100644 index 00000000000..535cc332b21 --- /dev/null +++ b/test/spec/modules/ipromBidAdapter_spec.js @@ -0,0 +1,193 @@ +import {expect} from 'chai'; +import {spec} from 'modules/ipromBidAdapter.js'; + +describe('iPROM Adapter', function () { + let bidRequests; + let bidderRequest; + + beforeEach(function () { + bidRequests = [ + { + bidder: 'iprom', + params: { + id: '1234', + dimension: '300x250', + }, + adUnitCode: '/19966331/header-bid-tag-1', + mediaTypes: { + banner: { + sizes: [[300, 250], [300, 600]], + } + }, + bidId: '29a72b151f7bd3', + auctionId: 'e36abb27-g3b1-1ad6-8a4c-701c8919d3hh', + bidderRequestId: '2z76da40m1b3cb8', + transactionId: 'j51lhf58-1ad6-g3b1-3j6s-912c9493g0gu' + } + ]; + + bidderRequest = { + timeout: 3000, + refererInfo: { + referer: 'https://adserver.si/index.html', + reachedTop: true, + numIframes: 1, + stack: [ + 'https://adserver.si/index.html', + 'https://adserver.si/iframe1.html', + ] + } + } + }); + + describe('validating bids', function () { + it('should accept valid bid', function () { + let validBid = { + bidder: 'iprom', + params: { + id: '1234', + dimension: '300x250', + }, + }; + + const isValid = spec.isBidRequestValid(validBid); + + expect(isValid).to.equal(true); + }); + + it('should reject bid if missing dimension and id', function () { + let invalidBid = { + bidder: 'iprom', + params: {} + }; + + const isValid = spec.isBidRequestValid(invalidBid); + + expect(isValid).to.equal(false); + }); + + it('should reject bid if missing dimension', function () { + let invalidBid = { + bidder: 'iprom', + params: { + id: '1234', + } + }; + + const isValid = spec.isBidRequestValid(invalidBid); + + expect(isValid).to.equal(false); + }); + + it('should reject bid if dimension is not a string', function () { + let invalidBid = { + bidder: 'iprom', + params: { + id: '1234', + dimension: 404, + } + }; + + const isValid = spec.isBidRequestValid(invalidBid); + + expect(isValid).to.equal(false); + }); + + it('should reject bid if missing id', function () { + let invalidBid = { + bidder: 'iprom', + params: { + dimension: '300x250', + } + }; + + const isValid = spec.isBidRequestValid(invalidBid); + + expect(isValid).to.equal(false); + }); + + it('should reject bid if id is not a string', function () { + let invalidBid = { + bidder: 'iprom', + params: { + id: 1234, + dimension: '300x250', + } + }; + + const isValid = spec.isBidRequestValid(invalidBid); + + expect(isValid).to.equal(false); + }); + }); + + describe('building requests', function () { + it('should go to correct endpoint', function () { + const request = spec.buildRequests(bidRequests, bidderRequest); + + expect(request.method).to.exist; + expect(request.method).to.equal('POST'); + expect(request.url).to.exist; + expect(request.url).to.equal('https://core.iprom.net/programmatic'); + }); + + it('should add referer info', function () { + const request = spec.buildRequests(bidRequests, bidderRequest); + const requestparse = JSON.parse(request.data); + + expect(requestparse.referer).to.exist; + expect(requestparse.referer.referer).to.equal('https://adserver.si/index.html'); + }); + + it('should add adapter version', function () { + const request = spec.buildRequests(bidRequests, bidderRequest); + const requestparse = JSON.parse(request.data); + + expect(requestparse.version).to.exist; + }); + + it('should contain id and dimension', function () { + const request = spec.buildRequests(bidRequests, bidderRequest); + const requestparse = JSON.parse(request.data); + + expect(requestparse.bids[0].params.id).to.equal('1234'); + expect(requestparse.bids[0].params.dimension).to.equal('300x250'); + }); + }); + + describe('handling responses', function () { + it('should return complete bid response', function () { + const serverResponse = { + body: [{ + requestId: '29a72b151f7bd3', + cpm: 0.5, + width: '300', + height: '250', + creativeId: 1234, + ad: 'Iprom Header bidding example' + } + ]}; + + const request = spec.buildRequests(bidRequests, bidderRequest); + const bids = spec.interpretResponse(serverResponse, request); + + expect(bids).to.be.lengthOf(1); + expect(bids[0].requestId).to.equal('29a72b151f7bd3'); + expect(bids[0].cpm).to.equal(0.5); + expect(bids[0].width).to.equal('300'); + expect(bids[0].height).to.equal('250'); + expect(bids[0].ad).to.have.length.above(1); + }); + + it('should return empty bid response', function () { + const emptyServerResponse = { + body: [] + }; + + const request = spec.buildRequests(bidRequests, bidderRequest); + const bids = spec.interpretResponse(emptyServerResponse, request); + + expect(bids).to.be.lengthOf(0); + }); + }); +}); diff --git a/test/spec/modules/ixBidAdapter_spec.js b/test/spec/modules/ixBidAdapter_spec.js index 7ac4bd94f9d..6d7b3a397bd 100644 --- a/test/spec/modules/ixBidAdapter_spec.js +++ b/test/spec/modules/ixBidAdapter_spec.js @@ -3,6 +3,7 @@ import { config } from 'src/config.js'; import { expect } from 'chai'; import { newBidder } from 'src/adapters/bidderFactory.js'; import { spec } from 'modules/ixBidAdapter.js'; +import { createEidsArray } from 'modules/userId/eids.js'; describe('IndexexchangeAdapter', function () { const IX_SECURE_ENDPOINT = 'https://htlb.casalemedia.com/cygnus'; @@ -126,7 +127,9 @@ describe('IndexexchangeAdapter', function () { 'video/mp4', 'video/webm' ], - minduration: 0 + minduration: 0, + maxduration: 60, + protocols: [2] }, size: [400, 100] }, @@ -146,6 +149,68 @@ describe('IndexexchangeAdapter', function () { } ]; + const DEFAULT_MULTIFORMAT_BANNER_VALID_BID = [ + { + bidder: 'ix', + params: { + siteId: '123', + size: [300, 250] + }, + sizes: [[300, 250], [300, 600]], + mediaTypes: { + video: { + context: 'outstream', + playerSize: [[400, 100]] + }, + banner: { + sizes: [[300, 250], [300, 600]] + } + }, + adUnitCode: 'div-gpt-ad-1460505748562-0', + transactionId: '173f49a8-7549-4218-a23c-e7ba59b47230', + bidId: '1a2b3c4e', + bidderRequestId: '11a22b33c44e', + auctionId: '1aa2bb3cc4de', + schain: SAMPLE_SCHAIN + } + ]; + + const DEFAULT_MULTIFORMAT_VIDEO_VALID_BID = [ + { + bidder: 'ix', + params: { + siteId: '456', + video: { + skippable: false, + mimes: [ + 'video/mp4', + 'video/webm' + ], + minduration: 0, + maxduration: 60, + protocols: [1] + }, + size: [400, 100] + }, + sizes: [[300, 250], [300, 600]], + mediaTypes: { + video: { + context: 'outstream', + playerSize: [[400, 100]] + }, + banner: { + sizes: [[300, 250], [300, 600]] + } + }, + adUnitCode: 'div-gpt-ad-1460505748562-0', + transactionId: '173f49a8-7549-4218-a23c-e7ba59b47230', + bidId: '1a2b3c4e', + bidderRequestId: '11a22b33c44e', + auctionId: '1aa2bb3cc4de', + schain: SAMPLE_SCHAIN + } + ]; + const DEFAULT_BANNER_BID_RESPONSE = { cur: 'USD', id: '11a22b33c44d', @@ -287,8 +352,13 @@ describe('IndexexchangeAdapter', function () { const DEFAULT_USERID_DATA = { idl_env: '1234-5678-9012-3456', // Liveramp + netId: 'testnetid123', // NetId + IDP: 'userIDP000', // IDP + fabrickId: 'fabrickId9000', // FabrickId }; + const DEFAULT_USERIDASEIDS_DATA = createEidsArray(DEFAULT_USERID_DATA); + const DEFAULT_USERID_PAYLOAD = [ { source: 'liveramp.com', @@ -298,6 +368,30 @@ describe('IndexexchangeAdapter', function () { rtiPartner: 'idl' } }] + }, { + source: 'netid.de', + uids: [{ + id: DEFAULT_USERID_DATA.netId, + ext: { + rtiPartner: 'NETID' + } + }] + }, { + source: 'neustar.biz', + uids: [{ + id: DEFAULT_USERID_DATA.fabrickId, + ext: { + rtiPartner: 'fabrickId' + } + }] + }, { + source: 'zeotap.com', + uids: [{ + id: DEFAULT_USERID_DATA.IDP, + ext: { + rtiPartner: 'zeotapIdPlus' + } + }] } ]; @@ -436,6 +530,16 @@ describe('IndexexchangeAdapter', function () { expect(spec.isBidRequestValid(bid)).to.equal(true); }); + it('should return true for banner bid when there are multiple mediaTypes (banner, outstream)', function () { + const bid = utils.deepClone(DEFAULT_MULTIFORMAT_BANNER_VALID_BID[0]); + expect(spec.isBidRequestValid(bid)).to.equal(true); + }); + + it('should return true for video bid when there are multiple mediaTypes (banner, outstream)', function () { + const bid = utils.deepClone(DEFAULT_MULTIFORMAT_VIDEO_VALID_BID[0]); + expect(spec.isBidRequestValid(bid)).to.equal(true); + }); + it('should return false when there is only bidFloor', function () { const bid = utils.deepClone(DEFAULT_BANNER_VALID_BID[0]); bid.params.bidFloor = 50; @@ -461,6 +565,43 @@ describe('IndexexchangeAdapter', function () { bid.params.bidFloorCur = 70; expect(spec.isBidRequestValid(bid)).to.equal(false); }); + + it('should return false when required video properties are missing on both adunit & param levels', function () { + const bid = utils.deepClone(DEFAULT_VIDEO_VALID_BID[0]); + delete bid.params.video.mimes; + expect(spec.isBidRequestValid(bid)).to.equal(false); + }); + + it('should return true when required video properties are at the adunit level', function () { + const bid = utils.deepClone(DEFAULT_VIDEO_VALID_BID[0]); + delete bid.params.video.mimes; + bid.mediaTypes.video.mimes = ['video/mp4']; + expect(spec.isBidRequestValid(bid)).to.equal(true); + }); + + it('should return true if protocols exists but protocol doesn\'t', function () { + const bid = utils.deepClone(DEFAULT_VIDEO_VALID_BID[0]); + expect(spec.isBidRequestValid(bid)).to.equal(true); + delete bid.params.video.protocols; + bid.mediaTypes.video.protocols = 1; + expect(spec.isBidRequestValid(bid)).to.equal(true); + }); + + it('should return true if protocol exists but protocols doesn\'t', function () { + const bid = utils.deepClone(DEFAULT_VIDEO_VALID_BID[0]); + delete bid.params.video.protocols; + bid.params.video.protocol = 1; + expect(spec.isBidRequestValid(bid)).to.equal(true); + delete bid.params.video.protocol; + bid.mediaTypes.video.protocol = 1; + expect(spec.isBidRequestValid(bid)).to.equal(true); + }); + + it('should return false if both protocol/protocols are missing', function () { + const bid = utils.deepClone(DEFAULT_VIDEO_VALID_BID[0]); + delete bid.params.video.protocols; + expect(spec.isBidRequestValid(bid)).to.equal(false); + }); }); describe('buildRequestsIdentity', function () { @@ -650,14 +791,18 @@ describe('IndexexchangeAdapter', function () { delete window.headertag; }); - it('IX adapter reads LiveRamp IDL envelope from Prebid and adds it to Video', function () { + it('IX adapter reads supported user modules from Prebid and adds it to Video', function () { const cloneValidBid = utils.deepClone(DEFAULT_VIDEO_VALID_BID); - cloneValidBid[0].userId = utils.deepClone(DEFAULT_USERID_DATA); + // cloneValidBid[0].userId = utils.deepClone(DEFAULT_USERID_DATA); + cloneValidBid[0].userIdAsEids = utils.deepClone(DEFAULT_USERIDASEIDS_DATA); const request = spec.buildRequests(cloneValidBid, DEFAULT_OPTION)[0]; const payload = JSON.parse(request.data.r); - expect(payload.user.eids).to.have.lengthOf(1); + expect(payload.user.eids).to.have.lengthOf(4); expect(payload.user.eids).to.deep.include(DEFAULT_USERID_PAYLOAD[0]); + expect(payload.user.eids).to.deep.include(DEFAULT_USERID_PAYLOAD[1]); + expect(payload.user.eids).to.deep.include(DEFAULT_USERID_PAYLOAD[2]); + expect(payload.user.eids).to.deep.include(DEFAULT_USERID_PAYLOAD[3]); }); it('We continue to send in IXL identity info and Prebid takes precedence over IXL', function () { @@ -711,11 +856,45 @@ describe('IndexexchangeAdapter', function () { } } ] + }, + NetIdIp: { + source: 'netid.de', + uids: [ + { + id: 'testnetid', + ext: { + rtiPartner: 'NETID' + } + } + ] + }, + NeustarIp: { + source: 'neustar.biz', + uids: [ + { + id: 'testfabrick', + ext: { + rtiPartner: 'fabrickId' + } + } + ] + }, + ZeotapIp: { + source: 'zeotap.com', + uids: [ + { + id: 'testzeotap', + ext: { + rtiPartner: 'zeotapIdPlus' + } + } + ] } }; const cloneValidBid = utils.deepClone(DEFAULT_BANNER_VALID_BID); - cloneValidBid[0].userId = utils.deepClone(DEFAULT_USERID_DATA) + // cloneValidBid[0].userId = utils.deepClone(DEFAULT_USERID_DATA); + cloneValidBid[0].userIdAsEids = utils.deepClone(DEFAULT_USERIDASEIDS_DATA); const request = spec.buildRequests(cloneValidBid, DEFAULT_OPTION)[0]; const payload = JSON.parse(request.data.r); @@ -756,10 +935,14 @@ describe('IndexexchangeAdapter', function () { }) expect(payload.user).to.exist; - expect(payload.user.eids).to.have.lengthOf(3); + expect(payload.user.eids).to.have.lengthOf(6); + expect(payload.user.eids).to.deep.include(validUserIdPayload[0]); expect(payload.user.eids).to.deep.include(validUserIdPayload[1]); expect(payload.user.eids).to.deep.include(validUserIdPayload[2]); + expect(payload.user.eids).to.deep.include(validUserIdPayload[3]); + expect(payload.user.eids).to.deep.include(validUserIdPayload[4]); + expect(payload.user.eids).to.deep.include(validUserIdPayload[5]); }); it('IXL and Prebid are mutually exclusive', function () { @@ -781,7 +964,8 @@ describe('IndexexchangeAdapter', function () { }; const cloneValidBid = utils.deepClone(DEFAULT_VIDEO_VALID_BID); - cloneValidBid[0].userId = utils.deepClone(DEFAULT_USERID_DATA); + // cloneValidBid[0].userId = utils.deepClone(DEFAULT_USERID_DATA); + cloneValidBid[0].userIdAsEids = utils.deepClone(DEFAULT_USERIDASEIDS_DATA); const request = spec.buildRequests(cloneValidBid, DEFAULT_OPTION)[0]; @@ -799,9 +983,12 @@ describe('IndexexchangeAdapter', function () { }); const payload = JSON.parse(request.data.r); - expect(payload.user.eids).to.have.lengthOf(2); + expect(payload.user.eids).to.have.lengthOf(5); expect(payload.user.eids).to.deep.include(validUserIdPayload[0]); expect(payload.user.eids).to.deep.include(validUserIdPayload[1]); + expect(payload.user.eids).to.deep.include(validUserIdPayload[2]); + expect(payload.user.eids).to.deep.include(validUserIdPayload[3]); + expect(payload.user.eids).to.deep.include(validUserIdPayload[4]); }); }); @@ -1292,6 +1479,102 @@ describe('IndexexchangeAdapter', function () { expect(impression.video.placement).to.exist; expect(impression.video.placement).to.equal(4); }); + + it('should not override video properties if they are already configured at the params video level', function () { + const bid = utils.deepClone(DEFAULT_VIDEO_VALID_BID[0]); + bid.mediaTypes.video.context = 'outstream'; + bid.mediaTypes.video.protocols = [1]; + bid.mediaTypes.video.mimes = ['video/override']; + const request = spec.buildRequests([bid])[0]; + const impression = JSON.parse(request.data.r).imp[0]; + + expect(impression.video.protocols[0]).to.equal(2); + expect(impression.video.mimes[0]).to.not.equal('video/override'); + }); + + it('should not add video adunit level properties in imp object if they are not whitelisted', function () { + const bid = utils.deepClone(DEFAULT_VIDEO_VALID_BID[0]); + bid.mediaTypes.video.context = 'outstream'; + bid.mediaTypes.video.random = true; + const request = spec.buildRequests([bid])[0]; + const impression = JSON.parse(request.data.r).imp[0]; + + expect(impression.video.random).to.not.exist; + }); + + it('should add whitelisted adunit level video properties in imp object if they are not configured at params level', function () { + const bid = utils.deepClone(DEFAULT_VIDEO_VALID_BID[0]); + bid.mediaTypes.video.context = 'outstream'; + delete bid.params.video.protocols; + delete bid.params.video.mimes; + bid.mediaTypes.video.protocols = [6]; + bid.mediaTypes.video.mimes = ['video/mp4']; + bid.mediaTypes.video.api = 2; + const request = spec.buildRequests([bid])[0]; + const impression = JSON.parse(request.data.r).imp[0]; + + expect(impression.video.protocols[0]).to.equal(6); + expect(impression.video.api).to.equal(2); + expect(impression.video.mimes[0]).to.equal('video/mp4'); + }); + }); + + describe('buildRequestMultiFormat', function () { + describe('only banner bidder params set', function () { + const request = spec.buildRequests(DEFAULT_MULTIFORMAT_BANNER_VALID_BID) + + const bannerImp = JSON.parse(request[0].data.r).imp[0]; + expect(JSON.parse(request[0].data.r).imp).to.have.lengthOf(2); + expect(JSON.parse(request[0].data.v)).to.equal(BANNER_ENDPOINT_VERSION); + expect(bannerImp.id).to.equal(DEFAULT_MULTIFORMAT_BANNER_VALID_BID[0].bidId); + expect(bannerImp.banner).to.exist; + expect(bannerImp.banner.w).to.equal(DEFAULT_MULTIFORMAT_BANNER_VALID_BID[0].params.size[0]); + expect(bannerImp.banner.h).to.equal(DEFAULT_MULTIFORMAT_BANNER_VALID_BID[0].params.size[1]); + }); + + describe('only video bidder params set', function () { + const request = spec.buildRequests(DEFAULT_MULTIFORMAT_VIDEO_VALID_BID); + + const videoImp = JSON.parse(request[0].data.r).imp[0]; + expect(JSON.parse(request[0].data.r).imp).to.have.lengthOf(1); + expect(JSON.parse(request[0].data.v)).to.equal(VIDEO_ENDPOINT_VERSION); + expect(videoImp.id).to.equal(DEFAULT_MULTIFORMAT_VIDEO_VALID_BID[0].bidId); + expect(videoImp.video).to.exist; + expect(videoImp.video.w).to.equal(DEFAULT_MULTIFORMAT_VIDEO_VALID_BID[0].params.size[0]); + expect(videoImp.video.h).to.equal(DEFAULT_MULTIFORMAT_VIDEO_VALID_BID[0].params.size[1]); + }); + describe('both banner and video bidder params set', function () { + const request = spec.buildRequests([DEFAULT_MULTIFORMAT_BANNER_VALID_BID[0], DEFAULT_MULTIFORMAT_VIDEO_VALID_BID[0]]); + + it('should return valid banner and video requests', function () { + const bannerImp = JSON.parse(request[0].data.r).imp[0]; + expect(JSON.parse(request[0].data.r).imp).to.have.lengthOf(2); + expect(JSON.parse(request[0].data.v)).to.equal(BANNER_ENDPOINT_VERSION); + expect(bannerImp.id).to.equal(DEFAULT_MULTIFORMAT_BANNER_VALID_BID[0].bidId); + expect(bannerImp.banner).to.exist; + expect(bannerImp.banner.w).to.equal(DEFAULT_MULTIFORMAT_BANNER_VALID_BID[0].params.size[0]); + expect(bannerImp.banner.h).to.equal(DEFAULT_MULTIFORMAT_BANNER_VALID_BID[0].params.size[1]); + + const videoImp = JSON.parse(request[1].data.r).imp[0]; + expect(JSON.parse(request[1].data.r).imp).to.have.lengthOf(1); + expect(JSON.parse(request[1].data.v)).to.equal(VIDEO_ENDPOINT_VERSION); + expect(videoImp.id).to.equal(DEFAULT_MULTIFORMAT_VIDEO_VALID_BID[0].bidId); + expect(videoImp.video).to.exist; + expect(videoImp.video.w).to.equal(DEFAULT_MULTIFORMAT_VIDEO_VALID_BID[0].params.size[0]); + expect(videoImp.video.h).to.equal(DEFAULT_MULTIFORMAT_VIDEO_VALID_BID[0].params.size[1]); + }); + + it('should contain all correct IXdiag properties', function () { + const diagObj = JSON.parse(request[0].data.r).ext.ixdiag; + expect(diagObj.iu).to.equal(0); + expect(diagObj.nu).to.equal(0); + expect(diagObj.ou).to.equal(1); + expect(diagObj.ren).to.equal(false); + expect(diagObj.mfu).to.equal(1); + expect(diagObj.allU).to.equal(1); + expect(diagObj.version).to.equal('$prebid.version$'); + }); + }); }); describe('interpretResponse', function () { @@ -1566,5 +1849,32 @@ describe('IndexexchangeAdapter', function () { expect(requestWithConsent.regs.ext.gdpr).to.equal(1); expect(requestWithConsent.regs.ext.us_privacy).to.equal('1YYN'); }); + + it('should contain `consented_providers_settings.consented_providers` & consent on user.ext when both are provided', function () { + const options = { + gdprConsent: { + consentString: '3huaa11=qu3198ae', + addtlConsent: '1~1.35.41.101', + } + }; + + const validBidWithConsent = spec.buildRequests(DEFAULT_BANNER_VALID_BID, options); + const requestWithConsent = JSON.parse(validBidWithConsent[0].data.r); + expect(requestWithConsent.user.ext.consented_providers_settings.consented_providers).to.equal('1~1.35.41.101'); + expect(requestWithConsent.user.ext.consent).to.equal('3huaa11=qu3198ae'); + }); + + it('should not contain `consented_providers_settings.consented_providers` on user.ext when consent is not provided', function () { + const options = { + gdprConsent: { + addtlConsent: '1~1.35.41.101', + } + }; + + const validBidWithConsent = spec.buildRequests(DEFAULT_BANNER_VALID_BID, options); + const requestWithConsent = JSON.parse(validBidWithConsent[0].data.r); + expect(utils.deepAccess(requestWithConsent, 'user.ext.consented_providers_settings')).to.not.exist; + expect(utils.deepAccess(requestWithConsent, 'user.ext.consent')).to.not.exist; + }); }); }); diff --git a/test/spec/modules/jwplayerRtdProvider_spec.js b/test/spec/modules/jwplayerRtdProvider_spec.js index 48b432b6bb4..f7beb6ba486 100644 --- a/test/spec/modules/jwplayerRtdProvider_spec.js +++ b/test/spec/modules/jwplayerRtdProvider_spec.js @@ -413,19 +413,43 @@ describe('jwplayerRtdProvider', function() { }); describe(' Extract Publisher Params', function () { - it('should default to config', function () { - const config = { mediaID: 'test' }; + const config = { mediaID: 'test' }; - const adUnit1 = { fpd: { context: {} } }; - const targeting1 = extractPublisherParams(adUnit1, config); - expect(targeting1).to.deep.equal(config); + it('should exclude adUnits that do not support instream video and do not specify jwTargeting', function () { + const oustreamAdUnit = { mediaTypes: { video: { context: 'outstream' } } }; + const oustreamTargeting = extractPublisherParams(oustreamAdUnit, config); + expect(oustreamTargeting).to.be.undefined; - const adUnit2 = { fpd: { context: { data: { jwTargeting: {} } } } }; - const targeting2 = extractPublisherParams(adUnit2, config); - expect(targeting2).to.deep.equal(config); + const bannerAdUnit = { mediaTypes: { banner: {} } }; + const bannerTargeting = extractPublisherParams(bannerAdUnit, config); + expect(bannerTargeting).to.be.undefined; - const targeting3 = extractPublisherParams(null, config); - expect(targeting3).to.deep.equal(config); + const targeting = extractPublisherParams({}, config); + expect(targeting).to.be.undefined; + }); + + it('should include ad unit when media type is video and is instream', function () { + const adUnit = { mediaTypes: { video: { context: 'instream' } } }; + const targeting = extractPublisherParams(adUnit, config); + expect(targeting).to.deep.equal(config); + }); + + it('should include banner ad units that specify jwTargeting', function() { + const adUnit = { mediaTypes: { banner: {} }, fpd: { context: { data: { jwTargeting: {} } } } }; + const targeting = extractPublisherParams(adUnit, config); + expect(targeting).to.deep.equal(config); + }); + + it('should include outstream ad units that specify jwTargeting', function() { + const adUnit = { mediaTypes: { video: { context: 'outstream' } }, fpd: { context: { data: { jwTargeting: {} } } } }; + const targeting = extractPublisherParams(adUnit, config); + expect(targeting).to.deep.equal(config); + }); + + it('should fallback to config when empty jwTargeting is defined in ad unit', function () { + const adUnit = { fpd: { context: { data: { jwTargeting: {} } } } }; + const targeting = extractPublisherParams(adUnit, config); + expect(targeting).to.deep.equal(config); }); it('should prioritize adUnit properties ', function () { @@ -450,9 +474,9 @@ describe('jwplayerRtdProvider', function() { expect(targeting).to.have.property('playerID', expectedPlayerID); }); - it('should return empty object when Publisher Params are absent', function () { - const targeting = extractPublisherParams(null, null); - expect(targeting).to.deep.equal({}); + it('should return undefined when Publisher Params are absent', function () { + const targeting = extractPublisherParams({}, null); + expect(targeting).to.be.undefined; }) }); diff --git a/test/spec/modules/kubientBidAdapter_spec.js b/test/spec/modules/kubientBidAdapter_spec.js new file mode 100644 index 00000000000..1df4370b2ba --- /dev/null +++ b/test/spec/modules/kubientBidAdapter_spec.js @@ -0,0 +1,259 @@ +import { expect, assert } from 'chai'; +import { spec } from 'modules/kubientBidAdapter.js'; + +describe('KubientAdapter', function () { + let bid = { + bidId: '2dd581a2b6281d', + bidder: 'kubient', + bidderRequestId: '145e1d6a7837c9', + params: { + zoneid: '5678', + floor: 0.05, + }, + auctionId: '74f78609-a92d-4cf1-869f-1b244bbfb5d2', + mediaTypes: { + banner: { + sizes: [[300, 250]] + } + }, + transactionId: '3bb2f6da-87a6-4029-aeb0-bfe951372e62', + schain: { + ver: '1.0', + complete: 1, + nodes: [ + { + asi: 'example.com', + sid: '0', + hp: 1, + rid: 'bidrequestid', + domain: 'example.com' + } + ] + } + }; + let consentString = 'BOJ8RZsOJ8RZsABAB8AAAAAZ+A=='; + let uspConsentData = '1YCC'; + let bidderRequest = { + bidderCode: 'kubient', + auctionId: 'fffffff-ffff-ffff-ffff-ffffffffffff', + bidderRequestId: 'ffffffffffffff', + start: 1472239426002, + auctionStart: 1472239426000, + timeout: 5000, + refererInfo: { + referer: 'http://www.example.com', + reachedTop: true, + }, + gdprConsent: { + consentString: consentString, + gdprApplies: true + }, + uspConsent: uspConsentData, + bids: [bid] + }; + describe('buildRequests', function () { + let serverRequests = spec.buildRequests([bid], bidderRequest); + it('Creates a ServerRequest object with method, URL and data', function () { + expect(serverRequests).to.be.an('array'); + }); + for (let i = 0; i < serverRequests.length; i++) { + let serverRequest = serverRequests[i]; + it('Creates a ServerRequest object with method, URL and data', function () { + expect(serverRequest.method).to.be.a('string'); + expect(serverRequest.url).to.be.a('string'); + expect(serverRequest.data).to.be.a('string'); + }); + it('Returns POST method', function () { + expect(serverRequest.method).to.equal('POST'); + }); + it('Returns valid URL', function () { + expect(serverRequest.url).to.equal('https://kssp.kbntx.ch/pbjs'); + }); + it('Returns valid data if array of bids is valid', function () { + let data = JSON.parse(serverRequest.data); + expect(data).to.be.an('object'); + expect(data).to.have.all.keys('v', 'requestId', 'adSlots', 'gdpr', 'referer', 'tmax', 'consent', 'consentGiven', 'uspConsent'); + expect(data.v).to.exist.and.to.be.a('string'); + expect(data.requestId).to.exist.and.to.be.a('string'); + expect(data.referer).to.be.a('string'); + expect(data.tmax).to.exist.and.to.be.a('number'); + expect(data.gdpr).to.exist.and.to.be.within(0, 1); + expect(data.consent).to.equal(consentString); + expect(data.uspConsent).to.exist.and.to.equal(uspConsentData); + for (let j = 0; j < data['adSlots'].length; j++) { + let adSlot = data['adSlots'][i]; + expect(adSlot).to.have.all.keys('bidId', 'zoneId', 'floor', 'sizes', 'schain', 'mediaTypes'); + expect(adSlot.bidId).to.be.a('string'); + expect(adSlot.zoneId).to.be.a('string'); + expect(adSlot.floor).to.be.a('number'); + expect(adSlot.sizes).to.be.an('array'); + expect(adSlot.schain).to.be.an('object'); + expect(adSlot.mediaTypes).to.be.an('object'); + } + }); + } + }); + + describe('isBidRequestValid', function () { + it('Should return true when required params are found', function () { + expect(spec.isBidRequestValid(bid)).to.be.true; + }); + it('Should return false when required params are not found', function () { + expect(spec.isBidRequestValid(bid)).to.be.true; + }); + it('Should return false when params are not found', function () { + delete bid.params; + expect(spec.isBidRequestValid(bid)).to.be.false; + }); + }); + + describe('interpretResponse', function () { + it('Should interpret response', function () { + const serverResponse = { + body: + { + seatbid: [ + { + bid: [ + { + bidId: '000', + price: 1.5, + adm: '
test
', + creativeId: 'creativeId', + w: 300, + h: 250, + cur: 'USD', + netRevenue: false, + ttl: 360 + } + ] + } + ] + } + }; + let bannerResponses = spec.interpretResponse(serverResponse); + expect(bannerResponses).to.be.an('array').that.is.not.empty; + let dataItem = bannerResponses[0]; + expect(dataItem).to.have.all.keys('requestId', 'cpm', 'ad', 'creativeId', 'width', 'height', 'currency', 'netRevenue', 'ttl'); + expect(dataItem.requestId).to.exist.and.to.be.a('string').and.to.equal(serverResponse.body.seatbid[0].bid[0].bidId); + expect(dataItem.cpm).to.exist.and.to.be.a('number').and.to.equal(serverResponse.body.seatbid[0].bid[0].price); + expect(dataItem.ad).to.exist.and.to.be.a('string').and.to.have.string(serverResponse.body.seatbid[0].bid[0].adm); + expect(dataItem.creativeId).to.exist.and.to.be.a('string').and.to.equal(serverResponse.body.seatbid[0].bid[0].creativeId); + expect(dataItem.width).to.exist.and.to.be.a('number').and.to.equal(serverResponse.body.seatbid[0].bid[0].w); + expect(dataItem.height).to.exist.and.to.be.a('number').and.to.equal(serverResponse.body.seatbid[0].bid[0].h); + expect(dataItem.currency).to.exist.and.to.be.a('string').and.to.equal(serverResponse.body.seatbid[0].bid[0].cur); + expect(dataItem.netRevenue).to.exist.and.to.be.a('boolean').and.to.equal(serverResponse.body.seatbid[0].bid[0].netRevenue); + expect(dataItem.ttl).to.exist.and.to.be.a('number').and.to.equal(serverResponse.body.seatbid[0].bid[0].ttl); + }); + + it('Should return no ad when not given a server response', function () { + const ads = spec.interpretResponse(null); + expect(ads).to.be.an('array').and.to.have.length(0); + }); + }); + + describe('getUserSyncs', function () { + it('should register the sync iframe without gdpr', function () { + let syncOptions = { + iframeEnabled: true + }; + let serverResponses = null; + let gdprConsent = { + consentString: consentString + }; + let uspConsent = null; + let syncs = spec.getUserSyncs(syncOptions, serverResponses, gdprConsent, uspConsent); + expect(syncs).to.be.an('array').and.to.have.length(1); + expect(syncs[0].type).to.equal('iframe'); + expect(syncs[0].url).to.equal('https://kdmp.kbntx.ch/init.html?consent_str=' + consentString + '&consent_given=0'); + }); + it('should register the sync iframe with gdpr', function () { + let syncOptions = { + iframeEnabled: true + }; + let serverResponses = null; + let gdprConsent = { + gdprApplies: true, + consentString: consentString + }; + let uspConsent = null; + let syncs = spec.getUserSyncs(syncOptions, serverResponses, gdprConsent, uspConsent); + expect(syncs).to.be.an('array').and.to.have.length(1); + expect(syncs[0].type).to.equal('iframe'); + expect(syncs[0].url).to.equal('https://kdmp.kbntx.ch/init.html?consent_str=' + consentString + '&gdpr=1&consent_given=0'); + }); + it('should register the sync iframe with gdpr vendor', function () { + let syncOptions = { + iframeEnabled: true + }; + let serverResponses = null; + let gdprConsent = { + gdprApplies: true, + consentString: consentString, + apiVersion: 1, + vendorData: { + vendorConsents: { + 794: 1 + } + } + }; + let uspConsent = null; + let syncs = spec.getUserSyncs(syncOptions, serverResponses, gdprConsent, uspConsent); + expect(syncs).to.be.an('array').and.to.have.length(1); + expect(syncs[0].type).to.equal('iframe'); + expect(syncs[0].url).to.equal('https://kdmp.kbntx.ch/init.html?consent_str=' + consentString + '&gdpr=1&consent_given=1'); + }); + it('should register the sync image without gdpr', function () { + let syncOptions = { + pixelEnabled: true + }; + let serverResponses = null; + let gdprConsent = { + consentString: consentString + }; + let uspConsent = null; + let syncs = spec.getUserSyncs(syncOptions, serverResponses, gdprConsent, uspConsent); + expect(syncs).to.be.an('array').and.to.have.length(1); + expect(syncs[0].type).to.equal('image'); + expect(syncs[0].url).to.equal('https://kdmp.kbntx.ch/init.png?consent_str=' + consentString + '&consent_given=0'); + }); + it('should register the sync image with gdpr', function () { + let syncOptions = { + pixelEnabled: true + }; + let serverResponses = null; + let gdprConsent = { + gdprApplies: true, + consentString: consentString + }; + let uspConsent = null; + let syncs = spec.getUserSyncs(syncOptions, serverResponses, gdprConsent, uspConsent); + expect(syncs).to.be.an('array').and.to.have.length(1); + expect(syncs[0].type).to.equal('image'); + expect(syncs[0].url).to.equal('https://kdmp.kbntx.ch/init.png?consent_str=' + consentString + '&gdpr=1&consent_given=0'); + }); + it('should register the sync image with gdpr vendor', function () { + let syncOptions = { + pixelEnabled: true + }; + let serverResponses = null; + let gdprConsent = { + gdprApplies: true, + consentString: consentString, + apiVersion: 2, + vendorData: { + vendor: { + consents: { + 794: 1 + } + } + } + }; + let uspConsent = null; + let syncs = spec.getUserSyncs(syncOptions, serverResponses, gdprConsent, uspConsent); + expect(syncs).to.be.an('array').and.to.have.length(1); + expect(syncs[0].type).to.equal('image'); + expect(syncs[0].url).to.equal('https://kdmp.kbntx.ch/init.png?consent_str=' + consentString + '&gdpr=1&consent_given=1'); + }); + }) +}); diff --git a/test/spec/modules/lemmaBidAdapter_spec.js b/test/spec/modules/lemmaBidAdapter_spec.js index a00c25d126c..a22f5650e50 100644 --- a/test/spec/modules/lemmaBidAdapter_spec.js +++ b/test/spec/modules/lemmaBidAdapter_spec.js @@ -346,24 +346,6 @@ describe('lemmaBidAdapter', function() { type: 'iframe', url: syncurl_iframe }]); }); - - it('CCPA/USP', function() { - expect(spec.getUserSyncs({ iframeEnabled: true }, {}, undefined, '1NYN')).to.deep.equal([{ - type: 'iframe', url: `${syncurl_iframe}&us_privacy=1NYN` - }]); - }); - - it('GDPR', function() { - expect(spec.getUserSyncs({ iframeEnabled: true }, {}, { gdprApplies: true, consentString: 'foo' }, undefined)).to.deep.equal([{ - type: 'iframe', url: `${syncurl_iframe}&gdpr=1&gdpr_consent=foo` - }]); - expect(spec.getUserSyncs({ iframeEnabled: true }, {}, { gdprApplies: false, consentString: 'foo' }, undefined)).to.deep.equal([{ - type: 'iframe', url: `${syncurl_iframe}&gdpr=0&gdpr_consent=foo` - }]); - expect(spec.getUserSyncs({ iframeEnabled: true }, {}, { gdprApplies: true, consentString: undefined }, undefined)).to.deep.equal([{ - type: 'iframe', url: `${syncurl_iframe}&gdpr=1&gdpr_consent=` - }]); - }); }); }); }); diff --git a/test/spec/modules/liveIntentIdMinimalSystem_spec.js b/test/spec/modules/liveIntentIdMinimalSystem_spec.js new file mode 100644 index 00000000000..b0f97ae0300 --- /dev/null +++ b/test/spec/modules/liveIntentIdMinimalSystem_spec.js @@ -0,0 +1,188 @@ +import * as utils from 'src/utils.js'; +import { gdprDataHandler, uspDataHandler } from '../../../src/adapterManager.js'; +import { server } from 'test/mocks/xhr.js'; +import { liveIntentIdSubmodule, reset as resetLiveIntentIdSubmodule, storage } from 'modules/liveIntentIdSystem.js'; + +const PUBLISHER_ID = '89899'; +const defaultConfigParams = { params: {publisherId: PUBLISHER_ID} }; +const responseHeader = {'Content-Type': 'application/json'}; + +describe('LiveIntentMinimalId', function() { + let logErrorStub; + let uspConsentDataStub; + let gdprConsentDataStub; + let getCookieStub; + let getDataFromLocalStorageStub; + let imgStub; + + beforeEach(function() { + liveIntentIdSubmodule.setModuleMode('minimal'); + imgStub = sinon.stub(utils, 'triggerPixel'); + getCookieStub = sinon.stub(storage, 'getCookie'); + getDataFromLocalStorageStub = sinon.stub(storage, 'getDataFromLocalStorage'); + logErrorStub = sinon.stub(utils, 'logError'); + uspConsentDataStub = sinon.stub(uspDataHandler, 'getConsentData'); + gdprConsentDataStub = sinon.stub(gdprDataHandler, 'getConsentData'); + }); + + afterEach(function() { + imgStub.restore(); + getCookieStub.restore(); + getDataFromLocalStorageStub.restore(); + logErrorStub.restore(); + uspConsentDataStub.restore(); + gdprConsentDataStub.restore(); + liveIntentIdSubmodule.setModuleMode('minimal'); + resetLiveIntentIdSubmodule(); + }); + it('should not fire an event when getId', function() { + uspConsentDataStub.returns('1YNY'); + gdprConsentDataStub.returns({ + gdprApplies: true, + consentString: 'consentDataString' + }) + liveIntentIdSubmodule.getId(defaultConfigParams); + expect(server.requests[0]).to.eql(undefined) + }); + + it('should not return a decoded identifier when the unifiedId is not present in the value', function() { + const result = liveIntentIdSubmodule.decode({ additionalData: 'data' }); + expect(result).to.be.undefined; + }); + + it('should initialize LiveConnect and send no data', function() { + liveIntentIdSubmodule.getId(defaultConfigParams); + liveIntentIdSubmodule.decode({}, defaultConfigParams); + liveIntentIdSubmodule.getId(defaultConfigParams); + liveIntentIdSubmodule.decode({}, defaultConfigParams); + expect(server.requests.length).to.be.eq(0); + }); + + it('should call the Custom URL of the LiveIntent Identity Exchange endpoint', function() { + getCookieStub.returns(null); + let callBackSpy = sinon.spy(); + let submoduleCallback = liveIntentIdSubmodule.getId({ params: {...defaultConfigParams.params, ...{'url': 'https://dummy.liveintent.com/idex'}} }).callback; + submoduleCallback(callBackSpy); + let request = server.requests[0]; + expect(request.url).to.be.eq('https://dummy.liveintent.com/idex/prebid/89899'); + request.respond( + 200, + responseHeader, + JSON.stringify({}) + ); + expect(callBackSpy.calledOnce).to.be.true; + }); + + it('should call the default url of the LiveIntent Identity Exchange endpoint, with a partner', function() { + getCookieStub.returns(null); + let callBackSpy = sinon.spy(); + let submoduleCallback = liveIntentIdSubmodule.getId({ params: { + ...defaultConfigParams.params, + ...{ + 'url': 'https://dummy.liveintent.com/idex', + 'partner': 'rubicon' + } + } }).callback; + submoduleCallback(callBackSpy); + let request = server.requests[0]; + expect(request.url).to.be.eq('https://dummy.liveintent.com/idex/rubicon/89899'); + request.respond( + 200, + responseHeader, + JSON.stringify({}) + ); + expect(callBackSpy.calledOnce).to.be.true; + }); + + it('should call the LiveIntent Identity Exchange endpoint, with no additional query params', function() { + getCookieStub.returns(null); + let callBackSpy = sinon.spy(); + let submoduleCallback = liveIntentIdSubmodule.getId(defaultConfigParams).callback; + submoduleCallback(callBackSpy); + let request = server.requests[0]; + expect(request.url).to.be.eq('https://idx.liadm.com/idex/prebid/89899'); + request.respond( + 200, + responseHeader, + JSON.stringify({}) + ); + expect(callBackSpy.calledOnce).to.be.true; + }); + + it('should log an error and continue to callback if ajax request errors', function() { + getCookieStub.returns(null); + let callBackSpy = sinon.spy(); + let submoduleCallback = liveIntentIdSubmodule.getId(defaultConfigParams).callback; + submoduleCallback(callBackSpy); + let request = server.requests[0]; + expect(request.url).to.be.eq('https://idx.liadm.com/idex/prebid/89899'); + request.respond( + 503, + responseHeader, + 'Unavailable' + ); + expect(logErrorStub.calledOnce).to.be.true; + expect(callBackSpy.calledOnce).to.be.true; + }); + + it('should include the LiveConnect identifier when calling the LiveIntent Identity Exchange endpoint', function() { + const oldCookie = 'a-xxxx--123e4567-e89b-12d3-a456-426655440000' + getDataFromLocalStorageStub.withArgs('_li_duid').returns(oldCookie); + let callBackSpy = sinon.spy(); + let submoduleCallback = liveIntentIdSubmodule.getId(defaultConfigParams).callback; + submoduleCallback(callBackSpy); + let request = server.requests[0]; + expect(request.url).to.be.eq(`https://idx.liadm.com/idex/prebid/89899?duid=${oldCookie}`); + request.respond( + 200, + responseHeader, + JSON.stringify({}) + ); + expect(callBackSpy.calledOnce).to.be.true; + }); + + it('should include the LiveConnect identifier and additional Identifiers to resolve', function() { + const oldCookie = 'a-xxxx--123e4567-e89b-12d3-a456-426655440000' + getDataFromLocalStorageStub.withArgs('_li_duid').returns(oldCookie); + getDataFromLocalStorageStub.withArgs('_thirdPC').returns('third-pc'); + const configParams = { params: { + ...defaultConfigParams.params, + ...{ + 'identifiersToResolve': ['_thirdPC'] + } + }}; + let callBackSpy = sinon.spy(); + let submoduleCallback = liveIntentIdSubmodule.getId(configParams).callback; + submoduleCallback(callBackSpy); + let request = server.requests[0]; + expect(request.url).to.be.eq(`https://idx.liadm.com/idex/prebid/89899?duid=${oldCookie}&_thirdPC=third-pc`); + request.respond( + 200, + responseHeader, + JSON.stringify({}) + ); + expect(callBackSpy.calledOnce).to.be.true; + }); + + it('should include an additional identifier value to resolve even if it is an object', function() { + getCookieStub.returns(null); + getDataFromLocalStorageStub.withArgs('_thirdPC').returns({'key': 'value'}); + const configParams = { params: { + ...defaultConfigParams.params, + ...{ + 'identifiersToResolve': ['_thirdPC'] + } + }}; + let callBackSpy = sinon.spy(); + let submoduleCallback = liveIntentIdSubmodule.getId(configParams).callback; + submoduleCallback(callBackSpy); + let request = server.requests[0]; + expect(request.url).to.be.eq('https://idx.liadm.com/idex/prebid/89899?_thirdPC=%7B%22key%22%3A%22value%22%7D'); + request.respond( + 200, + responseHeader, + JSON.stringify({}) + ); + expect(callBackSpy.calledOnce).to.be.true; + }); +}); diff --git a/test/spec/modules/liveIntentIdSystem_spec.js b/test/spec/modules/liveIntentIdSystem_spec.js index aae60cbcd19..3b4e4b9d9a7 100644 --- a/test/spec/modules/liveIntentIdSystem_spec.js +++ b/test/spec/modules/liveIntentIdSystem_spec.js @@ -2,7 +2,8 @@ import { liveIntentIdSubmodule, reset as resetLiveIntentIdSubmodule, storage } f import * as utils from 'src/utils.js'; import { gdprDataHandler, uspDataHandler } from '../../../src/adapterManager.js'; import { server } from 'test/mocks/xhr.js'; - +resetLiveIntentIdSubmodule(); +liveIntentIdSubmodule.setModuleMode('standard') const PUBLISHER_ID = '89899'; const defaultConfigParams = { params: {publisherId: PUBLISHER_ID} }; const responseHeader = {'Content-Type': 'application/json'} @@ -16,6 +17,7 @@ describe('LiveIntentId', function() { let imgStub; beforeEach(function() { + liveIntentIdSubmodule.setModuleMode('standard'); imgStub = sinon.stub(utils, 'triggerPixel'); getCookieStub = sinon.stub(storage, 'getCookie'); getDataFromLocalStorageStub = sinon.stub(storage, 'getDataFromLocalStorage'); diff --git a/test/spec/modules/loganBidAdapter_spec.js b/test/spec/modules/loganBidAdapter_spec.js new file mode 100644 index 00000000000..96029294df0 --- /dev/null +++ b/test/spec/modules/loganBidAdapter_spec.js @@ -0,0 +1,329 @@ +import {expect} from 'chai'; +import {spec} from '../../../modules/loganBidAdapter.js'; +import { BANNER, VIDEO, NATIVE } from '../../../src/mediaTypes.js'; + +describe('LoganBidAdapter', function () { + const bid = { + bidId: '23fhj33i987f', + bidder: 'logan', + mediaTypes: { + [BANNER]: { + sizes: [[300, 250]] + } + }, + params: { + placementId: 783, + traffic: BANNER + } + }; + + const bidderRequest = { + refererInfo: { + referer: 'test.com' + } + }; + + describe('isBidRequestValid', function () { + it('Should return true if there are bidId, params and key parameters present', function () { + expect(spec.isBidRequestValid(bid)).to.be.true; + }); + it('Should return false if at least one of parameters is not present', function () { + delete bid.params.placementId; + expect(spec.isBidRequestValid(bid)).to.be.false; + }); + }); + + describe('buildRequests', function () { + let serverRequest = spec.buildRequests([bid], bidderRequest); + it('Creates a ServerRequest object with method, URL and data', function () { + expect(serverRequest).to.exist; + expect(serverRequest.method).to.exist; + expect(serverRequest.url).to.exist; + expect(serverRequest.data).to.exist; + }); + it('Returns POST method', function () { + expect(serverRequest.method).to.equal('POST'); + }); + it('Returns valid URL', function () { + expect(serverRequest.url).to.equal('https://USeast2.logan.ai/?c=o&m=multi'); + }); + it('Returns valid data if array of bids is valid', function () { + let data = serverRequest.data; + expect(data).to.be.an('object'); + expect(data).to.have.all.keys('deviceWidth', 'deviceHeight', 'language', 'secure', 'host', 'page', 'placements'); + expect(data.deviceWidth).to.be.a('number'); + expect(data.deviceHeight).to.be.a('number'); + expect(data.language).to.be.a('string'); + expect(data.secure).to.be.within(0, 1); + expect(data.host).to.be.a('string'); + expect(data.page).to.be.a('string'); + expect(data.gdpr).to.not.exist; + expect(data.ccpa).to.not.exist; + let placement = data['placements'][0]; + expect(placement).to.have.keys('placementId', 'bidId', 'traffic', 'sizes', 'schain'); + expect(placement.placementId).to.equal(783); + expect(placement.bidId).to.equal('23fhj33i987f'); + expect(placement.traffic).to.equal(BANNER); + expect(placement.schain).to.be.an('object'); + expect(placement.sizes).to.be.an('array'); + }); + + it('Returns valid data for mediatype video', function () { + const playerSize = [300, 300]; + bid.mediaTypes = {}; + bid.params.traffic = VIDEO; + bid.mediaTypes[VIDEO] = { + playerSize + }; + serverRequest = spec.buildRequests([bid], bidderRequest); + let data = serverRequest.data; + expect(data).to.be.an('object'); + let placement = data['placements'][0]; + expect(placement).to.be.an('object'); + expect(placement).to.have.keys('placementId', 'bidId', 'traffic', 'wPlayer', 'hPlayer', 'schain'); + expect(placement.traffic).to.equal(VIDEO); + expect(placement.wPlayer).to.equal(playerSize[0]); + expect(placement.hPlayer).to.equal(playerSize[1]); + }); + + it('Returns valid data for mediatype native', function () { + const native = { + title: { + required: true + }, + body: { + required: true + }, + icon: { + required: true, + size: [64, 64] + } + }; + + bid.mediaTypes = {}; + bid.params.traffic = NATIVE; + bid.mediaTypes[NATIVE] = native; + serverRequest = spec.buildRequests([bid], bidderRequest); + let data = serverRequest.data; + expect(data).to.be.an('object'); + let placement = data['placements'][0]; + expect(placement).to.be.an('object'); + expect(placement).to.have.keys('placementId', 'bidId', 'traffic', 'native', 'schain'); + expect(placement.traffic).to.equal(NATIVE); + expect(placement.native).to.equal(native); + }); + + it('Returns data with gdprConsent and without uspConsent', function () { + bidderRequest.gdprConsent = 'test'; + serverRequest = spec.buildRequests([bid], bidderRequest); + let data = serverRequest.data; + expect(data.gdpr).to.exist; + expect(data.gdpr).to.be.a('string'); + expect(data.gdpr).to.equal(bidderRequest.gdprConsent); + expect(data.ccpa).to.not.exist; + delete bidderRequest.gdprConsent; + }); + + it('Returns data with uspConsent and without gdprConsent', function () { + bidderRequest.uspConsent = 'test'; + serverRequest = spec.buildRequests([bid], bidderRequest); + let data = serverRequest.data; + expect(data.ccpa).to.exist; + expect(data.ccpa).to.be.a('string'); + expect(data.ccpa).to.equal(bidderRequest.uspConsent); + expect(data.gdpr).to.not.exist; + }); + + it('Returns empty data if no valid requests are passed', function () { + serverRequest = spec.buildRequests([]); + let data = serverRequest.data; + expect(data.placements).to.be.an('array').that.is.empty; + }); + }); + describe('interpretResponse', function () { + it('Should interpret banner response', function () { + const banner = { + body: [{ + mediaType: 'banner', + width: 300, + height: 250, + cpm: 0.4, + ad: 'Test', + requestId: '23fhj33i987f', + ttl: 120, + creativeId: '2', + netRevenue: true, + currency: 'USD', + dealId: '1' + }] + }; + let bannerResponses = spec.interpretResponse(banner); + expect(bannerResponses).to.be.an('array').that.is.not.empty; + let dataItem = bannerResponses[0]; + expect(dataItem).to.have.all.keys('requestId', 'cpm', 'width', 'height', 'ad', 'ttl', 'creativeId', + 'netRevenue', 'currency', 'dealId', 'mediaType'); + expect(dataItem.requestId).to.equal('23fhj33i987f'); + expect(dataItem.cpm).to.equal(0.4); + expect(dataItem.width).to.equal(300); + expect(dataItem.height).to.equal(250); + expect(dataItem.ad).to.equal('Test'); + expect(dataItem.ttl).to.equal(120); + expect(dataItem.creativeId).to.equal('2'); + expect(dataItem.netRevenue).to.be.true; + expect(dataItem.currency).to.equal('USD'); + }); + it('Should interpret video response', function () { + const video = { + body: [{ + vastUrl: 'test.com', + mediaType: 'video', + cpm: 0.5, + requestId: '23fhj33i987f', + ttl: 120, + creativeId: '2', + netRevenue: true, + currency: 'USD', + dealId: '1' + }] + }; + let videoResponses = spec.interpretResponse(video); + expect(videoResponses).to.be.an('array').that.is.not.empty; + + let dataItem = videoResponses[0]; + expect(dataItem).to.have.all.keys('requestId', 'cpm', 'vastUrl', 'ttl', 'creativeId', + 'netRevenue', 'currency', 'dealId', 'mediaType'); + expect(dataItem.requestId).to.equal('23fhj33i987f'); + expect(dataItem.cpm).to.equal(0.5); + expect(dataItem.vastUrl).to.equal('test.com'); + expect(dataItem.ttl).to.equal(120); + expect(dataItem.creativeId).to.equal('2'); + expect(dataItem.netRevenue).to.be.true; + expect(dataItem.currency).to.equal('USD'); + }); + it('Should interpret native response', function () { + const native = { + body: [{ + mediaType: 'native', + native: { + clickUrl: 'test.com', + title: 'Test', + image: 'test.com', + impressionTrackers: ['test.com'], + }, + ttl: 120, + cpm: 0.4, + requestId: '23fhj33i987f', + creativeId: '2', + netRevenue: true, + currency: 'USD', + }] + }; + let nativeResponses = spec.interpretResponse(native); + expect(nativeResponses).to.be.an('array').that.is.not.empty; + + let dataItem = nativeResponses[0]; + expect(dataItem).to.have.keys('requestId', 'cpm', 'ttl', 'creativeId', 'netRevenue', 'currency', 'mediaType', 'native'); + expect(dataItem.native).to.have.keys('clickUrl', 'impressionTrackers', 'title', 'image') + expect(dataItem.requestId).to.equal('23fhj33i987f'); + expect(dataItem.cpm).to.equal(0.4); + expect(dataItem.native.clickUrl).to.equal('test.com'); + expect(dataItem.native.title).to.equal('Test'); + expect(dataItem.native.image).to.equal('test.com'); + expect(dataItem.native.impressionTrackers).to.be.an('array').that.is.not.empty; + expect(dataItem.native.impressionTrackers[0]).to.equal('test.com'); + expect(dataItem.ttl).to.equal(120); + expect(dataItem.creativeId).to.equal('2'); + expect(dataItem.netRevenue).to.be.true; + expect(dataItem.currency).to.equal('USD'); + }); + it('Should return an empty array if invalid banner response is passed', function () { + const invBanner = { + body: [{ + width: 300, + cpm: 0.4, + ad: 'Test', + requestId: '23fhj33i987f', + ttl: 120, + creativeId: '2', + netRevenue: true, + currency: 'USD', + dealId: '1' + }] + }; + + let serverResponses = spec.interpretResponse(invBanner); + expect(serverResponses).to.be.an('array').that.is.empty; + }); + it('Should return an empty array if invalid video response is passed', function () { + const invVideo = { + body: [{ + mediaType: 'video', + cpm: 0.5, + requestId: '23fhj33i987f', + ttl: 120, + creativeId: '2', + netRevenue: true, + currency: 'USD', + dealId: '1' + }] + }; + let serverResponses = spec.interpretResponse(invVideo); + expect(serverResponses).to.be.an('array').that.is.empty; + }); + it('Should return an empty array if invalid native response is passed', function () { + const invNative = { + body: [{ + mediaType: 'native', + clickUrl: 'test.com', + title: 'Test', + impressionTrackers: ['test.com'], + ttl: 120, + requestId: '23fhj33i987f', + creativeId: '2', + netRevenue: true, + currency: 'USD', + }] + }; + let serverResponses = spec.interpretResponse(invNative); + expect(serverResponses).to.be.an('array').that.is.empty; + }); + it('Should return an empty array if invalid response is passed', function () { + const invalid = { + body: [{ + ttl: 120, + creativeId: '2', + netRevenue: true, + currency: 'USD', + dealId: '1' + }] + }; + let serverResponses = spec.interpretResponse(invalid); + expect(serverResponses).to.be.an('array').that.is.empty; + }); + }); + describe('getUserSyncs', function() { + it('Should return array of objects with proper sync config , include GDPR', function() { + const syncData = spec.getUserSyncs({}, {}, { + consentString: 'ALL', + gdprApplies: true, + }, {}); + expect(syncData).to.be.an('array').which.is.not.empty; + expect(syncData[0]).to.be.an('object') + expect(syncData[0].type).to.be.a('string') + expect(syncData[0].type).to.equal('iframe') + expect(syncData[0].url).to.be.a('string') + expect(syncData[0].url).to.equal('https://ssp-cookie.logan.ai/html?src=pbjs&gdpr=1&gdpr_consent=ALL') + }); + it('Should return array of objects with proper sync config , include CCPA', function() { + const syncData = spec.getUserSyncs({}, {}, {}, { + consentString: '1NNN' + }); + expect(syncData).to.be.an('array').which.is.not.empty; + expect(syncData[0]).to.be.an('object') + expect(syncData[0].type).to.be.a('string') + expect(syncData[0].type).to.equal('iframe') + expect(syncData[0].url).to.be.a('string') + expect(syncData[0].url).to.equal('https://ssp-cookie.logan.ai/html?src=pbjs&ccpa_consent=1NNN') + }); + }); +}); diff --git a/test/spec/modules/lotamePanoramaIdSystem_spec.js b/test/spec/modules/lotamePanoramaIdSystem_spec.js index c6d3383374a..5bbec1ec26f 100644 --- a/test/spec/modules/lotamePanoramaIdSystem_spec.js +++ b/test/spec/modules/lotamePanoramaIdSystem_spec.js @@ -440,10 +440,289 @@ describe('LotameId', function() { }); }); - it('should retrieve the id when decode is called', function() { - var id = lotamePanoramaIdSubmodule.decode('1234'); - expect(id).to.be.eql({ - 'lotamePanoramaId': '1234' + describe('when gdpr applies and falls back to eupubconsent cookie', function () { + let request; + let callBackSpy = sinon.spy(); + let consentData = { + gdprApplies: true, + consentString: undefined + }; + + beforeEach(function () { + getCookieStub + .withArgs('eupubconsent-v2') + .returns('consentGiven'); + + let submoduleCallback = lotamePanoramaIdSubmodule.getId({}, consentData).callback; + submoduleCallback(callBackSpy); + + // the contents of the response don't matter for this + request = server.requests[0]; + request.respond(200, responseHeader, ''); + }); + + it('should call the remote server when getId is called', function () { + expect(callBackSpy.calledOnce).to.be.true; + }); + + it('should pass the gdpr consent string back', function() { + expect(request.url).to.be.eq( + 'https://id.crwdcntrl.net/id?gdpr_applies=true&gdpr_consent=consentGiven' + ); + }); + }); + + describe('when gdpr applies and falls back to euconsent cookie', function () { + let request; + let callBackSpy = sinon.spy(); + let consentData = { + gdprApplies: true, + consentString: undefined + }; + + beforeEach(function () { + getCookieStub + .withArgs('euconsent-v2') + .returns('consentGiven'); + + let submoduleCallback = lotamePanoramaIdSubmodule.getId({}, consentData).callback; + submoduleCallback(callBackSpy); + + // the contents of the response don't matter for this + request = server.requests[0]; + request.respond(200, responseHeader, ''); + }); + + it('should call the remote server when getId is called', function () { + expect(callBackSpy.calledOnce).to.be.true; + }); + + it('should pass the gdpr consent string back', function() { + expect(request.url).to.be.eq( + 'https://id.crwdcntrl.net/id?gdpr_applies=true&gdpr_consent=consentGiven' + ); + }); + }); + + describe('when gdpr applies but no consent string is available', function () { + let request; + let callBackSpy = sinon.spy(); + let consentData = { + gdprApplies: true, + consentString: undefined + }; + + beforeEach(function () { + let submoduleCallback = lotamePanoramaIdSubmodule.getId({}, consentData).callback; + submoduleCallback(callBackSpy); + + // the contents of the response don't matter for this + request = server.requests[0]; + request.respond(200, responseHeader, ''); + }); + + it('should call the remote server when getId is called', function () { + expect(callBackSpy.calledOnce).to.be.true; + }); + + it('should not include the gdpr consent string on the url', function() { + expect(request.url).to.be.eq( + 'https://id.crwdcntrl.net/id?gdpr_applies=true' + ); + }); + }); + + describe('when no consentData and falls back to eupubconsent cookie', function () { + let request; + let callBackSpy = sinon.spy(); + let consentData; + + beforeEach(function () { + getCookieStub + .withArgs('eupubconsent-v2') + .returns('consentGiven'); + + let submoduleCallback = lotamePanoramaIdSubmodule.getId({}, consentData).callback; + submoduleCallback(callBackSpy); + + // the contents of the response don't matter for this + request = server.requests[0]; + request.respond(200, responseHeader, ''); + }); + + it('should call the remote server when getId is called', function () { + expect(callBackSpy.calledOnce).to.be.true; + }); + + it('should pass the gdpr consent string back', function() { + expect(request.url).to.be.eq( + 'https://id.crwdcntrl.net/id?gdpr_consent=consentGiven' + ); + }); + }); + + describe('when no consentData and falls back to euconsent cookie', function () { + let request; + let callBackSpy = sinon.spy(); + let consentData; + + beforeEach(function () { + getCookieStub + .withArgs('euconsent-v2') + .returns('consentGiven'); + + let submoduleCallback = lotamePanoramaIdSubmodule.getId({}, consentData).callback; + submoduleCallback(callBackSpy); + + // the contents of the response don't matter for this + request = server.requests[0]; + request.respond(200, responseHeader, ''); + }); + + it('should call the remote server when getId is called', function () { + expect(callBackSpy.calledOnce).to.be.true; + }); + + it('should pass the gdpr consent string back', function() { + expect(request.url).to.be.eq( + 'https://id.crwdcntrl.net/id?gdpr_consent=consentGiven' + ); + }); + }); + + describe('when no consentData and no cookies', function () { + let request; + let callBackSpy = sinon.spy(); + let consentData; + + beforeEach(function () { + let submoduleCallback = lotamePanoramaIdSubmodule.getId({}, consentData).callback; + submoduleCallback(callBackSpy); + + // the contents of the response don't matter for this + request = server.requests[0]; + request.respond(200, responseHeader, ''); + }); + + it('should call the remote server when getId is called', function () { + expect(callBackSpy.calledOnce).to.be.true; + }); + + it('should pass the gdpr consent string back', function() { + expect(request.url).to.be.eq('https://id.crwdcntrl.net/id'); + }); + }); + + describe('with an empty cache, ignore profile id for error 111', function () { + let request; + let callBackSpy = sinon.spy(); + + beforeEach(function () { + let submoduleCallback = lotamePanoramaIdSubmodule.getId({}).callback; + submoduleCallback(callBackSpy); + + request = server.requests[0]; + + request.respond( + 200, + responseHeader, + JSON.stringify({ + profile_id: '4ec137245858469eb94a4e248f238694', + expiry_ts: 10, + errors: [111], + core_id: + 'ca22992567e3cd4d116a5899b88a55d0d857a23610db939ae6ac13ba2335d87a', + }) + ); + }); + + it('should not save the first party id', function () { + sinon.assert.neverCalledWith( + setLocalStorageStub, + '_cc_id', + '4ec137245858469eb94a4e248f238694' + ); + sinon.assert.neverCalledWith( + setCookieStub, + '_cc_id', + '4ec137245858469eb94a4e248f238694' + ); + }); + + it('should save the expiry', function () { + sinon.assert.calledWith(setLocalStorageStub, 'panoramaId_expiry', 10); + + sinon.assert.calledWith(setCookieStub, 'panoramaId_expiry', 10); + }); + + it('should save the id', function () { + sinon.assert.calledWith( + setLocalStorageStub, + 'panoramaId', + 'ca22992567e3cd4d116a5899b88a55d0d857a23610db939ae6ac13ba2335d87a' + ); + + sinon.assert.calledWith( + setCookieStub, + 'panoramaId', + 'ca22992567e3cd4d116a5899b88a55d0d857a23610db939ae6ac13ba2335d87a' + ); + }); + }); + + describe('receives an optout request with an error 111', function () { + let request; + let callBackSpy = sinon.spy(); + + beforeEach(function () { + getCookieStub.withArgs('panoramaId_expiry').returns('1000'); + getCookieStub + .withArgs('panoramaId') + .returns( + 'ca22992567e3cd4d116a5899b88a55d0d857a23610db939ae6ac13ba2335d87d' + ); + + let submoduleCallback = lotamePanoramaIdSubmodule.getId({}).callback; + submoduleCallback(callBackSpy); + + request = server.requests[0]; + + request.respond( + 200, + responseHeader, + JSON.stringify({ + errors: [111], + expiry_ts: Date.now() + 30 * 24 * 60 * 60 * 1000, + }) + ); + }); + + it('should call the remote server when getId is called', function () { + expect(callBackSpy.calledOnce).to.be.true; + }); + + it('should clear the panorama id', function () { + sinon.assert.calledWith(removeFromLocalStorageStub, 'panoramaId'); + + sinon.assert.calledWith( + setCookieStub, + 'panoramaId', + '', + 'Thu, 01 Jan 1970 00:00:00 GMT', + 'Lax' + ); + }); + + it('should not clear the profile id', function () { + sinon.assert.neverCalledWith(removeFromLocalStorageStub, '_cc_id'); + + sinon.assert.neverCalledWith( + setCookieStub, + '_cc_id', + '', + 'Thu, 01 Jan 1970 00:00:00 GMT', + 'Lax' + ); }); }); }); diff --git a/test/spec/modules/malltvBidAdapter_spec.js b/test/spec/modules/malltvBidAdapter_spec.js index e1e9ad867e7..ffe08ad1a5e 100644 --- a/test/spec/modules/malltvBidAdapter_spec.js +++ b/test/spec/modules/malltvBidAdapter_spec.js @@ -34,9 +34,7 @@ describe('malltvAdapterTest', () => { it('bidRequest without propertyId or placementId', () => { expect(spec.isBidRequestValid({ bidder: 'malltv', - params: { - propertyId: '{propertyId}', - } + params: {} })).to.equal(false); }); }); @@ -46,7 +44,17 @@ describe('malltvAdapterTest', () => { 'bidder': 'malltv', 'params': { 'propertyId': '{propertyId}', - 'placementId': '{placementId}' + 'placementId': '{placementId}', + 'data': { + 'catalogs': [{ + 'catalogId': 1, + 'items': ['1', '2', '3'] + }], + 'inventory': { + 'category': ['category1', 'category2'], + 'query': ['query'] + } + } }, 'adUnitCode': 'hb-leaderboard', 'transactionId': 'b6b889bb-776c-48fd-bc7b-d11a1cf0425e', @@ -86,6 +94,20 @@ describe('malltvAdapterTest', () => { expect(requestItem.data.placements[0].sizes).to.equal('300x250'); }); }); + + it('bidRequest data param', () => { + const requests = spec.buildRequests(bidRequests); + requests.forEach((requestItem) => { + expect(requestItem.data.data).to.exist; + expect(requestItem.data.data.catalogs).to.exist; + expect(requestItem.data.data.inventory).to.exist; + expect(requestItem.data.data.catalogs.length).to.equal(1); + expect(requestItem.data.data.catalogs[0].items.length).to.equal(3); + expect(Object.keys(requestItem.data.data.inventory).length).to.equal(2); + expect(requestItem.data.data.inventory.category.length).to.equal(2); + expect(requestItem.data.data.inventory.query.length).to.equal(1); + }); + }); }); describe('interpretResponse', () => { diff --git a/test/spec/modules/mediaforceBidAdapter_spec.js b/test/spec/modules/mediaforceBidAdapter_spec.js index 0b3271da770..24326afe5c0 100644 --- a/test/spec/modules/mediaforceBidAdapter_spec.js +++ b/test/spec/modules/mediaforceBidAdapter_spec.js @@ -142,7 +142,7 @@ describe('mediaforce bid adapter', function () { const requestUrl = `${baseUrl}/header_bid`; const dnt = utils.getDNT() ? 1 : 0; - const secure = window.location.protocol === 'https' ? 1 : 0; + const secure = window.location.protocol === 'https:' ? 1 : 0; const pageUrl = window.location.href; const timeout = 1500; diff --git a/test/spec/modules/missenaBidAdapter_spec.js b/test/spec/modules/missenaBidAdapter_spec.js new file mode 100644 index 00000000000..026e79c6d5a --- /dev/null +++ b/test/spec/modules/missenaBidAdapter_spec.js @@ -0,0 +1,131 @@ +import { expect } from 'chai'; +import { spec, _getPlatform } from 'modules/missenaBidAdapter.js'; +import { newBidder } from 'src/adapters/bidderFactory.js'; + +describe('Missena Adapter', function () { + const adapter = newBidder(spec); + + const bidId = 'abc'; + + const bid = { + bidder: 'missena', + bidId: bidId, + sizes: [[1, 1]], + params: { + apiKey: 'PA-34745704', + }, + }; + + describe('codes', function () { + it('should return a bidder code of missena', function () { + expect(spec.code).to.equal('missena'); + }); + }); + + describe('isBidRequestValid', function () { + it('should return true if the apiKey param is present', function () { + expect(spec.isBidRequestValid(bid)).to.equal(true); + }); + + it('should return false if the apiKey is missing', function () { + expect( + spec.isBidRequestValid(Object.assign(bid, { params: {} })) + ).to.equal(false); + }); + + it('should return false if the apiKey is an empty string', function () { + expect( + spec.isBidRequestValid(Object.assign(bid, { params: { apiKey: '' } })) + ).to.equal(false); + }); + }); + + describe('buildRequests', function () { + const consentString = 'AAAAAAAAA=='; + + const bidderRequest = { + gdprConsent: { + consentString: consentString, + gdprApplies: true, + }, + refererInfo: { + referer: 'https://referer', + canonicalUrl: 'https://canonical', + }, + }; + + const requests = spec.buildRequests([bid, bid], bidderRequest); + const request = requests[0]; + const payload = JSON.parse(request.data); + + it('should return as many server requests as bidder requests', function () { + expect(requests.length).to.equal(2); + }); + + it('should have a post method', function () { + expect(request.method).to.equal('POST'); + }); + + it('should send the bidder id', function () { + expect(payload.request_id).to.equal(bidId); + }); + + it('should send referer information to the request', function () { + expect(payload.referer).to.equal('https://referer'); + expect(payload.referer_canonical).to.equal('https://canonical'); + }); + + it('should send gdpr consent information to the request', function () { + expect(payload.consent_string).to.equal(consentString); + expect(payload.consent_required).to.equal(true); + }); + }); + + describe('interpretResponse', function () { + const serverResponse = { + requestId: bidId, + cpm: 0.5, + currency: 'USD', + ad: '', + }; + + const serverTimeoutResponse = { + requestId: bidId, + timeout: true, + ad: '', + }; + + const serverEmptyAdResponse = { + requestId: bidId, + cpm: 0.5, + currency: 'USD', + ad: '', + }; + + it('should return a proper bid response', function () { + const result = spec.interpretResponse({ body: serverResponse }, bid); + + expect(result.length).to.equal(1); + + expect(Object.keys(result[0])).to.have.members( + Object.keys(serverResponse) + ); + }); + + it('should return an empty response when the server answers with a timeout', function () { + const result = spec.interpretResponse( + { body: serverTimeoutResponse }, + bid + ); + expect(result).to.deep.equal([]); + }); + + it('should return an empty response when the server answers with an empty ad', function () { + const result = spec.interpretResponse( + { body: serverEmptyAdResponse }, + bid + ); + expect(result).to.deep.equal([]); + }); + }); +}); diff --git a/test/spec/modules/nobidBidAdapter_spec.js b/test/spec/modules/nobidBidAdapter_spec.js index e67d3b41f1d..8dc15fbc89e 100644 --- a/test/spec/modules/nobidBidAdapter_spec.js +++ b/test/spec/modules/nobidBidAdapter_spec.js @@ -257,12 +257,12 @@ describe('Nobid Adapter', function () { 'uids': [ { 'id': 'ID5_ID', - 'atype': 1 + 'atype': 1, + 'ext': { + 'linkType': 0 + } } - ], - 'ext': { - 'linkType': 0 - } + ] }, { 'source': 'adserver.org', @@ -284,7 +284,7 @@ describe('Nobid Adapter', function () { refererInfo: {referer: REFERER} } - it('should criteo eid', function () { + it('should get user ids from eids', function () { const request = spec.buildRequests(bidRequests, bidderRequest); const payload = JSON.parse(request.data); expect(payload.sid).to.exist.and.to.equal(2); diff --git a/test/spec/modules/oneVideoBidAdapter_spec.js b/test/spec/modules/oneVideoBidAdapter_spec.js index 331ac8976e6..bc37ea15255 100644 --- a/test/spec/modules/oneVideoBidAdapter_spec.js +++ b/test/spec/modules/oneVideoBidAdapter_spec.js @@ -1,5 +1,9 @@ -import { expect } from 'chai'; -import { spec } from 'modules/oneVideoBidAdapter.js'; +import { + expect +} from 'chai'; +import { + spec +} from 'modules/oneVideoBidAdapter.js'; import * as utils from 'src/utils.js'; describe('OneVideoBidAdapter', function () { @@ -200,24 +204,24 @@ describe('OneVideoBidAdapter', function () { describe('spec.buildRequests', function () { it('should create a POST request for every bid', function () { - const requests = spec.buildRequests([ bidRequest ], bidderRequest); + const requests = spec.buildRequests([bidRequest], bidderRequest); expect(requests[0].method).to.equal('POST'); expect(requests[0].url).to.equal(spec.ENDPOINT + bidRequest.params.pubId); }); it('should attach the bid request object', function () { - const requests = spec.buildRequests([ bidRequest ], bidderRequest); + const requests = spec.buildRequests([bidRequest], bidderRequest); expect(requests[0].bidRequest).to.equal(bidRequest); }); it('should attach request data', function () { - const requests = spec.buildRequests([ bidRequest ], bidderRequest); + const requests = spec.buildRequests([bidRequest], bidderRequest); const data = requests[0].data; - const [ width, height ] = bidRequest.sizes; + const [width, height] = bidRequest.sizes; const placement = bidRequest.params.video.placement; const rewarded = bidRequest.params.video.rewarded; const inventoryid = bidRequest.params.video.inventoryid; - const VERSION = '3.0.4'; + const VERSION = '3.0.5'; expect(data.imp[0].video.w).to.equal(width); expect(data.imp[0].video.h).to.equal(height); expect(data.imp[0].bidfloor).to.equal(bidRequest.params.bidfloor); @@ -231,8 +235,10 @@ describe('OneVideoBidAdapter', function () { it('must parse bid size from a nested array', function () { const width = 640; const height = 480; - bidRequest.sizes = [[ width, height ]]; - const requests = spec.buildRequests([ bidRequest ], bidderRequest); + bidRequest.sizes = [ + [width, height] + ]; + const requests = spec.buildRequests([bidRequest], bidderRequest); const data = requests[0].data; expect(data.imp[0].video.w).to.equal(width); expect(data.imp[0].video.h).to.equal(height); @@ -240,14 +246,14 @@ describe('OneVideoBidAdapter', function () { it('should set pubId to HBExchange when bid.params.video.e2etest = true', function () { bidRequest.params.video.e2etest = true; - const requests = spec.buildRequests([ bidRequest ], bidderRequest); + const requests = spec.buildRequests([bidRequest], bidderRequest); expect(requests[0].method).to.equal('POST'); expect(requests[0].url).to.equal(spec.E2ETESTENDPOINT + 'HBExchange'); }); it('should attach End 2 End test data', function () { bidRequest.params.video.e2etest = true; - const requests = spec.buildRequests([ bidRequest ], bidderRequest); + const requests = spec.buildRequests([bidRequest], bidderRequest); const data = requests[0].data; expect(data.imp[0].bidfloor).to.not.exist; expect(data.imp[0].video.w).to.equal(300); @@ -260,7 +266,7 @@ describe('OneVideoBidAdapter', function () { }); it('it should create new schain and send it if video.params.sid exists', function () { - const requests = spec.buildRequests([ bidRequest ], bidderRequest); + const requests = spec.buildRequests([bidRequest], bidderRequest); const data = requests[0].data; const schain = data.source.ext.schain; expect(schain.nodes.length).to.equal(1); @@ -281,7 +287,7 @@ describe('OneVideoBidAdapter', function () { }] }; bidRequest.schain = globalSchain; - const requests = spec.buildRequests([ bidRequest ], bidderRequest); + const requests = spec.buildRequests([bidRequest], bidderRequest); const data = requests[0].data; const schain = data.source.ext.schain; expect(schain.nodes.length).to.equal(1); @@ -300,7 +306,7 @@ describe('OneVideoBidAdapter', function () { }] }; bidRequest.schain = globalSchain; - const requests = spec.buildRequests([ bidRequest ], bidderRequest); + const requests = spec.buildRequests([bidRequest], bidderRequest); const data = requests[0].data; const schain = data.source.ext.schain; expect(schain.nodes.length).to.equal(1); @@ -310,44 +316,44 @@ describe('OneVideoBidAdapter', function () { }) it('should append hp to new schain created by sid if video.params.hp is passed', function () { - const requests = spec.buildRequests([ bidRequest ], bidderRequest); + const requests = spec.buildRequests([bidRequest], bidderRequest); const data = requests[0].data; const schain = data.source.ext.schain; expect(schain.nodes[0].hp).to.equal(bidRequest.params.video.hp); }) it('should not accept key values pairs if custom is Undefined ', function () { bidRequest.params.video.custom = null; - const requests = spec.buildRequests([ bidRequest ], bidderRequest); + const requests = spec.buildRequests([bidRequest], bidderRequest); const data = requests[0].data; expect(data.imp[0].ext.custom).to.be.undefined; }); it('should not accept key values pairs if custom is Array ', function () { bidRequest.params.video.custom = []; - const requests = spec.buildRequests([ bidRequest ], bidderRequest); + const requests = spec.buildRequests([bidRequest], bidderRequest); const data = requests[0].data; expect(data.imp[0].ext.custom).to.be.undefined; }); it('should not accept key values pairs if custom is Number ', function () { bidRequest.params.video.custom = 123456; - const requests = spec.buildRequests([ bidRequest ], bidderRequest); + const requests = spec.buildRequests([bidRequest], bidderRequest); const data = requests[0].data; expect(data.imp[0].ext.custom).to.be.undefined; }); it('should not accept key values pairs if custom is String ', function () { bidRequest.params.video.custom = 'keyValuePairs'; - const requests = spec.buildRequests([ bidRequest ], bidderRequest); + const requests = spec.buildRequests([bidRequest], bidderRequest); const data = requests[0].data; expect(data.imp[0].ext.custom).to.be.undefined; }); it('should not accept key values pairs if custom is Boolean ', function () { bidRequest.params.video.custom = true; - const requests = spec.buildRequests([ bidRequest ], bidderRequest); + const requests = spec.buildRequests([bidRequest], bidderRequest); const data = requests[0].data; expect(data.imp[0].ext.custom).to.be.undefined; }); it('should accept key values pairs if custom is Object ', function () { bidRequest.params.video.custom = {}; - const requests = spec.buildRequests([ bidRequest ], bidderRequest); + const requests = spec.buildRequests([bidRequest], bidderRequest); const data = requests[0].data; expect(data.imp[0].ext.custom).to.be.a('object'); }); @@ -357,12 +363,14 @@ describe('OneVideoBidAdapter', function () { key2: 'value2', key3: 4444444, key4: false, - key5: {nested: 'object'}, + key5: { + nested: 'object' + }, key6: ['string', 2, true, null], key7: null, key8: undefined }; - const requests = spec.buildRequests([ bidRequest ], bidderRequest); + const requests = spec.buildRequests([bidRequest], bidderRequest); const custom = requests[0].data.imp[0].ext.custom; expect(custom['key1']).to.be.a('string'); expect(custom['key2']).to.be.a('string'); @@ -373,29 +381,188 @@ describe('OneVideoBidAdapter', function () { expect(custom['key7']).to.not.exist; expect(custom['key8']).to.not.exist; }); + + describe('content object validations', function () { + it('should not accept content object if value is Undefined ', function () { + bidRequest.params.video.content = null; + const requests = spec.buildRequests([bidRequest], bidderRequest); + const data = requests[0].data; + expect(data.imp[0].content).to.be.undefined; + }); + it('should not accept content object if value is is Array ', function () { + bidRequest.params.video.content = []; + const requests = spec.buildRequests([bidRequest], bidderRequest); + const data = requests[0].data; + expect(data.imp[0].content).to.be.undefined; + }); + it('should not accept content object if value is Number ', function () { + bidRequest.params.video.content = 123456; + const requests = spec.buildRequests([bidRequest], bidderRequest); + const data = requests[0].data; + expect(data.imp[0].content).to.be.undefined; + }); + it('should not accept content object if value is String ', function () { + bidRequest.params.video.content = 'keyValuePairs'; + const requests = spec.buildRequests([bidRequest], bidderRequest); + const data = requests[0].data; + expect(data.imp[0].content).to.be.undefined; + }); + it('should not accept content object if value is Boolean ', function () { + bidRequest.params.video.content = true; + const requests = spec.buildRequests([bidRequest], bidderRequest); + const data = requests[0].data; + expect(data.imp[0].content).to.be.undefined; + }); + it('should accept content object if value is Object ', function () { + bidRequest.params.video.content = {}; + const requests = spec.buildRequests([bidRequest], bidderRequest); + const data = requests[0].data; + expect(data.imp[0].content).to.be.a('object'); + }); + + it('should not append unsupported content object keys', function () { + bidRequest.params.video.content = { + fake: 'news', + unreal: 'param', + counterfit: 'data' + }; + const requests = spec.buildRequests([bidRequest], bidderRequest); + const data = requests[0].data; + expect(data.imp[0].content).to.be.empty; + }); + + it('should not append content string parameters if value is not string ', function () { + bidRequest.params.video.content = { + id: 1234, + title: ['Title'], + series: ['Series'], + season: ['Season'], + genre: ['Genre'], + contentrating: {1: 'C-Rating'}, + language: {1: 'EN'} + }; + const requests = spec.buildRequests([bidRequest], bidderRequest); + const data = requests[0].data; + expect(data.imp[0].content).to.be.a('object'); + expect(data.imp[0].content).to.be.empty + }); + it('should not append content Number parameters if value is not Number ', function () { + bidRequest.params.video.content = { + episode: '1', + context: 'context', + livestream: {0: 'stream'}, + len: [360], + prodq: [1], + }; + const requests = spec.buildRequests([bidRequest], bidderRequest); + const data = requests[0].data; + expect(data.imp[0].content).to.be.a('object'); + expect(data.imp[0].content).to.be.empty + }); + it('should not append content Array parameters if value is not Array ', function () { + bidRequest.params.video.content = { + cat: 'categories', + }; + const requests = spec.buildRequests([bidRequest], bidderRequest); + const data = requests[0].data; + expect(data.imp[0].content).to.be.a('object'); + expect(data.imp[0].content).to.be.empty + }); + it('should not append content ext if value is not Object ', function () { + bidRequest.params.video.content = { + ext: 'content.ext', + }; + const requests = spec.buildRequests([bidRequest], bidderRequest); + const data = requests[0].data; + expect(data.imp[0].content).to.be.a('object'); + expect(data.imp[0].content).to.be.empty + }); + it('should append supported parameters if value match validations ', function () { + bidRequest.params.video.content = { + id: '1234', + title: 'Title', + series: 'Series', + season: 'Season', + cat: [ + 'IAB1' + ], + genre: 'Genre', + contentrating: 'C-Rating', + language: 'EN', + episode: 1, + prodq: 1, + context: 1, + livestream: 0, + len: 360, + ext: {} + }; + const requests = spec.buildRequests([bidRequest], bidderRequest); + const data = requests[0].data; + expect(data.imp[0].content).to.deep.equal(bidRequest.params.video.content); + }); + }); }); describe('spec.interpretResponse', function () { it('should return no bids if the response is not valid', function () { - const bidResponse = spec.interpretResponse({ body: null }, { bidRequest }); + const bidResponse = spec.interpretResponse({ + body: null + }, { + bidRequest + }); expect(bidResponse.length).to.equal(0); }); it('should return no bids if the response "nurl" and "adm" are missing', function () { - const serverResponse = {seatbid: [{bid: [{price: 6.01}]}]}; - const bidResponse = spec.interpretResponse({ body: serverResponse }, { bidRequest }); + const serverResponse = { + seatbid: [{ + bid: [{ + price: 6.01 + }] + }] + }; + const bidResponse = spec.interpretResponse({ + body: serverResponse + }, { + bidRequest + }); expect(bidResponse.length).to.equal(0); }); it('should return no bids if the response "price" is missing', function () { - const serverResponse = {seatbid: [{bid: [{adm: ''}]}]}; - const bidResponse = spec.interpretResponse({ body: serverResponse }, { bidRequest }); + const serverResponse = { + seatbid: [{ + bid: [{ + adm: '' + }] + }] + }; + const bidResponse = spec.interpretResponse({ + body: serverResponse + }, { + bidRequest + }); expect(bidResponse.length).to.equal(0); }); it('should return a valid video bid response with just "adm"', function () { - const serverResponse = {seatbid: [{bid: [{id: 1, adid: 123, crid: 2, price: 6.01, adm: ''}]}], cur: 'USD'}; - const bidResponse = spec.interpretResponse({ body: serverResponse }, { bidRequest }); + const serverResponse = { + seatbid: [{ + bid: [{ + id: 1, + adid: 123, + crid: 2, + price: 6.01, + adm: '' + }] + }], + cur: 'USD' + }; + const bidResponse = spec.interpretResponse({ + body: serverResponse + }, { + bidRequest + }); let o = { requestId: bidRequest.bidId, bidderCode: spec.code, @@ -407,7 +574,7 @@ describe('OneVideoBidAdapter', function () { height: 480, mediaType: 'video', currency: 'USD', - ttl: 100, + ttl: 300, netRevenue: true, adUnitCode: bidRequest.adUnitCode, renderer: (bidRequest.mediaTypes.video.context === 'outstream') ? newRenderer(bidRequest, bidResponse) : undefined, @@ -428,12 +595,51 @@ describe('OneVideoBidAdapter', function () { } } } - const serverResponse = {seatbid: [{bid: [{id: 1, adid: 123, crid: 2, price: 6.01, adm: '
DAP UNIT HERE
'}]}], cur: 'USD'}; - const bidResponse = spec.interpretResponse({ body: serverResponse }, { bidRequest }); + const serverResponse = { + seatbid: [{ + bid: [{ + id: 1, + adid: 123, + crid: 2, + price: 6.01, + adm: '
DAP UNIT HERE
' + }] + }], + cur: 'USD' + }; + const bidResponse = spec.interpretResponse({ + body: serverResponse + }, { + bidRequest + }); expect(bidResponse.ad).to.equal('
DAP UNIT HERE
'); expect(bidResponse.mediaType).to.equal('banner'); expect(bidResponse.renderer).to.be.undefined; }); + + it('should default ttl to 300', function () { + const serverResponse = {seatbid: [{bid: [{id: 1, adid: 123, crid: 2, price: 6.01, adm: ''}]}], cur: 'USD'}; + const bidResponse = spec.interpretResponse({ body: serverResponse }, { bidRequest }); + expect(bidResponse.ttl).to.equal(300); + }); + it('should not allow ttl above 3601, default to 300', function () { + bidRequest.params.video.ttl = 3601; + const serverResponse = {seatbid: [{bid: [{id: 1, adid: 123, crid: 2, price: 6.01, adm: ''}]}], cur: 'USD'}; + const bidResponse = spec.interpretResponse({ body: serverResponse }, { bidRequest }); + expect(bidResponse.ttl).to.equal(300); + }); + it('should not allow ttl below 1, default to 300', function () { + bidRequest.params.video.ttl = 0; + const serverResponse = {seatbid: [{bid: [{id: 1, adid: 123, crid: 2, price: 6.01, adm: ''}]}], cur: 'USD'}; + const bidResponse = spec.interpretResponse({ body: serverResponse }, { bidRequest }); + expect(bidResponse.ttl).to.equal(300); + }); + it('should use custom ttl if under 3600', function () { + bidRequest.params.video.ttl = 1000; + const serverResponse = {seatbid: [{bid: [{id: 1, adid: 123, crid: 2, price: 6.01, adm: ''}]}], cur: 'USD'}; + const bidResponse = spec.interpretResponse({ body: serverResponse }, { bidRequest }); + expect(bidResponse.ttl).to.equal(1000); + }); }); describe('when GDPR and uspConsent applies', function () { @@ -476,22 +682,22 @@ describe('OneVideoBidAdapter', function () { }); it('should send a signal to specify that GDPR applies to this request', function () { - const request = spec.buildRequests([ bidRequest ], bidderRequest); + const request = spec.buildRequests([bidRequest], bidderRequest); expect(request[0].data.regs.ext.gdpr).to.equal(1); }); it('should send the consent string', function () { - const request = spec.buildRequests([ bidRequest ], bidderRequest); + const request = spec.buildRequests([bidRequest], bidderRequest); expect(request[0].data.user.ext.consent).to.equal(bidderRequest.gdprConsent.consentString); }); it('should send the uspConsent string', function () { - const request = spec.buildRequests([ bidRequest ], bidderRequest); + const request = spec.buildRequests([bidRequest], bidderRequest); expect(request[0].data.regs.ext.us_privacy).to.equal(bidderRequest.uspConsent); }); it('should send the uspConsent and GDPR ', function () { - const request = spec.buildRequests([ bidRequest ], bidderRequest); + const request = spec.buildRequests([bidRequest], bidderRequest); expect(request[0].data.regs.ext.gdpr).to.equal(1); expect(request[0].data.regs.ext.us_privacy).to.equal(bidderRequest.uspConsent); }); @@ -535,7 +741,7 @@ describe('OneVideoBidAdapter', function () { pubId: 'OneMDisplay' } }; - const requests = spec.buildRequests([ bidRequest ], bidderRequest); + const requests = spec.buildRequests([bidRequest], bidderRequest); const data = requests[0].data; const width = bidRequest.params.video.playerWidth; const height = bidRequest.params.video.playerHeight; @@ -585,7 +791,7 @@ describe('OneVideoBidAdapter', function () { pubId: 'OneMDisplay' } }; - const requests = spec.buildRequests([ bidRequest ], bidderRequest); + const requests = spec.buildRequests([bidRequest], bidderRequest); const data = requests[0].data; const width = bidRequest.params.video.playerWidth; const height = bidRequest.params.video.playerHeight; @@ -631,7 +837,7 @@ describe('OneVideoBidAdapter', function () { pubId: 'OneMDisplay' } }; - const requests = spec.buildRequests([ bidRequest ], bidderRequest); + const requests = spec.buildRequests([bidRequest], bidderRequest); const data = requests[0].data; const width = bidRequest.params.video.playerWidth; const height = bidRequest.params.video.playerHeight; @@ -650,19 +856,28 @@ describe('OneVideoBidAdapter', function () { const GDPR_CONSENT_STRING = 'GDPR_CONSENT_STRING'; it('should get correct user sync when iframeEnabled', function () { - let pixel = spec.getUserSyncs({pixelEnabled: true}, {}, {gdprApplies: true, consentString: GDPR_CONSENT_STRING}) + let pixel = spec.getUserSyncs({ + pixelEnabled: true + }, {}, { + gdprApplies: true, + consentString: GDPR_CONSENT_STRING + }) expect(pixel[1].type).to.equal('image'); expect(pixel[1].url).to.equal('https://sync-tm.everesttech.net/upi/pid/m7y5t93k?gdpr=1&gdpr_consent=' + GDPR_CONSENT_STRING + '&redir=https%3A%2F%2Fpixel.advertising.com%2Fups%2F55986%2Fsync%3Fuid%3D%24%7BUSER_ID%7D%26_origin%3D0&gdpr=1&gdpr_consent=' + encodeURI(GDPR_CONSENT_STRING)); }); it('should default to gdprApplies=0 when consentData is undefined', function () { - let pixel = spec.getUserSyncs({pixelEnabled: true}, {}, undefined); + let pixel = spec.getUserSyncs({ + pixelEnabled: true + }, {}, undefined); expect(pixel[1].url).to.equal('https://sync-tm.everesttech.net/upi/pid/m7y5t93k?gdpr=0&gdpr_consent=&redir=https%3A%2F%2Fpixel.advertising.com%2Fups%2F55986%2Fsync%3Fuid%3D%24%7BUSER_ID%7D%26_origin%3D0&gdpr=0&gdpr_consent='); }); }); describe('verify sync pixels', function () { - let pixel = spec.getUserSyncs({pixelEnabled: true}, {}, undefined); + let pixel = spec.getUserSyncs({ + pixelEnabled: true + }, {}, undefined); it('should be UPS sync pixel for DBM', function () { expect(pixel[0].url).to.equal('https://pixel.advertising.com/ups/57304/sync?gdpr=&gdpr_consent=&_origin=0&redir=true') }); diff --git a/test/spec/modules/ozoneBidAdapter_spec.js b/test/spec/modules/ozoneBidAdapter_spec.js index c1022608b4a..10b8ce31d28 100644 --- a/test/spec/modules/ozoneBidAdapter_spec.js +++ b/test/spec/modules/ozoneBidAdapter_spec.js @@ -6,12 +6,13 @@ import {getGranularityKeyName, getGranularityObject} from '../../../modules/ozon import * as utils from '../../../src/utils.js'; const OZONEURI = 'https://elb.the-ozone-project.com/openrtb2/auction'; const BIDDER_CODE = 'ozone'; + /* NOTE - use firefox console to deep copy the objects to use here */ -var originalPropertyBag = {'lotameWasOverridden': 0, 'pageId': null}; +var originalPropertyBag = {'pageId': null}; var validBidRequests = [ { adUnitCode: 'div-gpt-ad-1460505748561-0', @@ -21,7 +22,7 @@ var validBidRequests = [ bidder: 'ozone', bidderRequestId: '1c1586b27a1b5c8', crumbs: {pubcid: '203a0692-f728-4856-87f6-9a25a6b63715'}, - params: { publisherId: '9876abcd12-3', customData: [{'settings': {}, 'targeting': {'gender': 'bart', 'age': 'low'}}], lotameData: {'Profile': {'tpid': 'c8ef27a0d4ba771a81159f0d2e792db4', 'Audiences': {'Audience': [{'id': '99999', 'abbr': 'sports'}, {'id': '88888', 'abbr': 'movie'}, {'id': '77777', 'abbr': 'blogger'}]}}}, placementId: '1310000099', siteId: '1234567890', id: 'fea37168-78f1-4a23-a40e-88437a99377e', auctionId: '27dcb421-95c6-4024-a624-3c03816c5f99', imp: [ { id: '2899ec066a91ff8', tagid: 'undefined', secure: 1, banner: { format: [{ w: 300, h: 250 }, { w: 300, h: 600 }], h: 250, topframe: 1, w: 300 } } ] }, + params: { publisherId: '9876abcd12-3', customData: [{'settings': {}, 'targeting': {'gender': 'bart', 'age': 'low'}}], placementId: '1310000099', siteId: '1234567890', id: 'fea37168-78f1-4a23-a40e-88437a99377e', auctionId: '27dcb421-95c6-4024-a624-3c03816c5f99', imp: [ { id: '2899ec066a91ff8', tagid: 'undefined', secure: 1, banner: { format: [{ w: 300, h: 250 }, { w: 300, h: 600 }], h: 250, topframe: 1, w: 300 } } ] }, sizes: [[300, 250], [300, 600]], transactionId: '2e63c0ed-b10c-4008-aed5-84582cecfe87' } @@ -36,7 +37,7 @@ var validBidRequestsMulti = [ bidder: 'ozone', bidderRequestId: '1c1586b27a1b5c8', crumbs: {pubcid: '203a0692-f728-4856-87f6-9a25a6b63715'}, - params: { publisherId: '9876abcd12-3', customData: [{'settings': {}, 'targeting': {'gender': 'bart', 'age': 'low'}}], lotameData: {'Profile': {'tpid': 'c8ef27a0d4ba771a81159f0d2e792db4', 'Audiences': {'Audience': [{'id': '99999', 'abbr': 'sports'}, {'id': '88888', 'abbr': 'movie'}, {'id': '77777', 'abbr': 'blogger'}]}}}, placementId: '1310000099', siteId: '1234567890', id: 'fea37168-78f1-4a23-a40e-88437a99377e', auctionId: '27dcb421-95c6-4024-a624-3c03816c5f99', imp: [ { id: '2899ec066a91ff8', tagid: 'undefined', secure: 1, banner: { format: [{ w: 300, h: 250 }, { w: 300, h: 600 }], h: 250, topframe: 1, w: 300 } } ] }, + params: { publisherId: '9876abcd12-3', customData: [{'settings': {}, 'targeting': {'gender': 'bart', 'age': 'low'}}], placementId: '1310000099', siteId: '1234567890', id: 'fea37168-78f1-4a23-a40e-88437a99377e', auctionId: '27dcb421-95c6-4024-a624-3c03816c5f99', imp: [ { id: '2899ec066a91ff8', tagid: 'undefined', secure: 1, banner: { format: [{ w: 300, h: 250 }, { w: 300, h: 600 }], h: 250, topframe: 1, w: 300 } } ] }, sizes: [[300, 250], [300, 600]], transactionId: '2e63c0ed-b10c-4008-aed5-84582cecfe87' }, @@ -49,11 +50,14 @@ var validBidRequestsMulti = [ bidder: 'ozone', bidderRequestId: '1c1586b27a1b5c0', crumbs: {pubcid: '203a0692-f728-4856-87f6-9a25a6b63715'}, - params: { publisherId: '9876abcd12-3', customData: [{'settings': {}, 'targeting': {'gender': 'bart', 'age': 'low'}}], lotameData: {'Profile': {'tpid': 'c8ef27a0d4ba771a81159f0d2e792db4', 'Audiences': {'Audience': [{'id': '99999', 'abbr': 'sports'}, {'id': '88888', 'abbr': 'movie'}, {'id': '77777', 'abbr': 'blogger'}]}}}, placementId: '1310000099', siteId: '1234567890', id: 'fea37168-78f1-4a23-a40e-88437a99377e', auctionId: '27dcb421-95c6-4024-a624-3c03816c5f99', imp: [ { id: '2899ec066a91ff8', tagid: 'undefined', secure: 1, banner: { format: [{ w: 300, h: 250 }, { w: 300, h: 600 }], h: 250, topframe: 1, w: 300 } } ] }, + params: { publisherId: '9876abcd12-3', customData: [{'settings': {}, 'targeting': {'gender': 'bart', 'age': 'low'}}], placementId: '1310000099', siteId: '1234567890', id: 'fea37168-78f1-4a23-a40e-88437a99377e', auctionId: '27dcb421-95c6-4024-a624-3c03816c5f99', imp: [ { id: '2899ec066a91ff8', tagid: 'undefined', secure: 1, banner: { format: [{ w: 300, h: 250 }, { w: 300, h: 600 }], h: 250, topframe: 1, w: 300 } } ] }, sizes: [[300, 250], [300, 600]], transactionId: '2e63c0ed-b10c-4008-aed5-84582cecfe87' } ]; +// use 'pubcid', 'tdid', 'id5id', 'parrableId', 'idl_env', 'criteoId', 'criteortus' +// NOTE THAT criteortus is no longer referenced anywhere - should be removed asap +// see http://prebid.org/dev-docs/modules/userId.html var validBidRequestsWithUserIdData = [ { adUnitCode: 'div-gpt-ad-1460505748561-0', @@ -63,10 +67,83 @@ var validBidRequestsWithUserIdData = [ bidder: 'ozone', bidderRequestId: '1c1586b27a1b5c8', crumbs: {pubcid: '203a0692-f728-4856-87f6-9a25a6b63715'}, - params: { publisherId: '9876abcd12-3', customData: [{'settings': {}, 'targeting': {'gender': 'bart', 'age': 'low'}}], lotameData: {'Profile': {'tpid': 'c8ef27a0d4ba771a81159f0d2e792db4', 'Audiences': {'Audience': [{'id': '99999', 'abbr': 'sports'}, {'id': '88888', 'abbr': 'movie'}, {'id': '77777', 'abbr': 'blogger'}]}}}, placementId: '1310000099', siteId: '1234567890', id: 'fea37168-78f1-4a23-a40e-88437a99377e', auctionId: '27dcb421-95c6-4024-a624-3c03816c5f99', imp: [ { id: '2899ec066a91ff8', tagid: 'undefined', secure: 1, banner: { format: [{ w: 300, h: 250 }, { w: 300, h: 600 }], h: 250, topframe: 1, w: 300 } } ] }, + params: { publisherId: '9876abcd12-3', customData: [{'settings': {}, 'targeting': {'gender': 'bart', 'age': 'low'}}], placementId: '1310000099', siteId: '1234567890', id: 'fea37168-78f1-4a23-a40e-88437a99377e', auctionId: '27dcb421-95c6-4024-a624-3c03816c5f99', imp: [ { id: '2899ec066a91ff8', tagid: 'undefined', secure: 1, banner: { format: [{ w: 300, h: 250 }, { w: 300, h: 600 }], h: 250, topframe: 1, w: 300 } } ] }, sizes: [[300, 250], [300, 600]], transactionId: '2e63c0ed-b10c-4008-aed5-84582cecfe87', - userId: {'pubcid': '12345678', 'id5id': { 'uid': 'ID5-someId' }, 'criteortus': {'ozone': {'userid': 'critId123'}}, 'idl_env': 'liverampId', 'lipb': {'lipbid': 'lipbidId123'}, 'parrableId': {eid: 'parrableid123'}} + userId: { + 'pubcid': '12345678', + 'tdid': '1111tdid', + 'id5id': 'ID5-someId', + 'criteortus': {'ozone': {'userid': 'critId123'}}, + 'criteoId': '1111criteoId', + 'idl_env': 'liverampId', + 'lipb': {'lipbid': 'lipbidId123'}, + 'parrableId': {'eid': '01.5678.parrableid'} + }, + userIdAsEids: [ + { + 'source': 'pubcid.org', + 'uids': [ + { + 'id': '12345678', + 'atype': 1 + } + ] + }, + { + 'source': 'adserver.org', + 'uids': [{ + 'id': '1111tdid', + 'atype': 1, + 'ext': { + 'rtiPartner': 'TDID' + } + }] + }, + { + 'source': 'id5-sync.com', + 'uids': [{ + 'id': 'ID5-someId', + 'atype': 1, + }] + }, + { + 'source': 'criteortus', + 'uids': [{ + 'id': {'ozone': {'userid': 'critId123'}}, + 'atype': 1, + }] + }, + { + 'source': 'criteoId', + 'uids': [{ + 'id': '1111criteoId', + 'atype': 1, + }] + }, + { + 'source': 'idl_env', + 'uids': [{ + 'id': 'liverampId', + 'atype': 1, + }] + }, + { + 'source': 'lipb', + 'uids': [{ + 'id': {'lipbid': 'lipbidId123'}, + 'atype': 1, + }] + }, + { + 'source': 'parrableId', + 'uids': [{ + 'id': {'eid': '01.5678.parrableid'}, + 'atype': 1, + }] + } + ] + } ]; var validBidRequestsMinimal = [ @@ -91,7 +168,7 @@ var validBidRequestsNoSizes = [ bidder: 'ozone', bidderRequestId: '1c1586b27a1b5c8', crumbs: {pubcid: '203a0692-f728-4856-87f6-9a25a6b63715'}, - params: { publisherId: '9876abcd12-3', customData: [{'settings': {}, 'targeting': {'gender': 'bart', 'age': 'low'}}], lotameData: {'Profile': {'tpid': 'c8ef27a0d4ba771a81159f0d2e792db4', 'Audiences': {'Audience': [{'id': '99999', 'abbr': 'sports'}, {'id': '88888', 'abbr': 'movie'}, {'id': '77777', 'abbr': 'blogger'}]}}}, placementId: '1310000099', siteId: '1234567890', id: 'fea37168-78f1-4a23-a40e-88437a99377e', auctionId: '27dcb421-95c6-4024-a624-3c03816c5f99', imp: [ { id: '2899ec066a91ff8', tagid: 'undefined', secure: 1, banner: { format: [{ w: 300, h: 250 }, { w: 300, h: 600 }], h: 250, topframe: 1, w: 300 } } ] }, + params: { publisherId: '9876abcd12-3', customData: [{'settings': {}, 'targeting': {'gender': 'bart', 'age': 'low'}}], placementId: '1310000099', siteId: '1234567890', id: 'fea37168-78f1-4a23-a40e-88437a99377e', auctionId: '27dcb421-95c6-4024-a624-3c03816c5f99', imp: [ { id: '2899ec066a91ff8', tagid: 'undefined', secure: 1, banner: { format: [{ w: 300, h: 250 }, { w: 300, h: 600 }], h: 250, topframe: 1, w: 300 } } ] }, transactionId: '2e63c0ed-b10c-4008-aed5-84582cecfe87' } ]; @@ -105,7 +182,7 @@ var validBidRequestsWithBannerMediaType = [ bidder: 'ozone', bidderRequestId: '1c1586b27a1b5c8', crumbs: {pubcid: '203a0692-f728-4856-87f6-9a25a6b63715'}, - params: { publisherId: '9876abcd12-3', customData: [{'settings': {}, 'targeting': {'gender': 'bart', 'age': 'low'}}], lotameData: {'Profile': {'tpid': 'c8ef27a0d4ba771a81159f0d2e792db4', 'Audiences': {'Audience': [{'id': '99999', 'abbr': 'sports'}, {'id': '88888', 'abbr': 'movie'}, {'id': '77777', 'abbr': 'blogger'}]}}}, placementId: '1310000099', siteId: '1234567890', id: 'fea37168-78f1-4a23-a40e-88437a99377e', auctionId: '27dcb421-95c6-4024-a624-3c03816c5f99', imp: [ { id: '2899ec066a91ff8', tagid: 'undefined', secure: 1, banner: { format: [{ w: 300, h: 250 }, { w: 300, h: 600 }], h: 250, topframe: 1, w: 300 } } ] }, + params: { publisherId: '9876abcd12-3', customData: [{'settings': {}, 'targeting': {'gender': 'bart', 'age': 'low'}}], placementId: '1310000099', siteId: '1234567890', id: 'fea37168-78f1-4a23-a40e-88437a99377e', auctionId: '27dcb421-95c6-4024-a624-3c03816c5f99', imp: [ { id: '2899ec066a91ff8', tagid: 'undefined', secure: 1, banner: { format: [{ w: 300, h: 250 }, { w: 300, h: 600 }], h: 250, topframe: 1, w: 300 } } ] }, mediaTypes: {banner: {sizes: [[300, 250], [300, 600]]}}, transactionId: '2e63c0ed-b10c-4008-aed5-84582cecfe87' } @@ -119,7 +196,7 @@ var validBidRequestsWithNonBannerMediaTypesAndValidOutstreamVideo = [ bidder: 'ozone', bidderRequestId: '1c1586b27a1b5c8', crumbs: {pubcid: '203a0692-f728-4856-87f6-9a25a6b63715'}, - params: { publisherId: '9876abcd12-3', customData: [{'settings': {}, 'targeting': {'gender': 'bart', 'age': 'low'}}], lotameData: {'Profile': {'tpid': 'c8ef27a0d4ba771a81159f0d2e792db4', 'Audiences': {'Audience': [{'id': '99999', 'abbr': 'sports'}, {'id': '88888', 'abbr': 'movie'}, {'id': '77777', 'abbr': 'blogger'}]}}}, placementId: '1310000099', siteId: '1234567890', id: 'fea37168-78f1-4a23-a40e-88437a99377e', auctionId: '27dcb421-95c6-4024-a624-3c03816c5f99', imp: [ { id: '2899ec066a91ff8', tagid: 'undefined', secure: 1, video: {skippable: true, playback_method: ['auto_play_sound_off'], targetDiv: 'some-different-div-id-to-my-adunitcode'} } ] }, + params: { publisherId: '9876abcd12-3', customData: [{'settings': {}, 'targeting': {'gender': 'bart', 'age': 'low'}}], placementId: '1310000099', siteId: '1234567890', id: 'fea37168-78f1-4a23-a40e-88437a99377e', auctionId: '27dcb421-95c6-4024-a624-3c03816c5f99', imp: [ { id: '2899ec066a91ff8', tagid: 'undefined', secure: 1, video: {skippable: true, playback_method: ['auto_play_sound_off'], targetDiv: 'some-different-div-id-to-my-adunitcode'} } ] }, mediaTypes: {video: {mimes: ['video/mp4'], 'context': 'outstream', 'sizes': [640, 480], playerSize: [640, 480]}, native: {info: 'dummy data'}}, transactionId: '2e63c0ed-b10c-4008-aed5-84582cecfe87' } @@ -161,43 +238,21 @@ var validBidRequests1OutstreamVideo2020 = [ } } ], - 'lotameData': { - 'Profile': { - 'tpid': '4e5c21fc7c181c2b1eb3a73d543a27f6', - 'pid': '3a45fd4872fa01f35c49586d8dcb7c60', - 'Audiences': { - 'Audience': [ - { - 'id': '439847', - 'abbr': 'all' - }, - { - 'id': '446197', - 'abbr': 'Arts, Culture & Literature' - }, - { - 'id': '446198', - 'abbr': 'Business' - } - ] - } + 'userId': { + 'pubcid': '2ada6ae6-aeca-4e07-8922-a99b3aaf8a56' + }, + 'userIdAsEids': [ + { + 'source': 'pubcid.org', + 'uids': [ + { + 'id': '2ada6ae6-aeca-4e07-8922-a99b3aaf8a56', + 'atype': 1 + } + ] } - } - }, - 'userId': { - 'pubcid': '2ada6ae6-aeca-4e07-8922-a99b3aaf8a56' + ] }, - 'userIdAsEids': [ - { - 'source': 'pubcid.org', - 'uids': [ - { - 'id': '2ada6ae6-aeca-4e07-8922-a99b3aaf8a56', - 'atype': 1 - } - ] - } - ], 'mediaTypes': { 'video': { 'playerSize': [ @@ -272,32 +327,10 @@ var validBidderRequest1OutstreamVideo2020 = { 'pt9': '|k0xw2vqzp33kklb3j5w4|||' } } - ], - 'lotameData': { - 'Profile': { - 'tpid': '4e5c21fc7c181c2b1eb3a73d543a27f6', - 'pid': '3a45fd4872fa01f35c49586d8dcb7c60', - 'Audiences': { - 'Audience': [ - { - 'id': '439847', - 'abbr': 'all' - }, - { - 'id': '446197', - 'abbr': 'Arts, Culture & Literature' - }, - { - 'id': '446198', - 'abbr': 'Business' - } - ] - } - } - } + ] }, 'userId': { - 'id5id': { uid: 'ID5-ZHMOpSv9CkZNiNd1oR4zc62AzCgSS73fPjmQ6Od7OA' }, + 'id5id': 'ID5-ZHMOpSv9CkZNiNd1oR4zc62AzCgSS73fPjmQ6Od7OA', 'pubcid': '2ada6ae6-aeca-4e07-8922-a99b3aaf8a56' }, 'userIdAsEids': [ @@ -370,7 +403,7 @@ var validBidderRequest = { bidder: 'ozone', bidderRequestId: '1c1586b27a1b5c8', crumbs: {pubcid: '203a0692-f728-4856-87f6-9a25a6b63715'}, - params: { publisherId: '9876abcd12-3', customData: [{'settings': {}, 'targeting': {'gender': 'bart', 'age': 'low'}}], lotameData: {'Profile': {'tpid': 'c8ef27a0d4ba771a81159f0d2e792db4', 'Audiences': {'Audience': [{'id': '99999', 'abbr': 'sports'}, {'id': '88888', 'abbr': 'movie'}, {'id': '77777', 'abbr': 'blogger'}]}}}, placementId: '1310000099', siteId: '1234567890', id: 'fea37168-78f1-4a23-a40e-88437a99377e', auctionId: '27dcb421-95c6-4024-a624-3c03816c5f99', imp: [ { banner: { topframe: 1, w: 300, h: 250, format: [{ w: 300, h: 250 }, { w: 300, h: 600 }] }, id: '2899ec066a91ff8', secure: 1, tagid: 'undefined' } ] }, + params: { publisherId: '9876abcd12-3', customData: [{'settings': {}, 'targeting': {'gender': 'bart', 'age': 'low'}}], placementId: '1310000099', siteId: '1234567890', id: 'fea37168-78f1-4a23-a40e-88437a99377e', auctionId: '27dcb421-95c6-4024-a624-3c03816c5f99', imp: [ { banner: { topframe: 1, w: 300, h: 250, format: [{ w: 300, h: 250 }, { w: 300, h: 600 }] }, id: '2899ec066a91ff8', secure: 1, tagid: 'undefined' } ] }, sizes: [[300, 250], [300, 600]], transactionId: '2e63c0ed-b10c-4008-aed5-84582cecfe87' }], @@ -399,7 +432,7 @@ var bidderRequestWithFullGdpr = { bidder: 'ozone', bidderRequestId: '1c1586b27a1b5c8', crumbs: {pubcid: '203a0692-f728-4856-87f6-9a25a6b63715'}, - params: { publisherId: '9876abcd12-3', customData: [{'settings': {}, 'targeting': {'gender': 'bart', 'age': 'low'}}], lotameData: {'Profile': {'tpid': 'c8ef27a0d4ba771a81159f0d2e792db4', 'Audiences': {'Audience': [{'id': '99999', 'abbr': 'sports'}, {'id': '88888', 'abbr': 'movie'}, {'id': '77777', 'abbr': 'blogger'}]}}}, placementId: '1310000099', siteId: '1234567890', id: 'fea37168-78f1-4a23-a40e-88437a99377e', auctionId: '27dcb421-95c6-4024-a624-3c03816c5f99', imp: [ { banner: { topframe: 1, w: 300, h: 250, format: [{ w: 300, h: 250 }, { w: 300, h: 600 }] }, id: '2899ec066a91ff8', secure: 1, tagid: 'undefined' } ] }, + params: { publisherId: '9876abcd12-3', customData: [{'settings': {}, 'targeting': {'gender': 'bart', 'age': 'low'}}], placementId: '1310000099', siteId: '1234567890', id: 'fea37168-78f1-4a23-a40e-88437a99377e', auctionId: '27dcb421-95c6-4024-a624-3c03816c5f99', imp: [ { banner: { topframe: 1, w: 300, h: 250, format: [{ w: 300, h: 250 }, { w: 300, h: 600 }] }, id: '2899ec066a91ff8', secure: 1, tagid: 'undefined' } ] }, sizes: [[300, 250], [300, 600]], transactionId: '2e63c0ed-b10c-4008-aed5-84582cecfe87' }], @@ -491,17 +524,6 @@ var bidderRequestWithPartialGdpr = { params: { publisherId: '9876abcd12-3', customData: [{'settings': {}, 'targeting': {'gender': 'bart', 'age': 'low'}}], - lotameData: { - 'Profile': { - 'tpid': 'c8ef27a0d4ba771a81159f0d2e792db4', - 'Audiences': { - 'Audience': [{'id': '99999', 'abbr': 'sports'}, { - 'id': '88888', - 'abbr': 'movie' - }, {'id': '77777', 'abbr': 'blogger'}] - } - } - }, placementId: '1310000099', siteId: '1234567890', id: 'fea37168-78f1-4a23-a40e-88437a99377e', @@ -982,29 +1004,7 @@ var multiRequest1 = [ 'pt9': '|k0xw2vqzp33kklb3j5w4|||' } } - ], - 'lotameData': { - 'Profile': { - 'tpid': '4e5c21fc7c181c2b1eb3a73d543a27f6', - 'pid': '3a45fd4872fa01f35c49586d8dcb7c60', - 'Audiences': { - 'Audience': [ - { - 'id': '439847', - 'abbr': 'all' - }, - { - 'id': '446197', - 'abbr': 'Arts, Culture & Literature' - }, - { - 'id': '446198', - 'abbr': 'Business' - } - ] - } - } - } + ] }, 'mediaTypes': { 'banner': { @@ -1069,29 +1069,7 @@ var multiRequest1 = [ 'pt9': '|k0xw2vqzp33kklb3j5w4|||' } } - ], - 'lotameData': { - 'Profile': { - 'tpid': '4e5c21fc7c181c2b1eb3a73d543a27f6', - 'pid': '3a45fd4872fa01f35c49586d8dcb7c60', - 'Audiences': { - 'Audience': [ - { - 'id': '439847', - 'abbr': 'all' - }, - { - 'id': '446197', - 'abbr': 'Arts, Culture & Literature' - }, - { - 'id': '446198', - 'abbr': 'Business' - } - ] - } - } - } + ] }, 'mediaTypes': { 'banner': { @@ -1165,29 +1143,7 @@ var multiBidderRequest1 = { 'pt9': '|k0xw2vqzp33kklb3j5w4|||' } } - ], - 'lotameData': { - 'Profile': { - 'tpid': '4e5c21fc7c181c2b1eb3a73d543a27f6', - 'pid': '3a45fd4872fa01f35c49586d8dcb7c60', - 'Audiences': { - 'Audience': [ - { - 'id': '439847', - 'abbr': 'all' - }, - { - 'id': '446197', - 'abbr': 'Arts, Culture & Literature' - }, - { - 'id': '446198', - 'abbr': 'Business' - } - ] - } - } - } + ] }, 'mediaTypes': { 'banner': { @@ -1252,29 +1208,7 @@ var multiBidderRequest1 = { 'pt9': '|k0xw2vqzp33kklb3j5w4|||' } } - ], - 'lotameData': { - 'Profile': { - 'tpid': '4e5c21fc7c181c2b1eb3a73d543a27f6', - 'pid': '3a45fd4872fa01f35c49586d8dcb7c60', - 'Audiences': { - 'Audience': [ - { - 'id': '439847', - 'abbr': 'all' - }, - { - 'id': '446197', - 'abbr': 'Arts, Culture & Literature' - }, - { - 'id': '446198', - 'abbr': 'Business' - } - ] - } - } - } + ] }, 'mediaTypes': { 'banner': { @@ -1588,8 +1522,7 @@ describe('ozone Adapter', function () { placementId: '1310000099', publisherId: '9876abcd12-3', siteId: '1234567890', - customData: [{'settings': {}, 'targeting': {'gender': 'bart', 'age': 'low'}}], - lotameData: {'Profile': {'tpid': 'c8ef27a0d4ba771a81159f0d2e792db4', 'Audiences': {'Audience': [{'id': '99999', 'abbr': 'sports'}, {'id': '88888', 'abbr': 'movie'}, {'id': '77777', 'abbr': 'blogger'}]}}}, + customData: [{'settings': {}, 'targeting': {'gender': 'bart', 'age': 'low'}}] }, siteId: 1234567890 } @@ -1883,26 +1816,11 @@ describe('ozone Adapter', function () { it('should not validate customParams - this is a renamed key', function () { expect(spec.isBidRequestValid(xBadCustomParams)).to.equal(false); }); - - var xBadLotame = { - bidder: BIDDER_CODE, - params: { - 'placementId': '1234567890', - 'publisherId': '9876abcd12-3', - 'lotameData': 'this should be an object', - siteId: '1234567890' - } - }; - it('should not validate lotameData being sent', function () { - expect(spec.isBidRequestValid(xBadLotame)).to.equal(false); - }); - var xBadVideoContext2 = { bidder: BIDDER_CODE, params: { 'placementId': '1234567890', 'publisherId': '9876abcd12-3', - 'lotameData': {}, siteId: '1234567890' }, mediaTypes: { @@ -1937,35 +1855,6 @@ describe('ozone Adapter', function () { instreamVid.mediaTypes.video.context = 'instream'; expect(spec.isBidRequestValid(instreamVid)).to.equal(true); }); - // validate lotame override parameters - it('should validate lotame override params', function () { - // mock the getGetParametersAsObject function to simulate GET parameters for lotame overrides: - spec.getGetParametersAsObject = function() { - return {'oz_lotameid': '123abc', 'oz_lotamepid': 'pid123', 'oz_lotametpid': 'tpid123'}; - }; - expect(spec.isBidRequestValid(validBidReq)).to.equal(true); - }); - it('should validate missing lotame override params', function () { - // mock the getGetParametersAsObject function to simulate GET parameters for lotame overrides: - spec.getGetParametersAsObject = function() { - return {'oz_lotameid': '123abc', 'oz_lotamepid': 'pid123'}; - }; - expect(spec.isBidRequestValid(validBidReq)).to.equal(false); - }); - it('should validate invalid lotame override params', function () { - // mock the getGetParametersAsObject function to simulate GET parameters for lotame overrides: - spec.getGetParametersAsObject = function() { - return {'oz_lotameid': '123abc', 'oz_lotamepid': 'pid123', 'oz_lotametpid': '123 "this ain\\t right!" eee'}; - }; - expect(spec.isBidRequestValid(validBidReq)).to.equal(false); - }); - it('should validate no lotame override params', function () { - // mock the getGetParametersAsObject function to simulate GET parameters for lotame overrides: - spec.getGetParametersAsObject = function() { - return {}; - }; - expect(spec.isBidRequestValid(validBidReq)).to.equal(true); - }); }); describe('buildRequests', function () { @@ -1989,19 +1878,27 @@ describe('ozone Adapter', function () { const request = spec.buildRequests(validBidRequests, validBidderRequest.bidderRequest); expect(request.data).to.be.a('string'); var data = JSON.parse(request.data); - expect(data.ext.ozone.lotameData).to.be.an('object'); + expect(data.imp[0].ext.ozone.customData).to.be.an('array'); + expect(request).not.to.have.key('lotameData'); + expect(request).not.to.have.key('customData'); + }); + + it('adds all parameters inside the ext object only - lightning', function () { + let localBidReq = JSON.parse(JSON.stringify(validBidRequests)); + const request = spec.buildRequests(localBidReq, validBidderRequest.bidderRequest); + expect(request.data).to.be.a('string'); + var data = JSON.parse(request.data); expect(data.imp[0].ext.ozone.customData).to.be.an('array'); expect(request).not.to.have.key('lotameData'); expect(request).not.to.have.key('customData'); }); it('ignores ozoneData in & after version 2.1.1', function () { - let validBidRequestsWithOzoneData = validBidRequests; + let validBidRequestsWithOzoneData = JSON.parse(JSON.stringify(validBidRequests)); validBidRequestsWithOzoneData[0].params.ozoneData = {'networkID': '3048', 'dfpSiteID': 'd.thesun', 'sectionID': 'homepage', 'path': '/', 'sec_id': 'null', 'sec': 'sec', 'topics': 'null', 'kw': 'null', 'aid': 'null', 'search': 'null', 'article_type': 'null', 'hide_ads': '', 'article_slug': 'null'}; - const request = spec.buildRequests(validBidRequests, validBidderRequest.bidderRequest); + const request = spec.buildRequests(validBidRequestsWithOzoneData, validBidderRequest.bidderRequest); expect(request.data).to.be.a('string'); var data = JSON.parse(request.data); - expect(data.ext.ozone.lotameData).to.be.an('object'); expect(data.imp[0].ext.ozone.customData).to.be.an('array'); expect(data.imp[0].ext.ozone.ozoneData).to.be.undefined; expect(request).not.to.have.key('lotameData'); @@ -2018,7 +1915,7 @@ describe('ozone Adapter', function () { expect(request).to.have.all.keys(['bidderRequest', 'data', 'method', 'url']); }); - it('handles no ozone, lotame or custom data', function () { + it('handles no ozone or custom data', function () { const request = spec.buildRequests(validBidRequestsMinimal, validBidderRequest.bidderRequest); expect(request).to.have.all.keys(['bidderRequest', 'data', 'method', 'url']); }); @@ -2080,6 +1977,7 @@ describe('ozone Adapter', function () { expect(payload.regs.ext.gdpr).to.equal(1); expect(payload.user.ext.consent).to.equal(consentString); }); + it('should set regs.ext.gdpr flag to 0 when gdprApplies is false', function () { let consentString = 'BOcocyaOcocyaAfEYDENCD-AAAAjx7_______9______9uz_Ov_v_f__33e8__9v_l_7_-___u_-33d4-_1vf99yfm1-7ftr3tp_87ues2_Xur__59__3z3_NphLgA=='; let bidderRequest = validBidderRequest.bidderRequest; @@ -2118,13 +2016,14 @@ describe('ozone Adapter', function () { bidRequests[0]['userId'] = { 'criteortus': '1111', 'digitrustid': {data: {id: 'DTID', keyv: 4, privacy: {optout: false}, producer: 'ABC', version: 2}}, - 'id5id': {'uid': '2222'}, + 'id5id': '2222', 'idl_env': '3333', 'lipb': {'lipbid': '4444'}, - 'parrableId': {eid: 'eidVersion.encryptionKeyReference.encryptedValue'}, + 'parrableid': 'eidVersion.encryptionKeyReference.encryptedValue', 'pubcid': '5555', 'tdid': '6666' }; + bidRequests[0]['userIdAsEids'] = validBidRequestsWithUserIdData[0]['userIdAsEids']; const request = spec.buildRequests(bidRequests, bidderRequest); const payload = JSON.parse(request.data); let firstBid = payload.imp[0].ext.ozone; @@ -2138,62 +2037,121 @@ describe('ozone Adapter', function () { bidRequests[0]['userId'] = { 'criteortus': '1111', 'digitrustid': {data: {id: 'DTID', keyv: 4, privacy: {optout: false}, producer: 'ABC', version: 2}}, - 'id5id': {'uid': '2222'}, + 'id5id': '2222', 'idl_env': '3333', 'lipb': {'lipbid': '4444'}, - 'parrableId': {eid: 'eidVersion.encryptionKeyReference.encryptedValue'}, + 'parrableid': 'eidVersion.encryptionKeyReference.encryptedValue', // 'pubcid': '5555', // remove pubcid from here to emulate the OLD module & cause the failover code to kick in 'tdid': '6666' }; + bidRequests[0]['userIdAsEids'] = validBidRequestsWithUserIdData[0]['userIdAsEids']; const request = spec.buildRequests(bidRequests, validBidderRequest.bidderRequest); const payload = JSON.parse(request.data); expect(payload.ext.ozone.pubcid).to.equal(bidRequests[0]['crumbs']['pubcid']); delete validBidRequests[0].userId; // tidy up now, else it will screw with other tests }); - it('should add a user.ext.eids object to contain user ID data in the new location (Nov 2019)', function() { + it('should add a user.ext.eids object to contain user ID data in the new location (Nov 2019) Updated Aug 2020', function() { const request = spec.buildRequests(validBidRequestsWithUserIdData, validBidderRequest.bidderRequest); + /* + 'pubcid': '12345678', + 'tdid': '1111tdid', + 'id5id': 'ID5-someId', + 'criteortus': {'ozone': {'userid': 'critId123'}}, + 'criteoId': '1111criteoId', + 'idl_env': 'liverampId', + 'lipb': {'lipbid': 'lipbidId123'}, + 'parrableId': {'eid': '01.5678.parrableid'} + */ + const payload = JSON.parse(request.data); expect(payload.user).to.exist; expect(payload.user.ext).to.exist; expect(payload.user.ext.eids).to.exist; - expect(payload.user.ext.eids[0]['source']).to.equal('pubcid'); + expect(payload.user.ext.eids[0]['source']).to.equal('pubcid.org'); expect(payload.user.ext.eids[0]['uids'][0]['id']).to.equal('12345678'); - expect(payload.user.ext.eids[1]['source']).to.equal('pubcommon'); - expect(payload.user.ext.eids[1]['uids'][0]['id']).to.equal('12345678'); + expect(payload.user.ext.eids[1]['source']).to.equal('adserver.org'); + expect(payload.user.ext.eids[1]['uids'][0]['id']).to.equal('1111tdid'); expect(payload.user.ext.eids[2]['source']).to.equal('id5-sync.com'); expect(payload.user.ext.eids[2]['uids'][0]['id']).to.equal('ID5-someId'); - expect(payload.user.ext.eids[3]['source']).to.equal('criteortus'); - expect(payload.user.ext.eids[3]['uids'][0]['id']).to.equal('critId123'); - expect(payload.user.ext.eids[4]['source']).to.equal('liveramp.com'); - expect(payload.user.ext.eids[4]['uids'][0]['id']).to.equal('liverampId'); - expect(payload.user.ext.eids[5]['source']).to.equal('liveintent.com'); - expect(payload.user.ext.eids[5]['uids'][0]['id']).to.equal('lipbidId123'); - expect(payload.user.ext.eids[6]['source']).to.equal('parrable.com'); - expect(payload.user.ext.eids[6]['uids'][0]['id']).to.equal('parrableid123'); + expect(payload.user.ext.eids[3]['source']).to.equal('criteortus'); // this is deprecated + expect(payload.user.ext.eids[3]['uids'][0]['id']['ozone']['userid']).to.equal('critId123'); + expect(payload.user.ext.eids[4]['source']).to.equal('criteoId'); + expect(payload.user.ext.eids[4]['uids'][0]['id']).to.equal('1111criteoId'); + expect(payload.user.ext.eids[5]['source']).to.equal('idl_env'); + expect(payload.user.ext.eids[5]['uids'][0]['id']).to.equal('liverampId'); + expect(payload.user.ext.eids[6]['source']).to.equal('lipb'); + expect(payload.user.ext.eids[6]['uids'][0]['id']['lipbid']).to.equal('lipbidId123'); + expect(payload.user.ext.eids[7]['source']).to.equal('parrableId'); + expect(payload.user.ext.eids[7]['uids'][0]['id']['eid']).to.equal('01.5678.parrableid'); + }); + + it('replaces the auction url for a config override', function () { + spec.propertyBag.whitelabel = null; + let fakeOrigin = 'http://sometestendpoint'; + config.setConfig({'ozone': {'endpointOverride': {'origin': fakeOrigin}}}); + const request = spec.buildRequests(validBidRequests, validBidderRequest.bidderRequest); + expect(request.url).to.equal(fakeOrigin + '/openrtb2/auction'); + expect(request.method).to.equal('POST'); + config.setConfig({'ozone': {'kvpPrefix': null, 'endpointOverride': null}}); + spec.propertyBag.whitelabel = null; }); + it('replaces the renderer url for a config override', function () { + spec.propertyBag.whitelabel = null; + let fakeUrl = 'http://renderer.com'; + config.setConfig({'ozone': {'endpointOverride': {'rendererUrl': fakeUrl}}}); + const request = spec.buildRequests(validBidRequests1OutstreamVideo2020, validBidderRequest1OutstreamVideo2020.bidderRequest); + const result = spec.interpretResponse(getCleanValidVideoResponse(), validBidderRequest1OutstreamVideo2020); + const bid = result[0]; + expect(bid.renderer).to.be.an.instanceOf(Renderer); + expect(bid.renderer.url).to.equal(fakeUrl); + config.setConfig({'ozone': {'kvpPrefix': null, 'endpointOverride': null}}); + spec.propertyBag.whitelabel = null; + }); + + it('replaces the kvp prefix ', function () { + spec.propertyBag.whitelabel = null; + config.setConfig({'ozone': {'kvpPrefix': 'test'}}); + const request = spec.buildRequests(validBidRequests, validBidderRequest.bidderRequest); + const data = JSON.parse(request.data); + expect(data.ext.ozone).to.haveOwnProperty('test_rw'); + config.setConfig({'ozone': {'kvpPrefix': null}}); + spec.propertyBag.whitelabel = null; + }); + + it('handles an alias ', function () { + spec.propertyBag.whitelabel = null; + config.setConfig({'lmc': {'kvpPrefix': 'test'}}); + let br = JSON.parse(JSON.stringify(validBidRequests)); + br[0]['bidder'] = 'lmc'; + const request = spec.buildRequests(br, validBidderRequest.bidderRequest); + const data = JSON.parse(request.data); + expect(data.ext.lmc).to.haveOwnProperty('test_rw'); + config.setConfig({'lmc': {'kvpPrefix': null}}); // I cant remove the key so set the value to null + spec.propertyBag.whitelabel = null; + }); + var specMock = utils.deepClone(spec); it('should use oztestmode GET value if set', function() { // mock the getGetParametersAsObject function to simulate GET parameters for oztestmode: - spec.getGetParametersAsObject = function() { + specMock.getGetParametersAsObject = function() { return {'oztestmode': 'mytestvalue_123'}; }; - const request = spec.buildRequests(validBidRequests, validBidderRequest.bidderRequest); + const request = specMock.buildRequests(validBidRequests, validBidderRequest.bidderRequest); const data = JSON.parse(request.data); expect(data.imp[0].ext.ozone.customData).to.be.an('array'); expect(data.imp[0].ext.ozone.customData[0].targeting.oztestmode).to.equal('mytestvalue_123'); }); it('should use oztestmode GET value if set, even if there is no customdata in config', function() { // mock the getGetParametersAsObject function to simulate GET parameters for oztestmode: - spec.getGetParametersAsObject = function() { + specMock.getGetParametersAsObject = function() { return {'oztestmode': 'mytestvalue_123'}; }; - const request = spec.buildRequests(validBidRequestsMinimal, validBidderRequest.bidderRequest); + const request = specMock.buildRequests(validBidRequestsMinimal, validBidderRequest.bidderRequest); const data = JSON.parse(request.data); expect(data.imp[0].ext.ozone.customData).to.be.an('array'); expect(data.imp[0].ext.ozone.customData[0].targeting.oztestmode).to.equal('mytestvalue_123'); }); - var specMock = utils.deepClone(spec); it('should use a valid ozstoredrequest GET value if set to override the placementId values, and set oz_rw if we find it', function() { // mock the getGetParametersAsObject function to simulate GET parameters for ozstoredrequest: specMock.getGetParametersAsObject = function() { @@ -2215,54 +2173,6 @@ describe('ozone Adapter', function () { expect(data.imp[0].ext.prebid.storedrequest.id).to.equal('1310000099'); }); - it('should pick up the value of valid lotame override parameters when there is a lotame object', function () { - spec.getGetParametersAsObject = function() { - return {'oz_lotameid': '123abc', 'oz_lotamepid': 'pid123', 'oz_lotametpid': '123eee'}; - }; - const request = spec.buildRequests(validBidRequests, validBidderRequest.bidderRequest); - const payload = JSON.parse(request.data); - expect(payload.ext.ozone.lotameData.Profile.Audiences.Audience[0].id).to.equal('123abc'); - expect(payload.ext.ozone.oz_lot_rw).to.equal(1); - }); - it('should pick up the value of valid lotame override parameters when there is an empty lotame object', function () { - let nolotameBidReq = JSON.parse(JSON.stringify(validBidRequests)); - nolotameBidReq[0].params.lotameData = {}; - spec.getGetParametersAsObject = function() { - return {'oz_lotameid': '123abc', 'oz_lotamepid': 'pid123', 'oz_lotametpid': '123eeetpid'}; - }; - const request = spec.buildRequests(nolotameBidReq, validBidderRequest.bidderRequest); - const payload = JSON.parse(request.data); - expect(payload.ext.ozone.lotameData.Profile.Audiences.Audience[0].id).to.equal('123abc'); - expect(payload.ext.ozone.lotameData.Profile.tpid).to.equal('123eeetpid'); - expect(payload.ext.ozone.lotameData.Profile.pid).to.equal('pid123'); - expect(payload.ext.ozone.oz_lot_rw).to.equal(1); - }); - it('should pick up the value of valid lotame override parameters when there is NO "lotame" key at all', function () { - let nolotameBidReq = JSON.parse(JSON.stringify(validBidRequests)); - delete (nolotameBidReq[0].params['lotameData']); - spec.getGetParametersAsObject = function() { - return {'oz_lotameid': '123abc', 'oz_lotamepid': 'pid123', 'oz_lotametpid': '123eeetpid'}; - }; - const request = spec.buildRequests(nolotameBidReq, validBidderRequest.bidderRequest); - const payload = JSON.parse(request.data); - expect(payload.ext.ozone.lotameData.Profile.Audiences.Audience[0].id).to.equal('123abc'); - expect(payload.ext.ozone.lotameData.Profile.tpid).to.equal('123eeetpid'); - expect(payload.ext.ozone.lotameData.Profile.pid).to.equal('pid123'); - expect(payload.ext.ozone.oz_lot_rw).to.equal(1); - spec.propertyBag = originalPropertyBag; // tidy up - }); - // NOTE - only one negative test case; - // you can't send invalid lotame params to buildRequests because 'validate' will have rejected them - it('should not use lotame override parameters if they dont exist', function () { - expect(spec.propertyBag.lotameWasOverridden).to.equal(0); - spec.getGetParametersAsObject = function() { - return {}; // no lotame override params - }; - const request = spec.buildRequests(validBidRequests, validBidderRequest.bidderRequest); - const payload = JSON.parse(request.data); - expect(payload.ext.ozone.oz_lot_rw).to.equal(0); - }); - it('should pick up the config value of coppa & set it in the request', function () { config.setConfig({'coppa': true}); const request = spec.buildRequests(validBidRequestsNoSizes, validBidderRequest.bidderRequest); @@ -2609,6 +2519,14 @@ describe('ozone Adapter', function () { const result = playerSizeIsNestedArray(obj); expect(result).to.be.null; }); + it('should add oz_appnexus_dealid into ads request if dealid exists in the auction response', function () { + const request = spec.buildRequests(validBidRequestsMulti, validBidderRequest.bidderRequest); + let validres = JSON.parse(JSON.stringify(validResponse2Bids)); + validres.body.seatbid[0].bid[0].dealid = '1234'; + const result = spec.interpretResponse(validres, request); + expect(utils.deepAccess(result[0].adserverTargeting, 'oz_appnexus_dealid')).to.equal('1234'); + expect(utils.deepAccess(result[1].adserverTargeting, 'oz_appnexus_dealid', '')).to.equal(''); + }); }); describe('default size', function () { @@ -2660,44 +2578,6 @@ describe('ozone Adapter', function () { config.resetConfig(); }); }); - describe('makeLotameObjectFromOverride', function() { - it('should update an object with valid lotame data', function () { - let objLotameOverride = {'oz_lotametpid': '1234', 'oz_lotameid': '12345', 'oz_lotamepid': '123456'}; - let result = spec.makeLotameObjectFromOverride( - objLotameOverride, - {'Profile': {'pid': 'originalpid', 'tpid': 'originaltpid', 'Audiences': {'Audience': [{'id': 'aud1'}]}}} - ); - expect(result.Profile.Audiences.Audience).to.be.an('array'); - expect(result.Profile.Audiences.Audience[0]).to.be.an('object'); - expect(result.Profile.Audiences.Audience[0]).to.deep.include({'id': '12345', 'abbr': '12345'}); - }); - it('should return the original object if it seems weird', function () { - let objLotameOverride = {'oz_lotametpid': '1234', 'oz_lotameid': '12345', 'oz_lotamepid': '123456'}; - let objLotameOriginal = {'Profile': {'pid': 'originalpid', 'tpid': 'originaltpid', 'somethingstrange': [{'id': 'aud1'}]}}; - let result = spec.makeLotameObjectFromOverride( - objLotameOverride, - objLotameOriginal - ); - expect(result).to.equal(objLotameOriginal); - }); - }); - describe('lotameDataIsValid', function() { - it('should allow a valid minimum lotame object', function() { - let obj = {'Profile': {'pid': '', 'tpid': '', 'Audiences': {'Audience': []}}}; - let result = spec.isLotameDataValid(obj); - expect(result).to.be.true; - }); - it('should allow a valid lotame object', function() { - let obj = {'Profile': {'pid': '12345', 'tpid': '45678', 'Audiences': {'Audience': [{'id': '1234', 'abbr': '567'}, {'id': '9999', 'abbr': '1111'}]}}}; - let result = spec.isLotameDataValid(obj); - expect(result).to.be.true; - }); - it('should disallow a lotame object without an Audience.id', function() { - let obj = {'Profile': {'tpid': '', 'pid': '', 'Audiences': {'Audience': [{'abbr': 'marktest'}]}}}; - let result = spec.isLotameDataValid(obj); - expect(result).to.be.false; - }); - }); describe('getPageId', function() { it('should return the same Page ID for multiple calls', function () { let result = spec.getPageId(); @@ -2720,24 +2600,6 @@ describe('ozone Adapter', function () { expect(result).to.equal('outstream'); }); }); - describe('getLotameOverrideParams', function() { - it('should get 3 valid lotame params that exist in GET params', function () { - // mock the getGetParametersAsObject function to simulate GET parameters for lotame overrides: - spec.getGetParametersAsObject = function() { - return {'oz_lotameid': '123abc', 'oz_lotamepid': 'pid123', 'oz_lotametpid': 'tpid123'}; - }; - let result = spec.getLotameOverrideParams(); - expect(Object.keys(result).length).to.equal(3); - }); - it('should get only 1 valid lotame param that exists in GET params', function () { - // mock the getGetParametersAsObject function to simulate GET parameters for lotame overrides: - spec.getGetParametersAsObject = function() { - return {'oz_lotameid': '123abc', 'xoz_lotamepid': 'pid123', 'xoz_lotametpid': 'tpid123'}; - }; - let result = spec.getLotameOverrideParams(); - expect(Object.keys(result).length).to.equal(1); - }); - }); describe('unpackVideoConfigIntoIABformat', function() { it('should correctly unpack a usual video config', function () { let mediaTypes = { diff --git a/test/spec/modules/parrableIdSystem_spec.js b/test/spec/modules/parrableIdSystem_spec.js index 5e62af9b2fa..3f517a66c68 100644 --- a/test/spec/modules/parrableIdSystem_spec.js +++ b/test/spec/modules/parrableIdSystem_spec.js @@ -18,7 +18,7 @@ const P_XHR_EID = '01.1588030911.test-new-eid' const P_CONFIG_MOCK = { name: 'parrableId', params: { - partner: 'parrable_test_partner_123,parrable_test_partner_456' + partners: 'parrable_test_partner_123,parrable_test_partner_456' } }; @@ -74,6 +74,15 @@ function removeParrableCookie() { storage.setCookie(P_COOKIE_NAME, '', EXPIRED_COOKIE_DATE); } +function decodeBase64UrlSafe(encBase64) { + const DEC = { + '-': '+', + '_': '/', + '.': '=' + }; + return encBase64.replace(/[-_.]/g, (m) => DEC[m]); +} + describe('Parrable ID System', function() { describe('parrableIdSystem.getId()', function() { describe('response callback function', function() { @@ -98,7 +107,7 @@ describe('Parrable ID System', function() { let request = server.requests[0]; let queryParams = utils.parseQS(request.url.split('?')[1]); - let data = JSON.parse(atob(queryParams.data)); + let data = JSON.parse(atob(decodeBase64UrlSafe(queryParams.data))); expect(getIdResult.callback).to.be.a('function'); expect(request.url).to.contain('h.parrable.com'); @@ -106,8 +115,10 @@ describe('Parrable ID System', function() { expect(queryParams).to.not.have.property('us_privacy'); expect(data).to.deep.equal({ eid: P_COOKIE_EID, - trackers: P_CONFIG_MOCK.params.partner.split(','), - url: getRefererInfo().referer + trackers: P_CONFIG_MOCK.params.partners.split(','), + url: getRefererInfo().referer, + prebidVersion: '$prebid.version$', + isIframe: true }); server.requests[0].respond(200, @@ -136,9 +147,22 @@ describe('Parrable ID System', function() { expect(server.requests[0].url).to.contain('us_privacy=' + uspString); }); + it('xhr base64 safely encodes url data object', function() { + const urlSafeBase64EncodedData = '-_.'; + const btoaStub = sinon.stub(window, 'btoa').returns('+/='); + let getIdResult = parrableIdSubmodule.getId(P_CONFIG_MOCK); + + getIdResult.callback(callbackSpy); + + let request = server.requests[0]; + let queryParams = utils.parseQS(request.url.split('?')[1]); + expect(queryParams.data).to.equal(urlSafeBase64EncodedData); + btoaStub.restore(); + }); + it('should log an error and continue to callback if ajax request errors', function () { let callBackSpy = sinon.spy(); - let submoduleCallback = parrableIdSubmodule.getId({ params: {partner: 'prebid'} }).callback; + let submoduleCallback = parrableIdSubmodule.getId({ params: {partners: 'prebid'} }).callback; submoduleCallback(callBackSpy); let request = server.requests[0]; expect(request.url).to.contain('h.parrable.com'); @@ -186,6 +210,38 @@ describe('Parrable ID System', function() { removeParrableCookie(); }); }); + + describe('GDPR consent', () => { + let callbackSpy = sinon.spy(); + + const config = { + params: { + partner: 'partner' + } + }; + + const gdprConsentTestCases = [ + { consentData: { gdprApplies: true, consentString: 'expectedConsentString' }, expected: { gdpr: 1, gdpr_consent: 'expectedConsentString' } }, + { consentData: { gdprApplies: false, consentString: 'expectedConsentString' }, expected: { gdpr: 0 } }, + { consentData: { gdprApplies: true, consentString: undefined }, expected: { gdpr: 1, gdpr_consent: '' } }, + { consentData: { gdprApplies: 'yes', consentString: 'expectedConsentString' }, expected: { gdpr: 0 } }, + { consentData: undefined, expected: { gdpr: 0 } } + ]; + + gdprConsentTestCases.forEach((testCase, index) => { + it(`should call user sync url with the gdprConsent - case ${index}`, () => { + parrableIdSubmodule.getId(config, testCase.consentData).callback(callbackSpy); + + if (testCase.expected.gdpr === 1) { + expect(server.requests[0].url).to.contain('gdpr=' + testCase.expected.gdpr); + expect(server.requests[0].url).to.contain('gdpr_consent=' + testCase.expected.gdpr_consent); + } else { + expect(server.requests[0].url).to.contain('gdpr=' + testCase.expected.gdpr); + expect(server.requests[0].url).to.not.contain('gdpr_consent'); + } + }) + }); + }); }); describe('parrableIdSystem.decode()', function() { @@ -213,7 +269,7 @@ describe('Parrable ID System', function() { it('permits an impression when no timezoneFilter is configured', function() { expect(parrableIdSubmodule.getId({ params: { - partner: 'prebid-test', + partners: 'prebid-test', } })).to.have.property('callback'); }); @@ -225,7 +281,7 @@ describe('Parrable ID System', function() { writeParrableCookie({ eid: P_COOKIE_EID }); expect(parrableIdSubmodule.getId({ params: { - partner: 'prebid-test', + partners: 'prebid-test', timezoneFilter: { blockedZones: [ blockedZone ] } @@ -241,7 +297,7 @@ describe('Parrable ID System', function() { Intl.DateTimeFormat.returns({ resolvedOptions }); expect(parrableIdSubmodule.getId({ params: { - partner: 'prebid-test', + partners: 'prebid-test', timezoneFilter: { allowedZones: [ allowedZone ] } @@ -249,13 +305,27 @@ describe('Parrable ID System', function() { expect(resolvedOptions.called).to.equal(true); }); + it('permits an impression from a lower cased allowed timezone', function() { + const allowedZone = 'America/New_York'; + const resolvedOptions = sinon.stub().returns({ timeZone: allowedZone }); + Intl.DateTimeFormat.returns({ resolvedOptions }); + + expect(parrableIdSubmodule.getId({ params: { + partner: 'prebid-test', + timezoneFilter: { + allowedZones: [ allowedZone.toLowerCase() ] + } + } })).to.have.property('callback'); + expect(resolvedOptions.called).to.equal(true); + }); + it('permits an impression from a timezone that is not blocked', function() { const blockedZone = 'America/New_York'; const resolvedOptions = sinon.stub().returns({ timeZone: 'Iceland' }); Intl.DateTimeFormat.returns({ resolvedOptions }); expect(parrableIdSubmodule.getId({ params: { - partner: 'prebid-test', + partners: 'prebid-test', timezoneFilter: { blockedZones: [ blockedZone ] } @@ -269,7 +339,7 @@ describe('Parrable ID System', function() { Intl.DateTimeFormat.returns({ resolvedOptions }); expect(parrableIdSubmodule.getId({ params: { - partner: 'prebid-test', + partners: 'prebid-test', timezoneFilter: { blockedZones: [ blockedZone ] } @@ -277,13 +347,27 @@ describe('Parrable ID System', function() { expect(resolvedOptions.called).to.equal(true); }); + it('does not permit an impression from a lower cased blocked timezone', function() { + const blockedZone = 'America/New_York'; + const resolvedOptions = sinon.stub().returns({ timeZone: blockedZone }); + Intl.DateTimeFormat.returns({ resolvedOptions }); + + expect(parrableIdSubmodule.getId({ params: { + partner: 'prebid-test', + timezoneFilter: { + blockedZones: [ blockedZone.toLowerCase() ] + } + } })).to.equal(null); + expect(resolvedOptions.called).to.equal(true); + }); + it('does not permit an impression from a blocked timezone even when also allowed', function() { const timezone = 'America/New_York'; const resolvedOptions = sinon.stub().returns({ timeZone: timezone }); Intl.DateTimeFormat.returns({ resolvedOptions }); expect(parrableIdSubmodule.getId({ params: { - partner: 'prebid-test', + partners: 'prebid-test', timezoneFilter: { allowedZones: [ timezone ], blockedZones: [ timezone ] @@ -313,7 +397,7 @@ describe('Parrable ID System', function() { writeParrableCookie({ eid: P_COOKIE_EID }); expect(parrableIdSubmodule.getId({ params: { - partner: 'prebid-test', + partners: 'prebid-test', timezoneFilter: { blockedOffsets: [ blockedOffset ] } @@ -327,7 +411,7 @@ describe('Parrable ID System', function() { Date.prototype.getTimezoneOffset.returns(allowedOffset * 60); expect(parrableIdSubmodule.getId({ params: { - partner: 'prebid-test', + partners: 'prebid-test', timezoneFilter: { allowedOffsets: [ allowedOffset ] } @@ -341,7 +425,7 @@ describe('Parrable ID System', function() { Date.prototype.getTimezoneOffset.returns(allowedOffset * 60); expect(parrableIdSubmodule.getId({ params: { - partner: 'prebid-test', + partners: 'prebid-test', timezoneFilter: { blockedOffsets: [ blockedOffset ] } @@ -354,7 +438,7 @@ describe('Parrable ID System', function() { Date.prototype.getTimezoneOffset.returns(blockedOffset * 60); expect(parrableIdSubmodule.getId({ params: { - partner: 'prebid-test', + partners: 'prebid-test', timezoneFilter: { blockedOffsets: [ blockedOffset ] } @@ -367,7 +451,7 @@ describe('Parrable ID System', function() { Date.prototype.getTimezoneOffset.returns(offset * 60); expect(parrableIdSubmodule.getId({ params: { - partner: 'prebid-test', + partners: 'prebid-test', timezoneFilter: { allowedOffset: [ offset ], blockedOffsets: [ offset ] @@ -444,4 +528,47 @@ describe('Parrable ID System', function() { }, { adUnits }); }); }); + + describe('partners parsing', () => { + let callbackSpy = sinon.spy(); + + const partnersTestCase = [ + { + name: '"partners" as an array', + config: { params: { partners: ['parrable_test_partner_123', 'parrable_test_partner_456'] } }, + expected: ['parrable_test_partner_123', 'parrable_test_partner_456'] + }, + { + name: '"partners" as a string list', + config: { params: { partners: 'parrable_test_partner_123,parrable_test_partner_456' } }, + expected: ['parrable_test_partner_123', 'parrable_test_partner_456'] + }, + { + name: '"partners" as a string', + config: { params: { partners: 'parrable_test_partner_123' } }, + expected: ['parrable_test_partner_123'] + }, + { + name: '"partner" as a string list', + config: { params: { partner: 'parrable_test_partner_123,parrable_test_partner_456' } }, + expected: ['parrable_test_partner_123', 'parrable_test_partner_456'] + }, + { + name: '"partner" as string', + config: { params: { partner: 'parrable_test_partner_123' } }, + expected: ['parrable_test_partner_123'] + }, + ]; + partnersTestCase.forEach(testCase => { + it(`accepts config property ${testCase.name}`, () => { + parrableIdSubmodule.getId(testCase.config).callback(callbackSpy); + + let request = server.requests[0]; + let queryParams = utils.parseQS(request.url.split('?')[1]); + let data = JSON.parse(atob(decodeBase64UrlSafe(queryParams.data))); + + expect(data.trackers).to.deep.equal(testCase.expected); + }); + }); + }); }); diff --git a/test/spec/modules/permutiveRtdProvider_spec.js b/test/spec/modules/permutiveRtdProvider_spec.js new file mode 100644 index 00000000000..d55bbc58056 --- /dev/null +++ b/test/spec/modules/permutiveRtdProvider_spec.js @@ -0,0 +1,282 @@ +import { permutiveSubmodule, storage, getSegments, initSegments, isAcEnabled, isPermutiveOnPage } from 'modules/permutiveRtdProvider.js' +import { deepAccess } from '../../../src/utils.js' + +describe('permutiveRtdProvider', function () { + before(function () { + const data = getTargetingData() + setLocalStorage(data) + }) + + after(function () { + const data = getTargetingData() + removeLocalStorage(data) + }) + + describe('permutiveSubmodule', function () { + it('should initalise and return true', function () { + expect(permutiveSubmodule.init()).to.equal(true) + }) + }) + + describe('Getting segments', function () { + it('should retrieve segments in the expected structure', function () { + const data = transformedTargeting() + expect(getSegments(250)).to.deep.equal(data) + }) + it('should enforce max segments', function () { + const max = 1 + const segments = getSegments(max) + + for (const key in segments) { + expect(segments[key]).to.have.length(max) + } + }) + }) + + describe('Default segment targeting', function () { + it('sets segment targeting for Xandr', function () { + const data = transformedTargeting() + const adUnits = getAdUnits() + const config = getConfig() + + initSegments({ adUnits }, callback, config) + + function callback () { + adUnits.forEach(adUnit => { + adUnit.bids.forEach(bid => { + const { bidder, params } = bid + + if (bidder === 'appnexus') { + expect(deepAccess(params, 'keywords.permutive')).to.eql(data.appnexus) + expect(deepAccess(params, 'keywords.p_standard')).to.eql(data.ac) + } + }) + }) + } + }) + it('sets segment targeting for Rubicon', function () { + const data = transformedTargeting() + const adUnits = getAdUnits() + const config = getConfig() + + initSegments({ adUnits }, callback, config) + + function callback () { + adUnits.forEach(adUnit => { + adUnit.bids.forEach(bid => { + const { bidder, params } = bid + + if (bidder === 'rubicon') { + expect(deepAccess(params, 'visitor.permutive')).to.eql(data.rubicon) + expect(deepAccess(params, 'visitor.p_standard')).to.eql(data.ac) + } + }) + }) + } + }) + it('sets segment targeting for Ozone', function () { + const data = transformedTargeting() + const adUnits = getAdUnits() + const config = getConfig() + + initSegments({ adUnits }, callback, config) + + function callback () { + adUnits.forEach(adUnit => { + adUnit.bids.forEach(bid => { + const { bidder, params } = bid + + if (bidder === 'ozone') { + expect(deepAccess(params, 'customData.0.targeting.p_standard')).to.eql(data.ac) + } + }) + }) + } + }) + }) + + describe('Custom segment targeting', function () { + it('sets custom segment targeting for Rubicon', function () { + const data = transformedTargeting() + const adUnits = getAdUnits() + const config = getConfig() + + config.params.overwrites = { + rubicon: function (bid, data, acEnabled, utils, defaultFn) { + if (defaultFn) { + bid = defaultFn(bid, data, acEnabled) + } + if (data.gam && data.gam.length) { + utils.deepSetValue(bid, 'params.visitor.permutive', data.gam) + } + } + } + + initSegments({ adUnits }, callback, config) + + function callback () { + adUnits.forEach(adUnit => { + adUnit.bids.forEach(bid => { + const { bidder, params } = bid + + if (bidder === 'rubicon') { + expect(deepAccess(params, 'visitor.permutive')).to.eql(data.gam) + expect(deepAccess(params, 'visitor.p_standard')).to.eql(data.ac) + } + }) + }) + } + }) + }) + + describe('Permutive on page', function () { + it('checks if Permutive is on page', function () { + expect(isPermutiveOnPage()).to.equal(false) + }) + }) + + describe('AC is enabled', function () { + it('checks if AC is enabled for Xandr', function () { + expect(isAcEnabled({ params: { acBidders: ['appnexus'] } }, 'appnexus')).to.equal(true) + expect(isAcEnabled({ params: { acBidders: ['kjdbfkvb'] } }, 'appnexus')).to.equal(false) + }) + it('checks if AC is enabled for Magnite', function () { + expect(isAcEnabled({ params: { acBidders: ['rubicon'] } }, 'rubicon')).to.equal(true) + expect(isAcEnabled({ params: { acBidders: ['kjdbfkb'] } }, 'rubicon')).to.equal(false) + }) + it('checks if AC is enabled for Ozone', function () { + expect(isAcEnabled({ params: { acBidders: ['ozone'] } }, 'ozone')).to.equal(true) + expect(isAcEnabled({ params: { acBidders: ['kjdvb'] } }, 'ozone')).to.equal(false) + }) + }) +}) + +function setLocalStorage (data) { + for (const key in data) { + storage.setDataInLocalStorage(key, JSON.stringify(data[key])) + } +} + +function removeLocalStorage (data) { + for (const key in data) { + storage.removeDataFromLocalStorage(key) + } +} + +function getConfig () { + return { + name: 'permutive', + waitForIt: true, + params: { + acBidders: ['appnexus', 'rubicon', 'ozone'], + maxSegs: 500 + } + } +} + +function transformedTargeting () { + const data = getTargetingData() + + return { + ac: [...data._pcrprs, ...data._ppam, ...data._psegs.filter(seg => seg >= 1000000)], + appnexus: data._papns, + rubicon: data._prubicons, + gam: data._pdfps + } +} + +function getTargetingData () { + return { + _pdfps: ['gam1', 'gam2'], + _prubicons: ['rubicon1', 'rubicon2'], + _papns: ['appnexus1', 'appnexus2'], + _psegs: ['1234', '1000001', '1000002'], + _ppam: ['ppam1', 'ppam2'], + _pcrprs: ['pcrprs1', 'pcrprs2'] + } +} + +function getAdUnits () { + return [ + { + code: '/19968336/header-bid-tag-0', + mediaTypes: { + banner: { + sizes: [ + [300, 250], + [300, 600] + ] + } + }, + bids: [ + { + bidder: 'appnexus', + params: { + placementId: 13144370, + keywords: { + inline_kvs: ['1'] + } + } + }, + { + bidder: 'rubicon', + params: { + accountId: '9840', + siteId: '123564', + zoneId: '583584', + inventory: { + area: ['home'] + }, + visitor: { + inline_kvs: ['1'] + } + } + }, + { + bidder: 'ozone', + params: { + publisherId: 'OZONEGMG0001', + siteId: '4204204209', + placementId: '0420420500', + customData: [ + { + settings: {}, + targeting: { + inline_kvs: ['1', '2', '3', '4'] + } + } + ], + ozoneData: {} + } + } + ] + }, + { + code: '/19968336/header-bid-tag-1', + mediaTypes: { + banner: { + sizes: [ + [728, 90], + [970, 250] + ] + } + }, + bids: [ + { + bidder: 'appnexus', + params: { + placementId: 13144370 + } + }, + { + bidder: 'ozone', + params: { + publisherId: 'OZONEGMG0001', + siteId: '4204204209', + placementId: '0420420500' + } + } + ] + } + ] +} diff --git a/test/spec/modules/prebidServerBidAdapter_spec.js b/test/spec/modules/prebidServerBidAdapter_spec.js index d069bb74944..8d88a3e0042 100644 --- a/test/spec/modules/prebidServerBidAdapter_spec.js +++ b/test/spec/modules/prebidServerBidAdapter_spec.js @@ -26,6 +26,7 @@ const REQUEST = { 'secure': 0, 'url': '', 'prebid_version': '0.30.0-pre', + 's2sConfig': CONFIG, 'ad_units': [ { 'code': 'div-gpt-ad-1460505748561-0', @@ -80,6 +81,7 @@ const VIDEO_REQUEST = { 'secure': 0, 'url': '', 'prebid_version': '1.4.0-pre', + 's2sConfig': CONFIG, 'ad_units': [ { 'code': 'div-gpt-ad-1460505748561-0', @@ -110,6 +112,7 @@ const OUTSTREAM_VIDEO_REQUEST = { 'secure': 0, 'url': '', 'prebid_version': '1.4.0-pre', + 's2sConfig': CONFIG, 'ad_units': [ { 'code': 'div-gpt-ad-1460505748561-0', @@ -162,7 +165,7 @@ const OUTSTREAM_VIDEO_REQUEST = { } } } - ] + ], }; let BID_REQUESTS; @@ -497,11 +500,9 @@ describe('S2S Adapter', function () { resetSyncedStatus(); }); - it('should not add outstream without renderer', function () { - let ortb2Config = utils.deepClone(CONFIG); - ortb2Config.endpoint = 'https://prebid.adnxs.com/pbs/v1/openrtb2/auction'; + it('should not add outstrean without renderer', function () { + config.setConfig({ s2sConfig: CONFIG }); - config.setConfig({ s2sConfig: ortb2Config }); adapter.callBids(OUTSTREAM_VIDEO_REQUEST, BID_REQUESTS, addBidResponse, done, ajax); const requestBid = JSON.parse(server.requests[0].requestBody); @@ -535,10 +536,7 @@ describe('S2S Adapter', function () { }); it('adds gdpr consent information to ortb2 request depending on presence of module', function () { - let ortb2Config = utils.deepClone(CONFIG); - ortb2Config.endpoint = 'https://prebid.adnxs.com/pbs/v1/openrtb2/auction'; - - let consentConfig = { consentManagement: { cmpApi: 'iab' }, s2sConfig: ortb2Config }; + let consentConfig = { consentManagement: { cmpApi: 'iab' }, s2sConfig: CONFIG }; config.setConfig(consentConfig); let gdprBidRequest = utils.deepClone(BID_REQUESTS); @@ -564,10 +562,7 @@ describe('S2S Adapter', function () { }); it('adds additional consent information to ortb2 request depending on presence of module', function () { - let ortb2Config = utils.deepClone(CONFIG); - ortb2Config.endpoint = 'https://prebid.adnxs.com/pbs/v1/openrtb2/auction'; - - let consentConfig = { consentManagement: { cmpApi: 'iab' }, s2sConfig: ortb2Config }; + let consentConfig = { consentManagement: { cmpApi: 'iab' }, s2sConfig: CONFIG }; config.setConfig(consentConfig); let gdprBidRequest = utils.deepClone(BID_REQUESTS); @@ -608,7 +603,10 @@ describe('S2S Adapter', function () { gdprApplies: true }; - adapter.callBids(REQUEST, gdprBidRequest, addBidResponse, done, ajax); + const s2sBidRequest = utils.deepClone(REQUEST); + s2sBidRequest.s2sConfig = cookieSyncConfig; + + adapter.callBids(s2sBidRequest, gdprBidRequest, addBidResponse, done, ajax); let requestBid = JSON.parse(server.requests[0].requestBody); expect(requestBid.gdpr).is.equal(1); @@ -624,13 +622,16 @@ describe('S2S Adapter', function () { let consentConfig = { consentManagement: { cmpApi: 'iab' }, s2sConfig: cookieSyncConfig }; config.setConfig(consentConfig); + const s2sBidRequest = utils.deepClone(REQUEST); + s2sBidRequest.s2sConfig = cookieSyncConfig; + let gdprBidRequest = utils.deepClone(BID_REQUESTS); gdprBidRequest[0].gdprConsent = { consentString: 'xyz789abcc', gdprApplies: false }; - adapter.callBids(REQUEST, gdprBidRequest, addBidResponse, done, ajax); + adapter.callBids(s2sBidRequest, gdprBidRequest, addBidResponse, done, ajax); let requestBid = JSON.parse(server.requests[0].requestBody); expect(requestBid.gdpr).is.equal(0); @@ -650,7 +651,10 @@ describe('S2S Adapter', function () { gdprApplies: undefined }; - adapter.callBids(REQUEST, gdprBidRequest, addBidResponse, done, ajax); + const s2sBidRequest = utils.deepClone(REQUEST); + s2sBidRequest.s2sConfig = cookieSyncConfig; + + adapter.callBids(s2sBidRequest, gdprBidRequest, addBidResponse, done, ajax); let requestBid = JSON.parse(server.requests[0].requestBody); expect(requestBid.gdpr).is.undefined; @@ -664,9 +668,7 @@ describe('S2S Adapter', function () { }); it('is added to ortb2 request when in bidRequest', function () { - let ortb2Config = utils.deepClone(CONFIG); - ortb2Config.endpoint = 'https://prebid.adnxs.com/pbs/v1/openrtb2/auction'; - config.setConfig({ s2sConfig: ortb2Config }); + config.setConfig({ s2sConfig: CONFIG }); let uspBidRequest = utils.deepClone(BID_REQUESTS); uspBidRequest[0].uspConsent = '1NYN'; @@ -693,7 +695,10 @@ describe('S2S Adapter', function () { let uspBidRequest = utils.deepClone(BID_REQUESTS); uspBidRequest[0].uspConsent = '1YNN'; - adapter.callBids(REQUEST, uspBidRequest, addBidResponse, done, ajax); + const s2sBidRequest = utils.deepClone(REQUEST); + s2sBidRequest.s2sConfig = cookieSyncConfig; + + adapter.callBids(s2sBidRequest, uspBidRequest, addBidResponse, done, ajax); let requestBid = JSON.parse(server.requests[0].requestBody); expect(requestBid.us_privacy).is.equal('1YNN'); @@ -708,9 +713,7 @@ describe('S2S Adapter', function () { }); it('is added to ortb2 request when in bidRequest', function () { - let ortb2Config = utils.deepClone(CONFIG); - ortb2Config.endpoint = 'https://prebid.adnxs.com/pbs/v1/openrtb2/auction'; - config.setConfig({ s2sConfig: ortb2Config }); + config.setConfig({ s2sConfig: CONFIG }); let consentBidRequest = utils.deepClone(BID_REQUESTS); consentBidRequest[0].uspConsent = '1NYN'; @@ -748,7 +751,10 @@ describe('S2S Adapter', function () { gdprApplies: true }; - adapter.callBids(REQUEST, consentBidRequest, addBidResponse, done, ajax); + const s2sBidRequest = utils.deepClone(REQUEST); + s2sBidRequest.s2sConfig = cookieSyncConfig + + adapter.callBids(s2sBidRequest, consentBidRequest, addBidResponse, done, ajax); let requestBid = JSON.parse(server.requests[0].requestBody); expect(requestBid.us_privacy).is.equal('1YNN'); @@ -806,11 +812,8 @@ describe('S2S Adapter', function () { }); it('adds debugging value from storedAuctionResponse to OpenRTB', function () { - const s2sConfig = Object.assign({}, CONFIG, { - endpoint: 'https://prebid.adnxs.com/pbs/v1/openrtb2/auction' - }); const _config = { - s2sConfig: s2sConfig, + s2sConfig: CONFIG, device: { ifa: '6D92078A-8246-4BA4-AE5B-76104861E7DC' }, app: { bundle: 'com.test.app' } }; @@ -826,13 +829,104 @@ describe('S2S Adapter', function () { expect(requestBid.imp[0].ext.prebid.storedauctionresponse.id).to.equal('11111'); }); - it('adds device.w and device.h even if the config lacks a device object', function () { - const s2sConfig = Object.assign({}, CONFIG, { - endpoint: 'https://prebid.adnxs.com/pbs/v1/openrtb2/auction' + describe('price floors module', function () { + function runTest(expectedFloor, expectedCur) { + adapter.callBids(REQUEST, BID_REQUESTS, addBidResponse, done, ajax); + const requestBid = JSON.parse(server.requests[requestCount].requestBody); + expect(requestBid.imp[0].bidfloor).to.equal(expectedFloor); + expect(requestBid.imp[0].bidfloorcur).to.equal(expectedCur); + requestCount += 1; + } + + let getFloorResponse, requestCount; + beforeEach(function () { + getFloorResponse = {}; + requestCount = 0; + }); + + it('should NOT pass bidfloor and bidfloorcur when getFloor not present or returns invalid response', function () { + const _config = { + s2sConfig: CONFIG, + }; + + config.setConfig(_config); + + // if no get floor + runTest(undefined, undefined); + + // if getFloor returns empty object + BID_REQUESTS[0].bids[0].getFloor = () => getFloorResponse; + sinon.spy(BID_REQUESTS[0].bids[0], 'getFloor'); + + runTest(undefined, undefined); + // make sure getFloor was called + expect( + BID_REQUESTS[0].bids[0].getFloor.calledWith({ + currency: 'USD', + }) + ).to.be.true; + + // if getFloor does not return number + getFloorResponse = {currency: 'EUR', floor: 'not a number'}; + runTest(undefined, undefined); + + // if getFloor does not return currency + getFloorResponse = {floor: 1.1}; + runTest(undefined, undefined); + }); + + it('should correctly pass bidfloor and bidfloorcur', function () { + const _config = { + s2sConfig: CONFIG, + }; + + config.setConfig(_config); + + BID_REQUESTS[0].bids[0].getFloor = () => getFloorResponse; + sinon.spy(BID_REQUESTS[0].bids[0], 'getFloor'); + + // returns USD and string floor + getFloorResponse = {currency: 'USD', floor: '1.23'}; + runTest(1.23, 'USD'); + // make sure getFloor was called + expect( + BID_REQUESTS[0].bids[0].getFloor.calledWith({ + currency: 'USD', + }) + ).to.be.true; + + // returns non USD and number floor + getFloorResponse = {currency: 'EUR', floor: 0.85}; + runTest(0.85, 'EUR'); + }); + + it('should correctly pass adServerCurrency when set to getFloor not default', function () { + config.setConfig({ + s2sConfig: CONFIG, + currency: { adServerCurrency: 'JPY' }, + }); + + // we have to start requestCount at 1 because a conversion rates fetch occurs when adServerCur is not USD! + requestCount = 1; + + BID_REQUESTS[0].bids[0].getFloor = () => getFloorResponse; + sinon.spy(BID_REQUESTS[0].bids[0], 'getFloor'); + + // returns USD and string floor + getFloorResponse = {currency: 'JPY', floor: 97.2}; + runTest(97.2, 'JPY'); + // make sure getFloor was called with JPY + expect( + BID_REQUESTS[0].bids[0].getFloor.calledWith({ + currency: 'JPY', + }) + ).to.be.true; }); + }); + it('adds device.w and device.h even if the config lacks a device object', function () { const _config = { - s2sConfig: s2sConfig, + s2sConfig: CONFIG, app: { bundle: 'com.test.app' }, }; @@ -850,12 +944,8 @@ describe('S2S Adapter', function () { }); it('adds native request for OpenRTB', function () { - const s2sConfig = Object.assign({}, CONFIG, { - endpoint: 'https://prebid.adnxs.com/pbs/v1/openrtb2/auction' - }); - const _config = { - s2sConfig: s2sConfig + s2sConfig: CONFIG }; config.setConfig(_config); @@ -909,12 +999,8 @@ describe('S2S Adapter', function () { }); it('adds site if app is not present', function () { - const s2sConfig = Object.assign({}, CONFIG, { - endpoint: 'https://prebid.adnxs.com/pbs/v1/openrtb2/auction' - }); - const _config = { - s2sConfig: s2sConfig, + s2sConfig: CONFIG, site: { publisher: { id: '1234', @@ -949,14 +1035,71 @@ describe('S2S Adapter', function () { }); it('adds appnexus aliases to request', function () { + config.setConfig({ s2sConfig: CONFIG }); + + const aliasBidder = { + bidder: 'brealtime', + params: { placementId: '123456' } + }; + + const request = utils.deepClone(REQUEST); + request.ad_units[0].bids = [aliasBidder]; + + adapter.callBids(request, BID_REQUESTS, addBidResponse, done, ajax); + + const requestBid = JSON.parse(server.requests[0].requestBody); + expect(requestBid.ext).to.haveOwnProperty('prebid'); + expect(requestBid.ext.prebid).to.deep.include({ + aliases: { + brealtime: 'appnexus' + }, + auctiontimestamp: 1510852447530, + targeting: { + includebidderkeys: false, + includewinners: true + } + }); + }); + + it('adds dynamic aliases to request', function () { + config.setConfig({ s2sConfig: CONFIG }); + + const alias = 'foobar'; + const aliasBidder = { + bidder: alias, + params: { placementId: '123456' } + }; + + const request = utils.deepClone(REQUEST); + request.ad_units[0].bids = [aliasBidder]; + + // TODO: stub this + $$PREBID_GLOBAL$$.aliasBidder('appnexus', alias); + adapter.callBids(request, BID_REQUESTS, addBidResponse, done, ajax); + + const requestBid = JSON.parse(server.requests[0].requestBody); + expect(requestBid.ext).to.haveOwnProperty('prebid'); + expect(requestBid.ext.prebid).to.deep.include({ + aliases: { + [alias]: 'appnexus' + }, + auctiontimestamp: 1510852447530, + targeting: { + includebidderkeys: false, + includewinners: true + } + }); + }); + + it('skips pbs alias when skipPbsAliasing is enabled in adapter', function() { const s2sConfig = Object.assign({}, CONFIG, { endpoint: 'https://prebid.adnxs.com/pbs/v1/openrtb2/auction' }); config.setConfig({ s2sConfig: s2sConfig }); const aliasBidder = { - bidder: 'brealtime', - params: { placementId: '123456' } + bidder: 'mediafuse', + params: { aid: 123 } }; const request = utils.deepClone(REQUEST); @@ -968,9 +1111,6 @@ describe('S2S Adapter', function () { expect(requestBid.ext).to.deep.equal({ prebid: { - aliases: { - brealtime: 'appnexus' - }, auctiontimestamp: 1510852447530, targeting: { includebidderkeys: false, @@ -980,32 +1120,29 @@ describe('S2S Adapter', function () { }); }); - it('adds dynamic aliases to request', function () { + it('skips dynamic aliases to request when skipPbsAliasing enabled', function () { const s2sConfig = Object.assign({}, CONFIG, { endpoint: 'https://prebid.adnxs.com/pbs/v1/openrtb2/auction' }); config.setConfig({ s2sConfig: s2sConfig }); - const alias = 'foobar'; + const alias = 'foobar_1'; const aliasBidder = { bidder: alias, - params: { placementId: '123456' } + params: { aid: 123456 } }; const request = utils.deepClone(REQUEST); request.ad_units[0].bids = [aliasBidder]; // TODO: stub this - $$PREBID_GLOBAL$$.aliasBidder('appnexus', alias); + $$PREBID_GLOBAL$$.aliasBidder('appnexus', alias, { skipPbsAliasing: true }); adapter.callBids(request, BID_REQUESTS, addBidResponse, done, ajax); const requestBid = JSON.parse(server.requests[0].requestBody); expect(requestBid.ext).to.deep.equal({ prebid: { - aliases: { - [alias]: 'appnexus' - }, auctiontimestamp: 1510852447530, targeting: { includebidderkeys: false, @@ -1049,10 +1186,13 @@ describe('S2S Adapter', function () { cookieSyncConfig.syncEndpoint = 'https://prebid.adnxs.com/pbs/v1/cookie_sync'; cookieSyncConfig.userSyncLimit = 1; + const s2sBidRequest = utils.deepClone(REQUEST); + s2sBidRequest.s2sConfig = cookieSyncConfig; + config.setConfig({ s2sConfig: cookieSyncConfig }); let bidRequest = utils.deepClone(BID_REQUESTS); - adapter.callBids(REQUEST, bidRequest, addBidResponse, done, ajax); + adapter.callBids(s2sBidRequest, bidRequest, addBidResponse, done, ajax); let requestBid = JSON.parse(server.requests[0].requestBody); expect(requestBid.bidders).to.contain('appnexus').and.to.have.lengthOf(1); @@ -1065,8 +1205,11 @@ describe('S2S Adapter', function () { cookieSyncConfig.syncEndpoint = 'https://prebid.adnxs.com/pbs/v1/cookie_sync'; config.setConfig({ s2sConfig: cookieSyncConfig }); + const s2sBidRequest = utils.deepClone(REQUEST); + s2sBidRequest.s2sConfig = cookieSyncConfig; + let bidRequest = utils.deepClone(BID_REQUESTS); - adapter.callBids(REQUEST, bidRequest, addBidResponse, done, ajax); + adapter.callBids(s2sBidRequest, bidRequest, addBidResponse, done, ajax); let requestBid = JSON.parse(server.requests[0].requestBody); expect(requestBid.bidders).to.contain('appnexus').and.to.have.lengthOf(1); @@ -1077,8 +1220,11 @@ describe('S2S Adapter', function () { config.resetConfig(); config.setConfig({ s2sConfig: cookieSyncConfig }); + const s2sBidRequest2 = utils.deepClone(REQUEST); + s2sBidRequest2.s2sConfig = cookieSyncConfig; + bidRequest = utils.deepClone(BID_REQUESTS); - adapter.callBids(REQUEST, bidRequest, addBidResponse, done, ajax); + adapter.callBids(s2sBidRequest2, bidRequest, addBidResponse, done, ajax); requestBid = JSON.parse(server.requests[0].requestBody); expect(requestBid.bidders).to.contain('appnexus').and.to.have.lengthOf(1); @@ -1088,7 +1234,6 @@ describe('S2S Adapter', function () { it('adds s2sConfig adapterOptions to request for ORTB', function () { const s2sConfig = Object.assign({}, CONFIG, { - endpoint: 'https://prebid.adnxs.com/pbs/v1/openrtb2/auction', adapterOptions: { appnexus: { key: 'value' @@ -1101,8 +1246,11 @@ describe('S2S Adapter', function () { app: { bundle: 'com.test.app' }, }; + const s2sBidRequest = utils.deepClone(REQUEST); + s2sBidRequest.s2sConfig = s2sConfig; + config.setConfig(_config); - adapter.callBids(REQUEST, BID_REQUESTS, addBidResponse, done, ajax); + adapter.callBids(s2sBidRequest, BID_REQUESTS, addBidResponse, done, ajax); const requestBid = JSON.parse(server.requests[0].requestBody); expect(requestBid.imp[0].ext.appnexus).to.haveOwnProperty('key'); expect(requestBid.imp[0].ext.appnexus.key).to.be.equal('value') @@ -1110,7 +1258,6 @@ describe('S2S Adapter', function () { describe('config site value is added to the oRTB request', function () { const s2sConfig = Object.assign({}, CONFIG, { - endpoint: 'https://prebid.adnxs.com/pbs/v1/openrtb2/auction', adapterOptions: { appnexus: { key: 'value' @@ -1122,6 +1269,9 @@ describe('S2S Adapter', function () { ip: '75.97.0.47' }; + const s2sBidRequest = utils.deepClone(REQUEST); + s2sBidRequest.s2sConfig = s2sConfig; + it('and overrides publisher and page', function () { config.setConfig({ s2sConfig: s2sConfig, @@ -1132,7 +1282,8 @@ describe('S2S Adapter', function () { }, device: device }); - adapter.callBids(REQUEST, BID_REQUESTS, addBidResponse, done, ajax); + + adapter.callBids(s2sBidRequest, BID_REQUESTS, addBidResponse, done, ajax); const requestBid = JSON.parse(server.requests[0].requestBody); expect(requestBid.site).to.exist.and.to.be.a('object'); @@ -1150,7 +1301,8 @@ describe('S2S Adapter', function () { }, device: device }); - adapter.callBids(REQUEST, BID_REQUESTS, addBidResponse, done, ajax); + + adapter.callBids(s2sBidRequest, BID_REQUESTS, addBidResponse, done, ajax); const requestBid = JSON.parse(server.requests[0].requestBody); expect(requestBid.site).to.exist.and.to.be.a('object'); @@ -1162,10 +1314,7 @@ describe('S2S Adapter', function () { }); it('when userId is defined on bids, it\'s properties should be copied to user.ext.tpid properties', function () { - let ortb2Config = utils.deepClone(CONFIG); - ortb2Config.endpoint = 'https://prebid.adnxs.com/pbs/v1/openrtb2/auction'; - - let consentConfig = { s2sConfig: ortb2Config }; + let consentConfig = { s2sConfig: CONFIG }; config.setConfig(consentConfig); let userIdBidRequest = utils.deepClone(BID_REQUESTS); @@ -1207,17 +1356,14 @@ describe('S2S Adapter', function () { expect(requestBid.user.ext.eids.filter(eid => eid.source === 'liveintent.com')[0].ext.segments[1]).is.equal('segB'); expect(requestBid.user.ext.eids.filter(eid => eid.source === 'id5-sync.com')).is.not.empty; expect(requestBid.user.ext.eids.filter(eid => eid.source === 'id5-sync.com')[0].uids[0].id).is.equal('11111'); - expect(requestBid.user.ext.eids.filter(eid => eid.source === 'id5-sync.com')[0].ext.linkType).is.equal('some-link-type'); + expect(requestBid.user.ext.eids.filter(eid => eid.source === 'id5-sync.com')[0].uids[0].ext.linkType).is.equal('some-link-type'); // LiveRamp should exist expect(requestBid.user.ext.eids.filter(eid => eid.source === 'liveramp.com')[0].uids[0].id).is.equal('0000-1111-2222-3333'); }); it('when config \'currency.adServerCurrency\' value is an array: ORTB has property \'cur\' value set to a single item array', function () { - let s2sConfig = utils.deepClone(CONFIG); - s2sConfig.endpoint = 'https://prebid.adnxs.com/pbs/v1/openrtb2/auction'; config.setConfig({ currency: { adServerCurrency: ['USD', 'GB', 'UK', 'AU'] }, - s2sConfig: s2sConfig }); const bidRequests = utils.deepClone(BID_REQUESTS); @@ -1228,11 +1374,8 @@ describe('S2S Adapter', function () { }); it('when config \'currency.adServerCurrency\' value is a string: ORTB has property \'cur\' value set to a single item array', function () { - let s2sConfig = utils.deepClone(CONFIG); - s2sConfig.endpoint = 'https://prebid.adnxs.com/pbs/v1/openrtb2/auction'; config.setConfig({ currency: { adServerCurrency: 'NZ' }, - s2sConfig: s2sConfig }); const bidRequests = utils.deepClone(BID_REQUESTS); @@ -1243,9 +1386,7 @@ describe('S2S Adapter', function () { }); it('when config \'currency.adServerCurrency\' is unset: ORTB should not define a \'cur\' property', function () { - let s2sConfig = utils.deepClone(CONFIG); - s2sConfig.endpoint = 'https://prebid.adnxs.com/pbs/v1/openrtb2/auction'; - config.setConfig({ s2sConfig: s2sConfig }); + config.setConfig({ s2sConfig: CONFIG }); const bidRequests = utils.deepClone(BID_REQUESTS); adapter.callBids(REQUEST, bidRequests, addBidResponse, done, ajax); @@ -1256,7 +1397,6 @@ describe('S2S Adapter', function () { it('always add ext.prebid.targeting.includebidderkeys: false for ORTB', function () { const s2sConfig = Object.assign({}, CONFIG, { - endpoint: 'https://prebid.adnxs.com/pbs/v1/openrtb2/auction', adapterOptions: { appnexus: { key: 'value' @@ -1270,7 +1410,11 @@ describe('S2S Adapter', function () { }; config.setConfig(_config); - adapter.callBids(REQUEST, BID_REQUESTS, addBidResponse, done, ajax); + + const s2sBidRequest = utils.deepClone(REQUEST); + s2sBidRequest.s2sConfig = s2sConfig; + + adapter.callBids(s2sBidRequest, BID_REQUESTS, addBidResponse, done, ajax); const requestBid = JSON.parse(server.requests[0].requestBody); expect(requestBid.ext.prebid.targeting).to.haveOwnProperty('includebidderkeys'); @@ -1279,7 +1423,6 @@ describe('S2S Adapter', function () { it('always add ext.prebid.targeting.includewinners: true for ORTB', function () { const s2sConfig = Object.assign({}, CONFIG, { - endpoint: 'https://prebid.adnxs.com/pbs/v1/openrtb2/auction', adapterOptions: { appnexus: { key: 'value' @@ -1291,9 +1434,12 @@ describe('S2S Adapter', function () { device: { ifa: '6D92078A-8246-4BA4-AE5B-76104861E7DC' }, app: { bundle: 'com.test.app' }, }; - config.setConfig(_config); - adapter.callBids(REQUEST, BID_REQUESTS, addBidResponse, done, ajax); + + const s2sBidRequest = utils.deepClone(REQUEST); + s2sBidRequest.s2sConfig = s2sConfig; + + adapter.callBids(s2sBidRequest, BID_REQUESTS, addBidResponse, done, ajax); const requestBid = JSON.parse(server.requests[0].requestBody); expect(requestBid.ext.prebid.targeting).to.haveOwnProperty('includewinners'); @@ -1302,7 +1448,6 @@ describe('S2S Adapter', function () { it('adds s2sConfig video.ext.prebid to request for ORTB', function () { const s2sConfig = Object.assign({}, CONFIG, { - endpoint: 'https://prebid.adnxs.com/pbs/v1/openrtb2/auction', extPrebid: { foo: 'bar' } @@ -1313,13 +1458,16 @@ describe('S2S Adapter', function () { app: { bundle: 'com.test.app' }, }; + const s2sBidRequest = utils.deepClone(REQUEST); + s2sBidRequest.s2sConfig = s2sConfig; + config.setConfig(_config); - adapter.callBids(REQUEST, BID_REQUESTS, addBidResponse, done, ajax); + adapter.callBids(s2sBidRequest, BID_REQUESTS, addBidResponse, done, ajax); const requestBid = JSON.parse(server.requests[0].requestBody); expect(requestBid).to.haveOwnProperty('ext'); expect(requestBid.ext).to.haveOwnProperty('prebid'); - expect(requestBid.ext.prebid).to.deep.equal({ + expect(requestBid.ext.prebid).to.deep.include({ auctiontimestamp: 1510852447530, foo: 'bar', targeting: { @@ -1331,7 +1479,6 @@ describe('S2S Adapter', function () { it('overrides request.ext.prebid properties using s2sConfig video.ext.prebid values for ORTB', function () { const s2sConfig = Object.assign({}, CONFIG, { - endpoint: 'https://prebid.adnxs.com/pbs/v1/openrtb2/auction', extPrebid: { targeting: { includewinners: false, @@ -1345,13 +1492,16 @@ describe('S2S Adapter', function () { app: { bundle: 'com.test.app' }, }; + const s2sBidRequest = utils.deepClone(REQUEST); + s2sBidRequest.s2sConfig = s2sConfig; + config.setConfig(_config); - adapter.callBids(REQUEST, BID_REQUESTS, addBidResponse, done, ajax); + adapter.callBids(s2sBidRequest, BID_REQUESTS, addBidResponse, done, ajax); const requestBid = JSON.parse(server.requests[0].requestBody); expect(requestBid).to.haveOwnProperty('ext'); expect(requestBid.ext).to.haveOwnProperty('prebid'); - expect(requestBid.ext.prebid).to.deep.equal({ + expect(requestBid.ext.prebid).to.deep.include({ auctiontimestamp: 1510852447530, targeting: { includewinners: false, @@ -1362,7 +1512,6 @@ describe('S2S Adapter', function () { it('overrides request.ext.prebid properties using s2sConfig video.ext.prebid values for ORTB', function () { const s2sConfig = Object.assign({}, CONFIG, { - endpoint: 'https://prebid.adnxs.com/pbs/v1/openrtb2/auction', extPrebid: { cache: { vastxml: 'vastxml-set-though-extPrebid.cache.vastXml' @@ -1379,13 +1528,16 @@ describe('S2S Adapter', function () { app: { bundle: 'com.test.app' }, }; + const s2sBidRequest = utils.deepClone(REQUEST); + s2sBidRequest.s2sConfig = s2sConfig; + config.setConfig(_config); - adapter.callBids(REQUEST, BID_REQUESTS, addBidResponse, done, ajax); + adapter.callBids(s2sBidRequest, BID_REQUESTS, addBidResponse, done, ajax); const requestBid = JSON.parse(server.requests[0].requestBody); expect(requestBid).to.haveOwnProperty('ext'); expect(requestBid.ext).to.haveOwnProperty('prebid'); - expect(requestBid.ext.prebid).to.deep.equal({ + expect(requestBid.ext.prebid).to.deep.include({ auctiontimestamp: 1510852447530, cache: { vastxml: 'vastxml-set-though-extPrebid.cache.vastXml' @@ -1468,9 +1620,7 @@ describe('S2S Adapter', function () { describe('pbAdSlot config', function () { it('should not send \"imp.ext.context.data.pbadslot\" if \"fpd.context\" is undefined', function () { - const ortb2Config = utils.deepClone(CONFIG); - ortb2Config.endpoint = 'https://prebid.adnxs.com/pbs/v1/openrtb2/auction'; - const consentConfig = { s2sConfig: ortb2Config }; + const consentConfig = { s2sConfig: CONFIG }; config.setConfig(consentConfig); const bidRequest = utils.deepClone(REQUEST); @@ -1483,9 +1633,7 @@ describe('S2S Adapter', function () { }); it('should not send \"imp.ext.context.data.pbadslot\" if \"fpd.context.pbAdSlot\" is undefined', function () { - const ortb2Config = utils.deepClone(CONFIG); - ortb2Config.endpoint = 'https://prebid.adnxs.com/pbs/v1/openrtb2/auction'; - const consentConfig = { s2sConfig: ortb2Config }; + const consentConfig = { s2sConfig: CONFIG }; config.setConfig(consentConfig); const bidRequest = utils.deepClone(REQUEST); bidRequest.ad_units[0].fpd = {}; @@ -1499,9 +1647,7 @@ describe('S2S Adapter', function () { }); it('should not send \"imp.ext.context.data.pbadslot\" if \"fpd.context.pbAdSlot\" is empty string', function () { - const ortb2Config = utils.deepClone(CONFIG); - ortb2Config.endpoint = 'https://prebid.adnxs.com/pbs/v1/openrtb2/auction'; - const consentConfig = { s2sConfig: ortb2Config }; + const consentConfig = { s2sConfig: CONFIG }; config.setConfig(consentConfig); const bidRequest = utils.deepClone(REQUEST); bidRequest.ad_units[0].fpd = { @@ -1519,9 +1665,7 @@ describe('S2S Adapter', function () { }); it('should send \"imp.ext.context.data.pbadslot\" if \"fpd.context.pbAdSlot\" value is a non-empty string', function () { - const ortb2Config = utils.deepClone(CONFIG); - ortb2Config.endpoint = 'https://prebid.adnxs.com/pbs/v1/openrtb2/auction'; - const consentConfig = { s2sConfig: ortb2Config }; + const consentConfig = { s2sConfig: CONFIG }; config.setConfig(consentConfig); const bidRequest = utils.deepClone(REQUEST); bidRequest.ad_units[0].fpd = { @@ -1542,9 +1686,7 @@ describe('S2S Adapter', function () { describe('GAM ad unit config', function () { it('should not send \"imp.ext.context.data.adserver.adslot\" if \"fpd.context\" is undefined', function () { - const ortb2Config = utils.deepClone(CONFIG); - ortb2Config.endpoint = 'https://prebid.adnxs.com/pbs/v1/openrtb2/auction'; - const consentConfig = { s2sConfig: ortb2Config }; + const consentConfig = { s2sConfig: CONFIG }; config.setConfig(consentConfig); const bidRequest = utils.deepClone(REQUEST); @@ -1557,9 +1699,7 @@ describe('S2S Adapter', function () { }); it('should not send \"imp.ext.context.data.adserver.adslot\" if \"fpd.context.adserver.adSlot\" is undefined', function () { - const ortb2Config = utils.deepClone(CONFIG); - ortb2Config.endpoint = 'https://prebid.adnxs.com/pbs/v1/openrtb2/auction'; - const consentConfig = { s2sConfig: ortb2Config }; + const consentConfig = { s2sConfig: CONFIG }; config.setConfig(consentConfig); const bidRequest = utils.deepClone(REQUEST); bidRequest.ad_units[0].fpd = {}; @@ -1573,9 +1713,7 @@ describe('S2S Adapter', function () { }); it('should not send \"imp.ext.context.data.adserver.adslot\" if \"fpd.context.adserver.adSlot\" is empty string', function () { - const ortb2Config = utils.deepClone(CONFIG); - ortb2Config.endpoint = 'https://prebid.adnxs.com/pbs/v1/openrtb2/auction'; - const consentConfig = { s2sConfig: ortb2Config }; + const consentConfig = { s2sConfig: CONFIG }; config.setConfig(consentConfig); const bidRequest = utils.deepClone(REQUEST); bidRequest.ad_units[0].fpd = { @@ -1595,9 +1733,7 @@ describe('S2S Adapter', function () { }); it('should send both \"adslot\" and \"name\" from \"imp.ext.context.data.adserver\" if \"fpd.context.adserver.adSlot\" and \"fpd.context.adserver.name\" values are non-empty strings', function () { - const ortb2Config = utils.deepClone(CONFIG); - ortb2Config.endpoint = 'https://prebid.adnxs.com/pbs/v1/openrtb2/auction'; - const consentConfig = { s2sConfig: ortb2Config }; + const consentConfig = { s2sConfig: CONFIG }; config.setConfig(consentConfig); const bidRequest = utils.deepClone(REQUEST); bidRequest.ad_units[0].fpd = { @@ -1770,10 +1906,7 @@ describe('S2S Adapter', function () { }; sinon.stub(adapterManager, 'getBidAdapter').returns(rubiconAdapter); - const s2sConfig = Object.assign({}, CONFIG, { - endpoint: 'https://prebid.adnxs.com/pbs/v1/openrtb2/auction' - }); - config.setConfig({ s2sConfig }); + config.setConfig({ CONFIG }); adapter.callBids(REQUEST, BID_REQUESTS, addBidResponse, done, ajax); server.requests[0].respond(200, {}, JSON.stringify(RESPONSE_OPENRTB)); @@ -1784,10 +1917,7 @@ describe('S2S Adapter', function () { }); it('handles OpenRTB responses and call BIDDER_DONE', function () { - const s2sConfig = Object.assign({}, CONFIG, { - endpoint: 'https://prebid.adnxs.com/pbs/v1/openrtb2/auction' - }); - config.setConfig({ s2sConfig }); + config.setConfig({ CONFIG }); adapter.callBids(REQUEST, BID_REQUESTS, addBidResponse, done, ajax); server.requests[0].respond(200, {}, JSON.stringify(RESPONSE_OPENRTB)); @@ -1813,12 +1943,13 @@ describe('S2S Adapter', function () { it('respects defaultTtl', function () { const s2sConfig = Object.assign({}, CONFIG, { - endpoint: 'https://prebid.adnxs.com/pbs/v1/openrtb2/auction', defaultTtl: 30 }); - config.setConfig({ s2sConfig }); - adapter.callBids(REQUEST, BID_REQUESTS, addBidResponse, done, ajax); + const s2sBidRequest = utils.deepClone(REQUEST); + s2sBidRequest.s2sConfig = s2sConfig; + + adapter.callBids(s2sBidRequest, BID_REQUESTS, addBidResponse, done, ajax); server.requests[0].respond(200, {}, JSON.stringify(RESPONSE_OPENRTB)); sinon.assert.calledOnce(events.emit); @@ -1834,7 +1965,10 @@ describe('S2S Adapter', function () { }); config.setConfig({ s2sConfig }); - adapter.callBids(VIDEO_REQUEST, BID_REQUESTS, addBidResponse, done, ajax); + const s2sVidRequest = utils.deepClone(VIDEO_REQUEST); + s2sVidRequest.s2sConfig = s2sConfig; + + adapter.callBids(s2sVidRequest, BID_REQUESTS, addBidResponse, done, ajax); server.requests[0].respond(200, {}, JSON.stringify(RESPONSE_OPENRTB_VIDEO)); sinon.assert.calledOnce(addBidResponse); @@ -1862,7 +1996,10 @@ describe('S2S Adapter', function () { } }); - adapter.callBids(VIDEO_REQUEST, BID_REQUESTS, addBidResponse, done, ajax); + const s2sVidRequest = utils.deepClone(VIDEO_REQUEST); + s2sVidRequest.s2sConfig = s2sConfig; + + adapter.callBids(s2sVidRequest, BID_REQUESTS, addBidResponse, done, ajax); server.requests[0].respond(200, {}, JSON.stringify(cacheResponse)); sinon.assert.calledOnce(addBidResponse); @@ -1887,7 +2024,11 @@ describe('S2S Adapter', function () { cacheResponse.seatbid.forEach(item => { item.bid[0].ext.prebid.targeting = targetingTestData }); - adapter.callBids(VIDEO_REQUEST, BID_REQUESTS, addBidResponse, done, ajax); + + const s2sVidRequest = utils.deepClone(VIDEO_REQUEST); + s2sVidRequest.s2sConfig = s2sConfig; + + adapter.callBids(s2sVidRequest, BID_REQUESTS, addBidResponse, done, ajax); server.requests[0].respond(200, {}, JSON.stringify(cacheResponse)); sinon.assert.calledOnce(addBidResponse); @@ -1913,7 +2054,11 @@ describe('S2S Adapter', function () { hb_cache_path: '/cache' } }); - adapter.callBids(VIDEO_REQUEST, BID_REQUESTS, addBidResponse, done, ajax); + + const s2sVidRequest = utils.deepClone(VIDEO_REQUEST); + s2sVidRequest.s2sConfig = s2sConfig; + + adapter.callBids(s2sVidRequest, BID_REQUESTS, addBidResponse, done, ajax); server.requests[0].respond(200, {}, JSON.stringify(cacheResponse)); sinon.assert.calledOnce(addBidResponse); @@ -1940,7 +2085,10 @@ describe('S2S Adapter', function () { hb_cache_path: '/cache' } }); - adapter.callBids(VIDEO_REQUEST, BID_REQUESTS, addBidResponse, done, ajax); + const s2sVidRequest = utils.deepClone(VIDEO_REQUEST); + s2sVidRequest.s2sConfig = s2sConfig; + + adapter.callBids(s2sVidRequest, BID_REQUESTS, addBidResponse, done, ajax); server.requests[0].respond(200, {}, JSON.stringify(cacheResponse)); sinon.assert.calledOnce(addBidResponse); @@ -1966,7 +2114,11 @@ describe('S2S Adapter', function () { hb_bidid: '1234567890', } }); - adapter.callBids(VIDEO_REQUEST, BID_REQUESTS, addBidResponse, done, ajax); + + const s2sVidRequest = utils.deepClone(VIDEO_REQUEST); + s2sVidRequest.s2sConfig = s2sConfig; + + adapter.callBids(s2sVidRequest, BID_REQUESTS, addBidResponse, done, ajax); server.requests[0].respond(200, {}, JSON.stringify(cacheResponse)); sinon.assert.calledOnce(addBidResponse); @@ -1986,7 +2138,10 @@ describe('S2S Adapter', function () { config.setConfig({ s2sConfig }); const cacheResponse = utils.deepClone(RESPONSE_OPENRTB_VIDEO); - adapter.callBids(VIDEO_REQUEST, BID_REQUESTS, addBidResponse, done, ajax); + const s2sVidRequest = utils.deepClone(VIDEO_REQUEST); + s2sVidRequest.s2sConfig = s2sConfig; + + adapter.callBids(s2sVidRequest, BID_REQUESTS, addBidResponse, done, ajax); server.requests[0].respond(200, {}, JSON.stringify(cacheResponse)); sinon.assert.calledOnce(addBidResponse); @@ -2006,7 +2161,10 @@ describe('S2S Adapter', function () { }); config.setConfig({ s2sConfig }); - adapter.callBids(REQUEST, BID_REQUESTS, addBidResponse, done, ajax); + const s2sBidRequest = utils.deepClone(REQUEST); + s2sBidRequest.s2sConfig = s2sConfig; + + adapter.callBids(s2sBidRequest, BID_REQUESTS, addBidResponse, done, ajax); server.requests[0].respond(200, {}, JSON.stringify(RESPONSE_OPENRTB_NATIVE)); sinon.assert.calledOnce(addBidResponse); @@ -2307,10 +2465,11 @@ describe('S2S Adapter', function () { s2sConfig.syncEndpoint = 'https://prebid.adnxs.com/pbs/v1/cookie_sync'; s2sConfig.bidders = ['appnexus', 'rubicon-alias']; - const request = utils.deepClone(REQUEST); + const s2sBidRequest = utils.deepClone(REQUEST); + s2sBidRequest.s2sConfig = s2sConfig; // Add another bidder, `rubicon-alias` - request.ad_units[0].bids.push({ + s2sBidRequest.ad_units[0].bids.push({ bidder: 'rubicon-alias', params: { accoundId: 14062, @@ -2322,8 +2481,6 @@ describe('S2S Adapter', function () { // create an alias for the Rubicon Bid Adapter adapterManager.aliasBidAdapter('rubicon', 'rubicon-alias'); - config.setConfig({ s2sConfig }); - const bidRequest = utils.deepClone(BID_REQUESTS); bidRequest.push({ 'bidderCode': 'rubicon-alias', @@ -2367,7 +2524,7 @@ describe('S2S Adapter', function () { 'src': 's2s' }); - adapter.callBids(request, bidRequest, addBidResponse, done, ajax); + adapter.callBids(s2sBidRequest, bidRequest, addBidResponse, done, ajax); const requestBid = JSON.parse(server.requests[0].requestBody); expect(requestBid.bidders).to.deep.equal(['appnexus', 'rubicon']); diff --git a/test/spec/modules/priceFloors_spec.js b/test/spec/modules/priceFloors_spec.js index 8c673d29701..548d2789d3e 100644 --- a/test/spec/modules/priceFloors_spec.js +++ b/test/spec/modules/priceFloors_spec.js @@ -23,6 +23,8 @@ describe('the price floors module', function () { let clock; const basicFloorData = { modelVersion: 'basic model', + modelWeight: 10, + modelTimestamp: 1606772895, currency: 'USD', schema: { delimiter: '|', @@ -37,6 +39,7 @@ describe('the price floors module', function () { const basicFloorDataHigh = { floorMin: 7.0, modelVersion: 'basic model', + modelWeight: 10, currency: 'USD', schema: { delimiter: '|', @@ -51,6 +54,7 @@ describe('the price floors module', function () { const basicFloorDataLow = { floorMin: 2.3, modelVersion: 'basic model', + modelWeight: 10, currency: 'USD', schema: { delimiter: '|', @@ -184,6 +188,8 @@ describe('the price floors module', function () { let resultingData = getFloorsDataForAuction(inputFloorData, 'test_div_1'); expect(resultingData).to.deep.equal({ modelVersion: 'basic model', + modelWeight: 10, + modelTimestamp: 1606772895, currency: 'USD', schema: { delimiter: '|', @@ -201,6 +207,8 @@ describe('the price floors module', function () { resultingData = getFloorsDataForAuction(inputFloorData, 'this_is_a_div'); expect(resultingData).to.deep.equal({ modelVersion: 'basic model', + modelWeight: 10, + modelTimestamp: 1606772895, currency: 'USD', schema: { delimiter: '^', @@ -429,6 +437,8 @@ describe('the price floors module', function () { skipped: true, floorMin: undefined, modelVersion: undefined, + modelWeight: undefined, + modelTimestamp: undefined, location: 'noData', skipRate: 0, fetchStatus: undefined, @@ -463,6 +473,8 @@ describe('the price floors module', function () { skipped: false, floorMin: undefined, modelVersion: 'adUnit Model Version', + modelWeight: 10, + modelTimestamp: 1606772895, location: 'adUnit', skipRate: 0, fetchStatus: undefined, @@ -496,6 +508,8 @@ describe('the price floors module', function () { validateBidRequests(true, { skipped: false, modelVersion: 'adUnit Model Version', + modelWeight: 10, + modelTimestamp: 1606772895, location: 'adUnit', skipRate: 0, floorMin: 7, @@ -510,6 +524,8 @@ describe('the price floors module', function () { skipped: false, floorMin: undefined, modelVersion: 'basic model', + modelWeight: 10, + modelTimestamp: 1606772895, location: 'setConfig', skipRate: 0, fetchStatus: undefined, @@ -531,6 +547,8 @@ describe('the price floors module', function () { skipped: false, floorMin: undefined, modelVersion: 'basic model', + modelWeight: 10, + modelTimestamp: 1606772895, location: 'setConfig', skipRate: 0, fetchStatus: undefined, @@ -545,6 +563,8 @@ describe('the price floors module', function () { skipped: false, floorMin: undefined, modelVersion: 'basic model', + modelWeight: 10, + modelTimestamp: 1606772895, location: 'setConfig', skipRate: 0, fetchStatus: undefined, @@ -559,6 +579,8 @@ describe('the price floors module', function () { skipped: false, floorMin: undefined, modelVersion: 'basic model', + modelWeight: 10, + modelTimestamp: 1606772895, location: 'setConfig', skipRate: 0, fetchStatus: undefined, @@ -582,6 +604,8 @@ describe('the price floors module', function () { skipped: false, floorMin: undefined, modelVersion: 'basic model', + modelWeight: 10, + modelTimestamp: 1606772895, location: 'setConfig', skipRate: 50, fetchStatus: undefined, @@ -596,6 +620,8 @@ describe('the price floors module', function () { skipped: false, floorMin: undefined, modelVersion: 'basic model', + modelWeight: 10, + modelTimestamp: 1606772895, location: 'setConfig', skipRate: 10, fetchStatus: undefined, @@ -610,6 +636,8 @@ describe('the price floors module', function () { skipped: false, floorMin: undefined, modelVersion: 'basic model', + modelWeight: 10, + modelTimestamp: 1606772895, location: 'setConfig', skipRate: 0, fetchStatus: undefined, @@ -674,6 +702,8 @@ describe('the price floors module', function () { skipped: false, floorMin: undefined, modelVersion: 'model-1', + modelWeight: 10, + modelTimestamp: undefined, location: 'setConfig', skipRate: 0, fetchStatus: undefined, @@ -687,6 +717,8 @@ describe('the price floors module', function () { skipped: false, floorMin: undefined, modelVersion: 'model-2', + modelWeight: 40, + modelTimestamp: undefined, location: 'setConfig', skipRate: 0, fetchStatus: undefined, @@ -700,6 +732,8 @@ describe('the price floors module', function () { skipped: false, floorMin: undefined, modelVersion: 'model-3', + modelWeight: 50, + modelTimestamp: undefined, location: 'setConfig', skipRate: 0, fetchStatus: undefined, @@ -729,6 +763,8 @@ describe('the price floors module', function () { skipped: false, floorMin: undefined, modelVersion: 'basic model', + modelWeight: 10, + modelTimestamp: 1606772895, location: 'setConfig', skipRate: 0, fetchStatus: undefined, @@ -808,6 +844,8 @@ describe('the price floors module', function () { skipped: false, floorMin: undefined, modelVersion: 'basic model', + modelWeight: 10, + modelTimestamp: 1606772895, location: 'setConfig', skipRate: 0, fetchStatus: 'timeout', @@ -846,6 +884,8 @@ describe('the price floors module', function () { skipped: false, floorMin: undefined, modelVersion: 'fetch model name', + modelWeight: 10, + modelTimestamp: 1606772895, location: 'fetch', skipRate: 0, fetchStatus: 'success', @@ -883,6 +923,8 @@ describe('the price floors module', function () { skipped: false, floorMin: undefined, modelVersion: 'fetch model name', + modelWeight: 10, + modelTimestamp: 1606772895, location: 'fetch', skipRate: 0, fetchStatus: 'success', @@ -923,6 +965,8 @@ describe('the price floors module', function () { skipped: false, floorMin: undefined, modelVersion: 'fetch model name', + modelWeight: 10, + modelTimestamp: 1606772895, location: 'fetch', skipRate: 95, fetchStatus: 'success', @@ -945,6 +989,8 @@ describe('the price floors module', function () { skipped: false, floorMin: undefined, modelVersion: 'basic model', + modelWeight: 10, + modelTimestamp: 1606772895, location: 'setConfig', skipRate: 0, fetchStatus: 'error', @@ -969,6 +1015,8 @@ describe('the price floors module', function () { skipped: false, floorMin: undefined, modelVersion: 'basic model', + modelWeight: 10, + modelTimestamp: 1606772895, location: 'setConfig', skipRate: 0, fetchStatus: 'success', @@ -1268,6 +1316,41 @@ describe('the price floors module', function () { floor: 1.3334 // 1.3334 * 0.75 = 1.000005 which is the floor (we cut off getFloor at 4 decimal points) }); }); + + it('should use standard cpmAdjustment if no bidder cpmAdjustment', function () { + getGlobal().bidderSettings = { + rubicon: { + bidCpmAdjustment: function (bidCpm, bidResponse) { + return bidResponse.cpm * 0.5; + }, + }, + standard: { + bidCpmAdjustment: function (bidCpm, bidResponse) { + return bidResponse.cpm * 0.75; + }, + } + }; + _floorDataForAuction[bidRequest.auctionId] = utils.deepClone(basicFloorConfig); + _floorDataForAuction[bidRequest.auctionId].data.values = { '*': 1.0 }; + let appnexusBid = { + ...bidRequest, + bidder: 'appnexus' + }; + + // the conversion should be what the bidder would need to return in order to match the actual floor + // rubicon + expect(bidRequest.getFloor()).to.deep.equal({ + currency: 'USD', + floor: 2.0 // a 2.0 bid after rubicons cpm adjustment would be 1.0 and thus is the floor after adjust + }); + + // appnexus + expect(appnexusBid.getFloor()).to.deep.equal({ + currency: 'USD', + floor: 1.3334 // 1.3334 * 0.75 = 1.000005 which is the floor (we cut off getFloor at 4 decimal points) + }); + }); + it('should work when cpmAdjust function uses bid object', function () { getGlobal().bidderSettings = { rubicon: { diff --git a/test/spec/modules/pubgeniusBidAdapter_spec.js b/test/spec/modules/pubgeniusBidAdapter_spec.js index c0b05510707..382199dcffc 100644 --- a/test/spec/modules/pubgeniusBidAdapter_spec.js +++ b/test/spec/modules/pubgeniusBidAdapter_spec.js @@ -472,7 +472,6 @@ describe('pubGENIUS adapter', () => { ...expectedBidResponse, vastUrl: 'http://vasturl/cache?id=x', vastXml: 'fake_creative', - videoCacheKey: 'x', mediaType: VIDEO, }; diff --git a/test/spec/modules/pubwiseBidAdapter_spec.js b/test/spec/modules/pubwiseBidAdapter_spec.js new file mode 100644 index 00000000000..450b028f6c7 --- /dev/null +++ b/test/spec/modules/pubwiseBidAdapter_spec.js @@ -0,0 +1,575 @@ +// import or require modules necessary for the test, e.g.: + +import {expect} from 'chai'; +import {spec} from 'modules/pubwiseBidAdapter.js'; +import {_checkMediaType} from 'modules/pubwiseBidAdapter.js'; // this is exported only for testing so maintaining the JS convention of _ to indicate the intent +import {_parseAdSlot} from 'modules/pubwiseBidAdapter.js'; // this is exported only for testing so maintaining the JS convention of _ to indicate the intent +import * as utils from 'src/utils.js'; + +const sampleRequestBanner = { + 'id': '6c148795eb836a', + 'tagid': 'div-gpt-ad-1460505748561-0', + 'bidfloor': 1, + 'secure': 1, + 'bidfloorcur': 'USD', + 'banner': { + 'w': 300, + 'h': 250, + 'format': [ + { + 'w': 300, + 'h': 600 + } + ], + 'pos': 0, + 'topframe': 1 + } +}; + +const sampleRequest = { + 'at': 1, + 'cur': [ + 'USD' + ], + 'imp': [ + sampleRequestBanner, + { + 'id': '7329ddc1d84eb3', + 'tagid': 'div-gpt-ad-1460505748561-1', + 'secure': 1, + 'bidfloorcur': 'USD', + 'native': { + 'request': '{"assets":[{"id":1,"required":1,"title":{"len":80}},{"id":5,"required":1,"data":{"type":2}},{"id":2,"required":1,"img":{"type":{"ID":2,"KEY":"image","TYPE":0},"w":150,"h":50}},{"id":4,"required":1,"data":{"type":1}}]}' + } + } + ], + 'site': { + 'page': 'http://localhost:9999/integrationExamples/gpt/hello_world.html?pbjs_debug=true', + 'ref': 'http://localhost:9999/integrationExamples/gpt/hello_world.html?pbjs_debug=true', + 'publisher': { + 'id': 'xxxxxx' + } + }, + 'device': { + 'ua': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/86.0.4240.198 Safari/537.36', + 'js': 1, + 'dnt': 0, + 'h': 600, + 'w': 800, + 'language': 'en-US', + 'geo': { + 'lat': 33.91989876432274, + 'lon': -84.38897708175764 + } + }, + 'user': { + 'gender': 'M', + 'geo': { + 'lat': 33.91989876432274, + 'lon': -84.38897708175764 + }, + 'yob': 2000 + }, + 'test': 0, + 'ext': { + 'version': '0.0.1' + }, + 'source': { + 'tid': '2c8cd034-f068-4419-8c30-f07292c0d17b' + } +}; + +const sampleValidBannerBidRequest = { + 'bidder': 'pubwise', + 'params': { + 'siteId': 'xxxxxx', + 'bidFloor': '1.00', + 'currency': 'USD', + 'gender': 'M', + 'lat': '33.91989876432274', + 'lon': '-84.38897708175764', + 'yob': '2000', + 'bcat': ['IAB25-3', 'IAB26-1', 'IAB26-2', 'IAB26-3', 'IAB26-4'], + }, + 'gdprConsent': { + 'consentString': 'BOEFEAyOEFEAyAHABDENAI4AAAB9vABAASA', + 'gdprApplies': 1, + }, + 'uspConsent': 1, + 'crumbs': { + 'pubcid': '9a62f261-3c0b-4cc8-8db3-a72ae86ec6ba' + }, + 'fpd': { + 'context': { + 'adServer': { + 'name': 'gam', + 'adSlot': '/19968336/header-bid-tag-0' + }, + 'pbAdSlot': '/19968336/header-bid-tag-0' + } + }, + 'mediaTypes': { + 'banner': { + 'sizes': [ + [ + 300, + 250 + ], + [ + 300, + 600 + ] + ] + } + }, + 'adUnitCode': 'div-gpt-ad-1460505748561-0', + 'transactionId': '2001a8b2-3bcf-417d-b64f-92641dae21e0', + 'sizes': [ + [ + 300, + 250 + ], + [ + 300, + 600 + ] + ], + 'bidId': '6c148795eb836a', + 'bidderRequestId': '18a45bff5ff705', + 'auctionId': '9f20663c-4629-4b5c-bff6-ff3aa8319358', + 'src': 'client', + 'bidRequestsCount': 1, + 'bidderRequestsCount': 1, + 'bidderWinsCount': 0 +}; + +const sampleValidBidRequests = [ + sampleValidBannerBidRequest, + { + 'bidder': 'pubwise', + 'params': { + 'siteId': 'xxxxxx' + }, + 'crumbs': { + 'pubcid': '9a62f261-3c0b-4cc8-8db3-a72ae86ec6ba' + }, + 'nativeParams': { + 'title': { + 'required': true, + 'len': 80 + }, + 'body': { + 'required': true + }, + 'image': { + 'required': true, + 'sizes': [ + 150, + 50 + ] + }, + 'sponsoredBy': { + 'required': true + }, + 'icon': { + 'required': false + } + }, + 'fpd': { + 'context': { + 'adServer': { + 'name': 'gam', + 'adSlot': '/19968336/header-bid-tag-0' + }, + 'pbAdSlot': '/19968336/header-bid-tag-0' + } + }, + 'mediaTypes': { + 'native': { + 'title': { + 'required': true, + 'len': 80 + }, + 'body': { + 'required': true + }, + 'image': { + 'required': true, + 'sizes': [ + 150, + 50 + ] + }, + 'sponsoredBy': { + 'required': true + }, + 'icon': { + 'required': false + } + } + }, + 'adUnitCode': 'div-gpt-ad-1460505748561-1', + 'transactionId': '2c8cd034-f068-4419-8c30-f07292c0d17b', + 'sizes': [], + 'bidId': '30ab7516a51a7c', + 'bidderRequestId': '18a45bff5ff705', + 'auctionId': '9f20663c-4629-4b5c-bff6-ff3aa8319358', + 'src': 'client', + 'bidRequestsCount': 1, + 'bidderRequestsCount': 1, + 'bidderWinsCount': 0 + } +] + +const sampleBidderBannerRequest = { + 'bidder': 'pubwise', + 'params': { + 'siteId': 'xxxxxx', + 'height': 250, + 'width': 300, + 'gender': 'M', + 'yob': '2000', + 'lat': '33.91989876432274', + 'lon': '-84.38897708175764', + 'bidFloor': '1.00', + 'currency': 'USD', + 'adSlot': '', + 'adUnit': '', + 'bcat': [ + 'IAB25-3', + 'IAB26-1', + 'IAB26-2', + 'IAB26-3', + 'IAB26-4', + ], + }, + 'crumbs': { + 'pubcid': '9a62f261-3c0b-4cc8-8db3-a72ae86ec6ba' + }, + 'fpd': { + 'context': { + 'adServer': { + 'name': 'gam', + 'adSlot': '/19968336/header-bid-tag-0' + }, + 'pbAdSlot': '/19968336/header-bid-tag-0' + } + }, + 'mediaTypes': { + 'banner': { + 'sizes': [ + [ + 300, + 600 + ] + ] + } + }, + 'adUnitCode': 'div-gpt-ad-1460505748561-0', + 'transactionId': '2001a8b2-3bcf-417d-b64f-92641dae21e0', + 'sizes': [ + [ + 300, + 250 + ], + [ + 300, + 600 + ] + ], + 'bidId': '6c148795eb836a', + 'bidderRequestId': '18a45bff5ff705', + 'auctionId': '9f20663c-4629-4b5c-bff6-ff3aa8319358', + 'src': 'client', + 'bidRequestsCount': 1, + 'bidderRequestsCount': 1, + 'bidderWinsCount': 0, + 'gdprConsent': { + 'consentString': 'BOEFEAyOEFEAyAHABDENAI4AAAB9vABAASA', + 'gdprApplies': 1, + }, + 'uspConsent': 1, +}; + +const sampleBidderRequest = { + 'bidderCode': 'pubwise', + 'auctionId': '9f20663c-4629-4b5c-bff6-ff3aa8319358', + 'bidderRequestId': '18a45bff5ff705', + 'bids': [ + sampleBidderBannerRequest, + { + 'bidder': 'pubwise', + 'params': { + 'siteId': 'xxxxxx' + }, + 'crumbs': { + 'pubcid': '9a62f261-3c0b-4cc8-8db3-a72ae86ec6ba' + }, + 'nativeParams': { + 'title': { + 'required': true, + 'len': 80 + }, + 'body': { + 'required': true + }, + 'image': { + 'required': true, + 'sizes': [ + 150, + 50 + ] + }, + 'sponsoredBy': { + 'required': true + }, + 'icon': { + 'required': false + } + }, + 'fpd': { + 'context': { + 'adServer': { + 'name': 'gam', + 'adSlot': '/19968336/header-bid-tag-0' + }, + 'pbAdSlot': '/19968336/header-bid-tag-0' + } + }, + 'mediaTypes': { + 'native': { + 'title': { + 'required': true, + 'len': 80 + }, + 'body': { + 'required': true + }, + 'image': { + 'required': true, + 'sizes': [ + 150, + 50 + ] + }, + 'sponsoredBy': { + 'required': true + }, + 'icon': { + 'required': false + } + } + }, + 'adUnitCode': 'div-gpt-ad-1460505748561-1', + 'transactionId': '2c8cd034-f068-4419-8c30-f07292c0d17b', + 'sizes': [], + 'bidId': '30ab7516a51a7c', + 'bidderRequestId': '18a45bff5ff705', + 'auctionId': '9f20663c-4629-4b5c-bff6-ff3aa8319358', + 'src': 'client', + 'bidRequestsCount': 1, + 'bidderRequestsCount': 1, + 'bidderWinsCount': 0 + } + ], + 'auctionStart': 1606269202001, + 'timeout': 1000, + 'gdprConsent': { + 'consentString': 'BOEFEAyOEFEAyAHABDENAI4AAAB9vABAASA', + 'gdprApplies': 1, + }, + 'uspConsent': 1, + 'refererInfo': { + 'referer': 'http://localhost:9999/integrationExamples/gpt/hello_world.html?pbjs_debug=true', + 'reachedTop': true, + 'isAmp': false, + 'numIframes': 0, + 'stack': [ + 'http://localhost:9999/integrationExamples/gpt/hello_world.html?pbjs_debug=true' + ], + 'canonicalUrl': null + }, + 'start': 1606269202004 +}; + +const sampleRTBResponse = { + 'body': { + 'id': '1606251348404', + 'seatbid': [ + { + 'bid': [ + { + 'id': '1606579704052', + 'impid': '6c148795eb836a', + 'price': 1.23, + 'adm': '\u003cdiv style="box-sizing: border-box;width:298px;height:248px;border: 1px solid rgba(0,0,0,.25);border-radius:10px;"\u003e\n\t\u003ch3 style="margin-top:80px;text-align: center;"\u003ePubWise Test Bid\u003c/h3\u003e\n\u003c/div\u003e', + 'crid': 'test', + 'w': 300, + 'h': 250 + }, + { + 'id': '1606579704052', + 'impid': '7329ddc1d84eb3', + 'price': 1.23, + 'adm': '{"ver":"1.2","assets":[{"id":1,"title":{"text":"PubWise Test"}},{"id":2,"img":{"type":3,"url":"http://www.pubwise.io","w":300,"h":250}},{"id":3,"img":{"type":1,"url":"http://www.pubwise.io","w":150,"h":125}},{"id":5,"data":{"type":2,"value":"PubWise Test Desc"}},{"id":4,"data":{"type":1,"value":"PubWise.io"}}],"link":{"url":"http://www.pubwise.io"}}', + 'crid': 'test', + 'w': 300, + 'h': 250 + } + ] + } + ], + 'bidid': 'testtesttest' + } +}; + +const samplePBBidObjects = [ + { + 'requestId': '6c148795eb836a', + 'cpm': '1.23', + 'width': 300, + 'height': 250, + 'creativeId': 'test', + 'currency': 'USD', + 'netRevenue': true, + 'ttl': 300, + 'ad': '
\n\t

PubWise Test Bid

\n
', + 'pw_seat': null, + 'pw_dspid': null, + 'partnerImpId': '1606579704052', + 'meta': {}, + 'mediaType': 'banner', + }, + { + 'requestId': '7329ddc1d84eb3', + 'cpm': '1.23', + 'width': 300, + 'height': 250, + 'creativeId': 'test', + 'currency': 'USD', + 'netRevenue': true, + 'ttl': 300, + 'ad': '{\"ver\":\"1.2\",\"assets\":[{\"id\":1,\"title\":{\"text\":\"PubWise Test\"}},{\"id\":2,\"img\":{\"type\":3,\"url\":\"http://www.pubwise.io\",\"w\":300,\"h\":250}},{\"id\":3,\"img\":{\"type\":1,\"url\":\"http://www.pubwise.io\",\"w\":150,\"h\":125}},{\"id\":5,\"data\":{\"type\":2,\"value\":\"PubWise Test Desc\"}},{\"id\":4,\"data\":{\"type\":1,\"value\":\"PubWise.io\"}}],\"link\":{\"url\":\"http://www.pubwise.io\"}}', + 'pw_seat': null, + 'pw_dspid': null, + 'partnerImpId': '1606579704052', + 'mediaType': 'native', + 'native': { + 'body': 'PubWise Test Desc', + 'icon': { + 'height': 125, + 'url': 'http://www.pubwise.io', + 'width': 150, + }, + 'image': { + 'height': 250, + 'url': 'http://www.pubwise.io', + 'width': 300, + }, + 'sponsoredBy': 'PubWise.io', + 'title': 'PubWise Test' + }, + 'meta': {}, + 'impressionTrackers': [], + 'jstracker': [], + 'clickTrackers': [], + 'clickUrl': 'http://www.pubwise.io' + } +]; + +describe('PubWiseAdapter', function () { + describe('Properly Validates Bids', function () { + it('valid bid', function () { + let validBid = { + bidder: 'pubwise', + params: { + siteId: 'xxxxxx' + } + }, + isValid = spec.isBidRequestValid(validBid); + expect(isValid).to.equal(true); + }); + + it('valid bid: extra fields are ok', function () { + let validBid = { + bidder: 'pubwise', + params: { + siteId: 'xxxxxx', + gender: 'M', + } + }, + isValid = spec.isBidRequestValid(validBid); + expect(isValid).to.equal(true); + }); + + it('invalid bid: no siteId', function () { + let inValidBid = { + bidder: 'pubwise', + params: { + gender: 'M', + } + }, + isValid = spec.isBidRequestValid(inValidBid); + expect(isValid).to.equal(false); + }); + + it('invalid bid: siteId should be a string', function () { + let validBid = { + bidder: 'pubwise', + params: { + siteId: 123456 + } + }, + isValid = spec.isBidRequestValid(validBid); + expect(isValid).to.equal(false); + }); + }); + + describe('Handling Request Construction', function () { + it('bid requests are not mutable', function() { + let sourceBidRequest = utils.deepClone(sampleValidBidRequests) + spec.buildRequests(sampleValidBidRequests, {auctinId: 'placeholder'}); + expect(sampleValidBidRequests).to.deep.equal(sourceBidRequest, 'Should be unedited as they are used elsewhere'); + }); + it('should handle complex bidRequest', function() { + let request = spec.buildRequests(sampleValidBidRequests, sampleBidderRequest); + expect(request.bidderRequest).to.equal(sampleBidderRequest); + }); + it('must conform to API for buildRequests', function() { + let request = spec.buildRequests(sampleValidBidRequests); + expect(request.bidderRequest).to.be.undefined; + }); + }); + + describe('Identifies Media Types', function () { + it('identifies native adm type', function() { + let adm = '{"ver":"1.2","assets":[{"title":{"text":"PubWise Test"}},{"img":{"type":3,"url":"http://www.pubwise.io"}},{"img":{"type":1,"url":"http://www.pubwise.io"}},{"data":{"type":2,"value":"PubWise Test Desc"}},{"data":{"type":1,"value":"PubWise.io"}}],"link":{"url":""}}'; + let newBid = {mediaType: 'unknown'}; + _checkMediaType(adm, newBid); + expect(newBid.mediaType).to.equal('native', adm + ' Is a Native adm'); + }); + + it('identifies banner adm type', function() { + let adm = '

PubWise Test Bid

'; + let newBid = {mediaType: 'unknown'}; + _checkMediaType(adm, newBid); + expect(newBid.mediaType).to.equal('banner', adm + ' Is a Banner adm'); + }); + }); + + describe('Properly Parses AdSlot Data', function () { + it('parses banner', function() { + let testBid = utils.deepClone(sampleValidBannerBidRequest) + _parseAdSlot(testBid) + expect(testBid).to.deep.equal(sampleBidderBannerRequest); + }); + }); + + describe('Properly Handles Response', function () { + it('handles response with muiltiple responses', function() { + // the request when it comes back is on the data object + let pbResponse = spec.interpretResponse(sampleRTBResponse, {'data': sampleRequest}) + expect(pbResponse).to.deep.equal(samplePBBidObjects); + }); + }); +}); diff --git a/test/spec/modules/pubxBidAdapter_spec.js b/test/spec/modules/pubxBidAdapter_spec.js index d5f1a0f5da3..6cea8787845 100644 --- a/test/spec/modules/pubxBidAdapter_spec.js +++ b/test/spec/modules/pubxBidAdapter_spec.js @@ -60,6 +60,70 @@ describe('pubxAdapter', function () { }); }); + describe('getUserSyncs', function () { + const sandbox = sinon.sandbox.create(); + + const keywordsText = 'meta1,meta2,meta3,meta4,meta5'; + const descriptionText = 'description1description2description3description4description5description'; + + let documentStubMeta; + + beforeEach(function () { + documentStubMeta = sandbox.stub(document, 'getElementsByName'); + const metaElKeywords = document.createElement('meta'); + metaElKeywords.setAttribute('name', 'keywords'); + metaElKeywords.setAttribute('content', keywordsText); + documentStubMeta.withArgs('keywords').returns([metaElKeywords]); + + const metaElDescription = document.createElement('meta'); + metaElDescription.setAttribute('name', 'description'); + metaElDescription.setAttribute('content', descriptionText); + documentStubMeta.withArgs('description').returns([metaElDescription]); + }); + + afterEach(function () { + documentStubMeta.restore(); + }); + + let kwString = ''; + let kwEnc = ''; + let descContent = ''; + let descEnc = ''; + + it('returns empty sync array when iframe is not enabled', function () { + const syncOptions = {}; + expect(spec.getUserSyncs(syncOptions)).to.deep.equal([]); + }); + + it('returns kwEnc when there is kwTag with more than 20 length', function () { + const kwArray = keywordsText.substr(0, 20).split(','); + kwArray.pop(); + kwString = kwArray.join(); + kwEnc = encodeURIComponent(kwString); + const syncs = spec.getUserSyncs({ iframeEnabled: true }); + expect(syncs[0].url).to.include(`pkw=${kwEnc}`); + }); + + it('returns kwEnc when there is kwTag with more than 60 length', function () { + descContent = descContent.substr(0, 60); + descEnc = encodeURIComponent(descContent); + const syncs = spec.getUserSyncs({ iframeEnabled: true }); + expect(syncs[0].url).to.include(`pkw=${descEnc}`); + }); + + it('returns titleEnc when there is titleContent with more than 30 length', function () { + let titleText = 'title1title2title3title4title5title'; + const documentStubTitle = sandbox.stub(document, 'title').value(titleText); + + if (titleText.length > 30) { + titleText = titleText.substr(0, 30); + } + + const syncs = spec.getUserSyncs({ iframeEnabled: true }); + expect(syncs[0].url).to.include(`pt=${encodeURIComponent(titleText)}`); + }); + }); + describe('interpretResponse', function () { const serverResponse = { body: { diff --git a/test/spec/modules/pubxaiAnalyticsAdapter_spec.js b/test/spec/modules/pubxaiAnalyticsAdapter_spec.js index 91c81dcae8d..95245f2c6c9 100644 --- a/test/spec/modules/pubxaiAnalyticsAdapter_spec.js +++ b/test/spec/modules/pubxaiAnalyticsAdapter_spec.js @@ -27,6 +27,8 @@ describe('pubxai analytics adapter', function() { pubxId: '6c415fc0-8b0e-4cf5-be73-01526a4db625' }; + let location = utils.getWindowLocation(); + let prebidEvent = { 'auctionInit': { 'auctionId': 'bc3806e4-873e-453c-8ae5-204f35e923b4', @@ -219,6 +221,12 @@ describe('pubxai analytics adapter', function() { 'originalCpm': 0.5, 'originalCurrency': 'USD', 'floorData': { + 'fetchStatus': 'success', + 'floorProvider': 'PubXFloor', + 'location': 'fetch', + 'modelVersion': 'new model 1.0', + 'skipRate': 0, + 'skipped': false, 'floorValue': 0.4, 'floorRule': '/19968336/header-bid-tag-1|banner', 'floorCurrency': 'USD', @@ -381,6 +389,12 @@ describe('pubxai analytics adapter', function() { 'originalCpm': 0.5, 'originalCurrency': 'USD', 'floorData': { + 'fetchStatus': 'success', + 'floorProvider': 'PubXFloor', + 'location': 'fetch', + 'modelVersion': 'new model 1.0', + 'skipRate': 0, + 'skipped': false, 'floorValue': 0.4, 'floorRule': '/19968336/header-bid-tag-1|banner', 'floorCurrency': 'USD', @@ -449,6 +463,12 @@ describe('pubxai analytics adapter', function() { 'originalCpm': 0.5, 'originalCurrency': 'USD', 'floorData': { + 'fetchStatus': 'success', + 'floorProvider': 'PubXFloor', + 'location': 'fetch', + 'modelVersion': 'new model 1.0', + 'skipRate': 0, + 'skipped': false, 'floorValue': 0.4, 'floorRule': '/19968336/header-bid-tag-1|banner', 'floorCurrency': 'USD', @@ -488,9 +508,13 @@ describe('pubxai analytics adapter', function() { 'params': [{ 'placementId': 13144370 }] - } + }, + 'pageDetail': { + 'host': location.host, + 'path': location.pathname, + 'search': location.search + }, }; - let location = utils.getWindowLocation(); let expectedAfterBid = { 'bids': [{ @@ -509,6 +533,12 @@ describe('pubxai analytics adapter', function() { 'mediaType': 'banner', 'statusMessage': 'Bid available', 'floorData': { + 'fetchStatus': 'success', + 'floorProvider': 'PubXFloor', + 'location': 'fetch', + 'modelVersion': 'new model 1.0', + 'skipRate': 0, + 'skipped': false, 'floorValue': 0.4, 'floorRule': '/19968336/header-bid-tag-1|banner', 'floorCurrency': 'USD', @@ -527,118 +557,21 @@ describe('pubxai analytics adapter', function() { 'timeToRespond': 267, 'responseTimestamp': 1603865707449, 'platform': navigator.platform, + 'placementId': 13144370, 'deviceType': getDeviceType() }], - 'auctionInit': { + 'pageDetail': { 'host': location.host, 'path': location.pathname, - 'search': location.search, - 'auctionId': 'bc3806e4-873e-453c-8ae5-204f35e923b4', - 'timestamp': 1603865707180, - 'auctionStatus': 'inProgress', - 'adUnits': [{ - 'code': '/19968336/header-bid-tag-1', - 'mediaTypes': { - 'banner': { - 'sizes': [ - [ - 300, - 250 - ] - ] - } - }, - 'bids': [{ - 'bidder': 'appnexus', - 'params': { - 'placementId': 13144370 - }, - 'auctionId': 'bc3806e4-873e-453c-8ae5-204f35e923b4', - 'floorData': { - 'skipped': false, - 'skipRate': 0, - 'modelVersion': 'new model 1.0', - 'location': 'fetch', - 'floorProvider': 'PubXFloor', - 'fetchStatus': 'success' - } - }], - 'sizes': [ - [ - 300, - 250 - ] - ], - 'transactionId': '41ec8eaf-3e7c-4a8b-8344-ab796ff6e294' - }], - 'adUnitCodes': [ - '/19968336/header-bid-tag-1' - ], - 'bidderRequests': [{ - 'bidderCode': 'appnexus', - 'auctionId': 'bc3806e4-873e-453c-8ae5-204f35e923b4', - 'bidderRequestId': '184cbc05bb90ba', - 'bids': [{ - 'bidder': 'appnexus', - 'params': { - 'placementId': 13144370 - }, - 'auctionId': 'bc3806e4-873e-453c-8ae5-204f35e923b4', - 'floorData': { - 'skipped': false, - 'skipRate': 0, - 'modelVersion': 'new model 1.0', - 'location': 'fetch', - 'floorProvider': 'PubXFloor', - 'fetchStatus': 'success' - }, - 'mediaTypes': { - 'banner': { - 'sizes': [ - [ - 300, - 250 - ] - ] - } - }, - 'adUnitCode': '/19968336/header-bid-tag-1', - 'transactionId': '41ec8eaf-3e7c-4a8b-8344-ab796ff6e294', - 'sizes': [ - [ - 300, - 250 - ] - ], - 'bidId': '248f9a4489835e', - 'bidderRequestId': '184cbc05bb90ba', - 'src': 'client', - 'bidRequestsCount': 1, - 'bidderRequestsCount': 1, - 'bidderWinsCount': 0 - }], - 'auctionStart': 1603865707180, - 'timeout': 1000, - 'refererInfo': { - 'referer': 'http://local-pnh.net:8080/stream/', - 'reachedTop': true, - 'isAmp': false, - 'numIframes': 0, - 'stack': [ - 'http://local-pnh.net:8080/stream/' - ], - 'canonicalUrl': null - }, - 'start': 1603865707182 - }], - 'noBids': [], - 'bidsReceived': [], - 'winningBids': [], - 'timeout': 1000, - 'config': { - 'samplingRate': '1', - 'pubxId': '6c415fc0-8b0e-4cf5-be73-01526a4db625' - } + 'search': location.search + }, + 'floorDetail': { + 'fetchStatus': 'success', + 'floorProvider': 'PubXFloor', + 'location': 'fetch', + 'modelVersion': 'new model 1.0', + 'skipRate': 0, + 'skipped': false }, 'initOptions': initOptions }; @@ -660,6 +593,12 @@ describe('pubxai analytics adapter', function() { 'status': 'rendered', 'statusMessage': 'Bid available', 'floorData': { + 'fetchStatus': 'success', + 'floorProvider': 'PubXFloor', + 'location': 'fetch', + 'modelVersion': 'new model 1.0', + 'skipRate': 0, + 'skipped': false, 'floorValue': 0.4, 'floorRule': '/19968336/header-bid-tag-1|banner', 'floorCurrency': 'USD', @@ -680,6 +619,11 @@ describe('pubxai analytics adapter', function() { 'platform': navigator.platform, 'deviceType': getDeviceType() }, + 'pageDetail': { + 'host': location.host, + 'path': location.pathname, + 'search': location.search + }, 'initOptions': initOptions } diff --git a/test/spec/modules/readpeakBidAdapter_spec.js b/test/spec/modules/readpeakBidAdapter_spec.js index 0c6f942e724..d5a877f6221 100644 --- a/test/spec/modules/readpeakBidAdapter_spec.js +++ b/test/spec/modules/readpeakBidAdapter_spec.js @@ -28,7 +28,8 @@ describe('ReadPeakAdapter', function() { params: { bidfloor: 5.0, publisherId: '11bc5dd5-7421-4dd8-c926-40fa653bec76', - siteId: '11bc5dd5-7421-4dd8-c926-40fa653bec77' + siteId: '11bc5dd5-7421-4dd8-c926-40fa653bec77', + tagId: 'test-tag-1' }, bidId: '2ffb201a808da7', bidderRequestId: '178e34bad3658f', @@ -104,7 +105,8 @@ describe('ReadPeakAdapter', function() { ver: '1.1' }, bidfloor: 5, - bidfloorcur: 'USD' + bidfloorcur: 'USD', + tagId: 'test-tag-1' } ], site: { @@ -177,6 +179,7 @@ describe('ReadPeakAdapter', function() { expect(data.id).to.equal(bidRequest.bidderRequestId); expect(data.imp[0].bidfloor).to.equal(bidRequest.params.bidfloor); expect(data.imp[0].bidfloorcur).to.equal('USD'); + expect(data.imp[0].tagId).to.equal('test-tag-1'); expect(data.site.publisher.id).to.equal(bidRequest.params.publisherId); expect(data.site.id).to.equal(bidRequest.params.siteId); expect(data.site.page).to.equal(bidderRequest.refererInfo.referer); diff --git a/test/spec/modules/relevantAnalyticsAdapter_spec.js b/test/spec/modules/relevantAnalyticsAdapter_spec.js new file mode 100644 index 00000000000..3e31db2d7dc --- /dev/null +++ b/test/spec/modules/relevantAnalyticsAdapter_spec.js @@ -0,0 +1,43 @@ +import relevantAnalytics from '../../../modules/relevantAnalyticsAdapter.js'; +import adapterManager from 'src/adapterManager'; +import events from 'src/events'; +import constants from 'src/constants.json' +import { expect } from 'chai'; + +describe('Relevant Analytics Adapter', () => { + beforeEach(() => { + adapterManager.enableAnalytics({ + provider: 'relevant' + }); + }); + + afterEach(() => { + relevantAnalytics.disableAnalytics(); + }); + + it('should pass all events to the global array', () => { + // Given + const testEvents = [ + { ev: constants.EVENTS.AUCTION_INIT, args: { test: 1 } }, + { ev: constants.EVENTS.BID_REQUESTED, args: { test: 2 } }, + ]; + + // When + testEvents.forEach(({ ev, args }) => ( + events.emit(ev, args) + )); + + // Then + const eventQueue = (window.relevantDigital || {}).pbEventLog; + expect(eventQueue).to.be.an('array'); + expect(eventQueue.length).to.be.at.least(testEvents.length); + + // The last events should be our test events + const myEvents = eventQueue.slice(-testEvents.length); + testEvents.forEach(({ ev, args }, idx) => { + const actualEvent = myEvents[idx]; + expect(actualEvent.ev).to.eql(ev); + expect(actualEvent.args).to.eql(args); + }); + }); +}); diff --git a/test/spec/modules/richaudienceBidAdapter_spec.js b/test/spec/modules/richaudienceBidAdapter_spec.js index 90723fb863f..3b9f07b6efc 100644 --- a/test/spec/modules/richaudienceBidAdapter_spec.js +++ b/test/spec/modules/richaudienceBidAdapter_spec.js @@ -4,7 +4,6 @@ import { spec } from 'modules/richaudienceBidAdapter.js'; import {config} from 'src/config.js'; -import * as utils from 'src/utils.js'; describe('Richaudience adapter tests', function () { var DEFAULT_PARAMS_NEW_SIZES = [{ @@ -20,7 +19,8 @@ describe('Richaudience adapter tests', function () { params: { bidfloor: 0.5, pid: 'ADb1f40rmi', - supplyType: 'site' + supplyType: 'site', + keywords: 'coche=mercedes;coche=audi' }, auctionId: '0cb3144c-d084-4686-b0d6-f5dbe917c563', bidRequestsCount: 1, @@ -240,6 +240,9 @@ describe('Richaudience adapter tests', function () { expect(requestContent).to.have.property('transactionId').and.to.equal('29df2112-348b-4961-8863-1b33684d95e6'); expect(requestContent).to.have.property('timeout').and.to.equal(3000); expect(requestContent).to.have.property('numIframes').and.to.equal(0); + expect(typeof requestContent.scr_rsl === 'string') + expect(typeof requestContent.cpuc === 'number') + expect(requestContent).to.have.property('kws').and.to.equal('coche=mercedes;coche=audi'); }) it('Verify build request to prebid video inestream', function() { diff --git a/test/spec/modules/riseBidAdapter_spec.js b/test/spec/modules/riseBidAdapter_spec.js new file mode 100644 index 00000000000..176437c4f27 --- /dev/null +++ b/test/spec/modules/riseBidAdapter_spec.js @@ -0,0 +1,363 @@ +import { expect } from 'chai'; +import { spec } from 'modules/riseBidAdapter.js'; +import { newBidder } from 'src/adapters/bidderFactory.js'; +import { config } from 'src/config.js'; +import { VIDEO } from '../../../src/mediaTypes.js'; + +const ENDPOINT = 'https://hb.yellowblue.io/hb'; +const TEST_ENDPOINT = 'https://hb.yellowblue.io/hb-test'; +const TTL = 360; + +describe('riseAdapter', function () { + const adapter = newBidder(spec); + + describe('inherited functions', function () { + it('exists and is a function', function () { + expect(adapter.callBids).to.exist.and.to.be.a('function'); + }); + }); + + describe('isBidRequestValid', function () { + const bid = { + 'bidder': spec.code, + 'adUnitCode': 'adunit-code', + 'sizes': [['640', '480']], + 'params': { + 'org': 'jdye8weeyirk00000001' + } + }; + + it('should return true when required params are passed', function () { + expect(spec.isBidRequestValid(bid)).to.equal(true); + }); + + it('should return false when required params are not found', function () { + const newBid = Object.assign({}, bid); + delete newBid.params; + newBid.params = { + 'org': null + }; + expect(spec.isBidRequestValid(newBid)).to.equal(false); + }); + }); + + describe('buildRequests', function () { + const bidRequests = [ + { + 'bidder': spec.code, + 'adUnitCode': 'adunit-code', + 'sizes': [[640, 480]], + 'params': { + 'org': 'jdye8weeyirk00000001' + }, + 'bidId': '299ffc8cca0b87', + 'bidderRequestId': '1144f487e563f9', + 'auctionId': 'bfc420c3-8577-4568-9766-a8a935fb620d', + } + ]; + + const testModeBidRequests = [ + { + 'bidder': spec.code, + 'adUnitCode': 'adunit-code', + 'sizes': [[640, 480]], + 'params': { + 'org': 'jdye8weeyirk00000001', + 'testMode': true + }, + 'bidId': '299ffc8cca0b87', + 'bidderRequestId': '1144f487e563f9', + 'auctionId': 'bfc420c3-8577-4568-9766-a8a935fb620d', + } + ]; + + const bidderRequest = { + bidderCode: 'rise', + } + + it('sends bid request to ENDPOINT via GET', function () { + const requests = spec.buildRequests(bidRequests, bidderRequest); + for (const request of requests) { + expect(request.url).to.equal(ENDPOINT); + expect(request.method).to.equal('GET'); + } + }); + + it('sends bid request to test ENDPOINT via GET', function () { + const requests = spec.buildRequests(testModeBidRequests, bidderRequest); + for (const request of requests) { + expect(request.url).to.equal(TEST_ENDPOINT); + expect(request.method).to.equal('GET'); + } + }); + + it('should send the correct bid Id', function () { + const requests = spec.buildRequests(bidRequests, bidderRequest); + for (const request of requests) { + expect(request.data.bid_id).to.equal('299ffc8cca0b87'); + } + }); + + it('should send the correct width and height', function () { + const requests = spec.buildRequests(bidRequests, bidderRequest); + for (const request of requests) { + expect(request.data).to.be.an('object'); + expect(request.data).to.have.property('width', 640); + expect(request.data).to.have.property('height', 480); + } + }); + + it('should respect syncEnabled option', function() { + config.setConfig({ + userSync: { + syncEnabled: false, + filterSettings: { + all: { + bidders: '*', + filter: 'include' + } + } + } + }); + const requests = spec.buildRequests(bidRequests, bidderRequest); + for (const request of requests) { + expect(request.data).to.be.an('object'); + expect(request.data).to.not.have.property('cs_method'); + } + }); + + it('should respect "iframe" filter settings', function () { + config.setConfig({ + userSync: { + syncEnabled: true, + filterSettings: { + iframe: { + bidders: [spec.code], + filter: 'include' + } + } + } + }); + const requests = spec.buildRequests(bidRequests, bidderRequest); + for (const request of requests) { + expect(request.data).to.be.an('object'); + expect(request.data).to.have.property('cs_method', 'iframe'); + } + }); + + it('should respect "all" filter settings', function () { + config.setConfig({ + userSync: { + syncEnabled: true, + filterSettings: { + all: { + bidders: [spec.code], + filter: 'include' + } + } + } + }); + const requests = spec.buildRequests(bidRequests, bidderRequest); + for (const request of requests) { + expect(request.data).to.be.an('object'); + expect(request.data).to.have.property('cs_method', 'iframe'); + } + }); + + it('should send the pixel user sync param if userSync is enabled and no "iframe" or "all" configs are present', function () { + config.setConfig({ + userSync: { + syncEnabled: true + } + }); + const requests = spec.buildRequests(bidRequests, bidderRequest); + for (const request of requests) { + expect(request.data).to.be.an('object'); + expect(request.data).to.have.property('cs_method', 'pixel'); + } + }); + + it('should respect total exclusion', function() { + config.setConfig({ + userSync: { + syncEnabled: true, + filterSettings: { + image: { + bidders: [spec.code], + filter: 'exclude' + }, + iframe: { + bidders: [spec.code], + filter: 'exclude' + } + } + } + }); + const requests = spec.buildRequests(bidRequests, bidderRequest); + for (const request of requests) { + expect(request.data).to.be.an('object'); + expect(request.data).to.not.have.property('cs_method'); + } + }); + + it('should have us_privacy param if usPrivacy is available in the bidRequest', function () { + const bidderRequestWithUSP = Object.assign({uspConsent: '1YNN'}, bidderRequest); + const requests = spec.buildRequests(bidRequests, bidderRequestWithUSP); + for (const request of requests) { + expect(request.data).to.be.an('object'); + expect(request.data).to.have.property('us_privacy', '1YNN'); + } + }); + + it('should have an empty us_privacy param if usPrivacy is missing in the bidRequest', function () { + const requests = spec.buildRequests(bidRequests, bidderRequest); + for (const request of requests) { + expect(request.data).to.be.an('object'); + expect(request.data).to.not.have.property('us_privacy'); + } + }); + + it('should not send the gdpr param if gdprApplies is false in the bidRequest', function () { + const bidderRequestWithGDPR = Object.assign({gdprConsent: {gdprApplies: false}}, bidderRequest); + const requests = spec.buildRequests(bidRequests, bidderRequestWithGDPR); + for (const request of requests) { + expect(request.data).to.be.an('object'); + expect(request.data).to.not.have.property('gdpr'); + expect(request.data).to.not.have.property('gdpr_consent'); + } + }); + + it('should send the gdpr param if gdprApplies is true in the bidRequest', function () { + const bidderRequestWithGDPR = Object.assign({gdprConsent: {gdprApplies: true, consentString: 'test-consent-string'}}, bidderRequest); + const requests = spec.buildRequests(bidRequests, bidderRequestWithGDPR); + for (const request of requests) { + expect(request.data).to.be.an('object'); + expect(request.data).to.have.property('gdpr', true); + expect(request.data).to.have.property('gdpr_consent', 'test-consent-string'); + } + }); + + it('should have schain param if it is available in the bidRequest', () => { + const schain = { + ver: '1.0', + complete: 1, + nodes: [{ asi: 'indirectseller.com', sid: '00001', hp: 1 }], + }; + bidRequests[0].schain = schain; + const requests = spec.buildRequests(bidRequests, bidderRequest); + for (const request of requests) { + expect(request.data).to.be.an('object'); + expect(request.data).to.have.property('schain', '1.0,1!indirectseller.com,00001,,,,'); + } + }); + }); + + describe('interpretResponse', function () { + const response = { + cpm: 12.5, + vastXml: '', + width: 640, + height: 480, + requestId: '21e12606d47ba7', + netRevenue: true, + currency: 'USD' + }; + + it('should get correct bid response', function () { + let expectedResponse = [ + { + requestId: '21e12606d47ba7', + cpm: 12.5, + width: 640, + height: 480, + creativeId: '21e12606d47ba7', + currency: 'USD', + netRevenue: true, + ttl: TTL, + vastXml: '', + mediaType: VIDEO + } + ]; + const result = spec.interpretResponse({ body: response }); + expect(Object.keys(result[0])).to.have.members(Object.keys(expectedResponse[0])); + }); + }) + + describe('getUserSyncs', function() { + const imageSyncResponse = { + body: { + userSyncPixels: [ + 'https://image-sync-url.test/1', + 'https://image-sync-url.test/2', + 'https://image-sync-url.test/3' + ] + } + }; + + const iframeSyncResponse = { + body: { + userSyncURL: 'https://iframe-sync-url.test' + } + }; + + it('should register all img urls from the response', function() { + const syncs = spec.getUserSyncs({ pixelEnabled: true }, [imageSyncResponse]); + expect(syncs).to.deep.equal([ + { + type: 'image', + url: 'https://image-sync-url.test/1' + }, + { + type: 'image', + url: 'https://image-sync-url.test/2' + }, + { + type: 'image', + url: 'https://image-sync-url.test/3' + } + ]); + }); + + it('should register the iframe url from the response', function() { + const syncs = spec.getUserSyncs({ iframeEnabled: true }, [iframeSyncResponse]); + expect(syncs).to.deep.equal([ + { + type: 'iframe', + url: 'https://iframe-sync-url.test' + } + ]); + }); + + it('should register both image and iframe urls from the responses', function() { + const syncs = spec.getUserSyncs({ pixelEnabled: true, iframeEnabled: true }, [iframeSyncResponse, imageSyncResponse]); + expect(syncs).to.deep.equal([ + { + type: 'iframe', + url: 'https://iframe-sync-url.test' + }, + { + type: 'image', + url: 'https://image-sync-url.test/1' + }, + { + type: 'image', + url: 'https://image-sync-url.test/2' + }, + { + type: 'image', + url: 'https://image-sync-url.test/3' + } + ]); + }); + + it('should handle an empty response', function() { + const syncs = spec.getUserSyncs({ iframeEnabled: true }, []); + expect(syncs).to.deep.equal([]); + }); + + it('should handle when user syncs are disabled', function() { + const syncs = spec.getUserSyncs({ pixelEnabled: false }, [imageSyncResponse]); + expect(syncs).to.deep.equal([]); + }); + }) +}); diff --git a/test/spec/modules/rubiconAnalyticsAdapter_spec.js b/test/spec/modules/rubiconAnalyticsAdapter_spec.js index 4891b8d3282..505a2885404 100644 --- a/test/spec/modules/rubiconAnalyticsAdapter_spec.js +++ b/test/spec/modules/rubiconAnalyticsAdapter_spec.js @@ -532,7 +532,9 @@ const ANALYTICS_MESSAGE = { 'bidwonStatus': 'success' } ], - 'wrapperName': '10000_fakewrapper_test' + 'wrapper': { + 'name': '10000_fakewrapper_test' + } }; function performStandardAuction(gptEvents) { @@ -634,6 +636,7 @@ describe('rubicon analytics adapter', function () { } }); expect(rubiConf).to.deep.equal({ + analyticsEventDelay: 0, pvid: '12345678', wrapperName: '1001_general', int_type: 'dmpbjs', @@ -652,6 +655,7 @@ describe('rubicon analytics adapter', function () { } }); expect(rubiConf).to.deep.equal({ + analyticsEventDelay: 0, pvid: '12345678', wrapperName: '1001_general', int_type: 'dmpbjs', @@ -672,6 +676,7 @@ describe('rubicon analytics adapter', function () { } }); expect(rubiConf).to.deep.equal({ + analyticsEventDelay: 0, pvid: '12345678', wrapperName: '1001_general', int_type: 'dmpbjs', @@ -852,6 +857,8 @@ describe('rubicon analytics adapter', function () { auctionInit.bidderRequests[0].bids[0].floorData = { skipped: false, modelVersion: 'someModelName', + modelWeight: 10, + modelTimestamp: 1606772895, location: 'setConfig', skipRate: 15, fetchStatus: 'error', @@ -953,6 +960,8 @@ describe('rubicon analytics adapter', function () { expect(message.auctions[0].floors).to.deep.equal({ location: 'setConfig', modelName: 'someModelName', + modelWeight: 10, + modelTimestamp: 1606772895, skipped: false, enforcement: true, dealsEnforced: false, @@ -998,6 +1007,8 @@ describe('rubicon analytics adapter', function () { expect(message.auctions[0].floors).to.deep.equal({ location: 'setConfig', modelName: 'someModelName', + modelWeight: 10, + modelTimestamp: 1606772895, skipped: false, enforcement: true, dealsEnforced: false, @@ -1083,6 +1094,34 @@ describe('rubicon analytics adapter', function () { expect(message).to.deep.equal(expectedMessage); }); + it('should convert kvs to strings before sending', function () { + config.setConfig({rubicon: { + fpkvs: { + number: 24, + boolean: false, + string: 'hello', + array: ['one', 2, 'three'], + object: {one: 'two'} + } + }}); + performStandardAuction(); + expect(server.requests.length).to.equal(1); + let request = server.requests[0]; + let message = JSON.parse(request.requestBody); + validate(message); + + let expectedMessage = utils.deepClone(ANALYTICS_MESSAGE); + expectedMessage.session.pvid = STUBBED_UUID.slice(0, 8); + expectedMessage.fpkvs = [ + {key: 'number', value: '24'}, + {key: 'boolean', value: 'false'}, + {key: 'string', value: 'hello'}, + {key: 'array', value: 'one,2,three'}, + {key: 'object', value: '[object Object]'} + ] + expect(message).to.deep.equal(expectedMessage); + }); + it('should use the query utm param rubicon kv value and pass updated kv and pvid when defined', function () { sandbox.stub(utils, 'getWindowLocation').returns({'search': '?utm_source=other', 'pbjs_debug': 'true'}); @@ -1323,12 +1362,12 @@ describe('rubicon analytics adapter', function () { }); }); describe('with googletag enabled', function () { - let gptSlot0, gptSlot1, gptEvent0, gptEvent1; + let gptSlot0, gptSlot1; + let gptSlotRenderEnded0, gptSlotRenderEnded1; beforeEach(function () { mockGpt.enable(); gptSlot0 = mockGpt.makeSlot({code: '/19968336/header-bid-tag-0'}); - gptSlot1 = mockGpt.makeSlot({code: '/19968336/header-bid-tag1'}); - gptEvent0 = { + gptSlotRenderEnded0 = { eventName: 'slotRenderEnded', params: { slot: gptSlot0, @@ -1338,7 +1377,9 @@ describe('rubicon analytics adapter', function () { lineItemId: 3333 } }; - gptEvent1 = { + + gptSlot1 = mockGpt.makeSlot({code: '/19968336/header-bid-tag1'}); + gptSlotRenderEnded1 = { eventName: 'slotRenderEnded', params: { slot: gptSlot1, @@ -1355,7 +1396,7 @@ describe('rubicon analytics adapter', function () { }); it('should add necessary gam information if gpt is enabled and slotRender event emmited', function () { - performStandardAuction([gptEvent0, gptEvent1]); + performStandardAuction([gptSlotRenderEnded0, gptSlotRenderEnded1]); expect(server.requests.length).to.equal(1); let request = server.requests[0]; let message = JSON.parse(request.requestBody); @@ -1378,7 +1419,7 @@ describe('rubicon analytics adapter', function () { }); it('should handle empty gam renders', function () { - performStandardAuction([gptEvent0, { + performStandardAuction([gptSlotRenderEnded0, { eventName: 'slotRenderEnded', params: { slot: gptSlot1, @@ -1405,7 +1446,7 @@ describe('rubicon analytics adapter', function () { }); it('should still add gam ids if falsy', function () { - performStandardAuction([gptEvent0, { + performStandardAuction([gptSlotRenderEnded0, { eventName: 'slotRenderEnded', params: { slot: gptSlot1, @@ -1436,14 +1477,54 @@ describe('rubicon analytics adapter', function () { expect(message).to.deep.equal(expectedMessage); }); - it('should handle empty gam renders', function () { - performStandardAuction([gptEvent0, gptEvent1]); + it('should correctly set adUnit for associated slots', function () { + performStandardAuction([gptSlotRenderEnded0, gptSlotRenderEnded1]); + expect(server.requests.length).to.equal(1); + let request = server.requests[0]; + let message = JSON.parse(request.requestBody); + validate(message); + + let expectedMessage = utils.deepClone(ANALYTICS_MESSAGE); + expectedMessage.auctions[0].adUnits[0].gam = { + advertiserId: 1111, + creativeId: 2222, + lineItemId: 3333, + adSlot: '/19968336/header-bid-tag-0' + }; + expectedMessage.auctions[0].adUnits[1].gam = { + advertiserId: 4444, + creativeId: 5555, + lineItemId: 6666, + adSlot: '/19968336/header-bid-tag1' + }; + expect(message).to.deep.equal(expectedMessage); + }); + + it('should send request when waitForGamSlots is present but no bidWons are sent', function () { + config.setConfig({ + rubicon: { + waitForGamSlots: true, + } + }); + events.emit(AUCTION_INIT, MOCK.AUCTION_INIT); + events.emit(BID_REQUESTED, MOCK.BID_REQUESTED); + events.emit(BID_RESPONSE, MOCK.BID_RESPONSE[0]); + events.emit(BID_RESPONSE, MOCK.BID_RESPONSE[1]); + events.emit(BIDDER_DONE, MOCK.BIDDER_DONE); + events.emit(AUCTION_END, MOCK.AUCTION_END); + events.emit(SET_TARGETING, MOCK.SET_TARGETING); + + // should send if just slotRenderEnded is emmitted for both + mockGpt.emitEvent(gptSlotRenderEnded0.eventName, gptSlotRenderEnded0.params); + mockGpt.emitEvent(gptSlotRenderEnded1.eventName, gptSlotRenderEnded1.params); + expect(server.requests.length).to.equal(1); let request = server.requests[0]; let message = JSON.parse(request.requestBody); validate(message); let expectedMessage = utils.deepClone(ANALYTICS_MESSAGE); + delete expectedMessage.bidsWon; // should not be any of these expectedMessage.auctions[0].adUnits[0].gam = { advertiserId: 1111, creativeId: 2222, @@ -1458,6 +1539,51 @@ describe('rubicon analytics adapter', function () { }; expect(message).to.deep.equal(expectedMessage); }); + + it('should delay the event call depending on analyticsEventDelay config', function () { + config.setConfig({ + rubicon: { + waitForGamSlots: true, + analyticsEventDelay: 2000 + } + }); + events.emit(AUCTION_INIT, MOCK.AUCTION_INIT); + events.emit(BID_REQUESTED, MOCK.BID_REQUESTED); + events.emit(BID_RESPONSE, MOCK.BID_RESPONSE[0]); + events.emit(BID_RESPONSE, MOCK.BID_RESPONSE[1]); + events.emit(BIDDER_DONE, MOCK.BIDDER_DONE); + events.emit(AUCTION_END, MOCK.AUCTION_END); + events.emit(SET_TARGETING, MOCK.SET_TARGETING); + + // should send if just slotRenderEnded is emmitted for both + mockGpt.emitEvent(gptSlotRenderEnded0.eventName, gptSlotRenderEnded0.params); + mockGpt.emitEvent(gptSlotRenderEnded1.eventName, gptSlotRenderEnded1.params); + + // Should not be sent until delay + expect(server.requests.length).to.equal(0); + + // tick the clock and it should fire + clock.tick(2000); + + expect(server.requests.length).to.equal(1); + let request = server.requests[0]; + let message = JSON.parse(request.requestBody); + validate(message); + let expectedGam0 = { + advertiserId: 1111, + creativeId: 2222, + lineItemId: 3333, + adSlot: '/19968336/header-bid-tag-0' + }; + let expectedGam1 = { + advertiserId: 4444, + creativeId: 5555, + lineItemId: 6666, + adSlot: '/19968336/header-bid-tag1' + }; + expect(expectedGam0).to.deep.equal(message.auctions[0].adUnits[0].gam); + expect(expectedGam1).to.deep.equal(message.auctions[0].adUnits[1].gam); + }); }); it('should correctly overwrite bidId if seatBidId is on the bidResponse', function () { @@ -1591,6 +1717,35 @@ describe('rubicon analytics adapter', function () { expect(timedOutBid).to.not.have.property('bidResponse'); }); + it('should pass aupName as pattern', function () { + let bidRequest = utils.deepClone(MOCK.BID_REQUESTED); + bidRequest.bids[0].fpd = { + context: { + aupName: '1234/mycoolsite/*&gpt_leaderboard&deviceType=mobile' + } + }; + bidRequest.bids[1].fpd = { + context: { + aupName: '1234/mycoolsite/*&gpt_skyscraper&deviceType=mobile' + } + }; + events.emit(AUCTION_INIT, MOCK.AUCTION_INIT); + events.emit(BID_REQUESTED, bidRequest); + events.emit(BID_RESPONSE, MOCK.BID_RESPONSE[0]); + events.emit(BIDDER_DONE, MOCK.BIDDER_DONE); + events.emit(AUCTION_END, MOCK.AUCTION_END); + events.emit(SET_TARGETING, MOCK.SET_TARGETING); + + clock.tick(SEND_TIMEOUT + 1000); + + expect(server.requests.length).to.equal(1); + + let message = JSON.parse(server.requests[0].requestBody); + validate(message); + expect(message.auctions[0].adUnits[0].pattern).to.equal('1234/mycoolsite/*&gpt_leaderboard&deviceType=mobile'); + expect(message.auctions[0].adUnits[1].pattern).to.equal('1234/mycoolsite/*&gpt_skyscraper&deviceType=mobile'); + }); + it('should successfully convert bid price to USD in parseBidResponse', function () { // Set the rates setConfig({ @@ -1644,6 +1799,36 @@ describe('rubicon analytics adapter', function () { }); }); + describe('wrapper details passed in', () => { + it('should correctly pass in the wrapper details if provided', () => { + config.setConfig({rubicon: { + wrapperName: '1001_wrapperName_exp.4', + wrapperFamily: '1001_wrapperName', + rule_name: 'na-mobile' + }}); + + rubiconAnalyticsAdapter.enableAnalytics({ + options: { + endpoint: '//localhost:9999/event', + accountId: 1001 + } + }); + + performStandardAuction(); + + expect(server.requests.length).to.equal(1); + const request = server.requests[0]; + const message = JSON.parse(request.requestBody); + expect(message.wrapper).to.deep.equal({ + name: '1001_wrapperName_exp.4', + family: '1001_wrapperName', + rule: 'na-mobile' + }); + + rubiconAnalyticsAdapter.disableAnalytics(); + }); + }); + it('getHostNameFromReferer correctly grabs hostname from an input URL', function () { let inputUrl = 'https://www.prebid.org/some/path?pbjs_debug=true'; expect(getHostNameFromReferer(inputUrl)).to.equal('www.prebid.org'); diff --git a/test/spec/modules/rubiconBidAdapter_spec.js b/test/spec/modules/rubiconBidAdapter_spec.js index 6944034a787..8c25d97dada 100644 --- a/test/spec/modules/rubiconBidAdapter_spec.js +++ b/test/spec/modules/rubiconBidAdapter_spec.js @@ -826,16 +826,22 @@ describe('the rubicon adapter', function () { }); }); - it('should use first party data from getConfig over the bid params, if present', () => { + it('should merge first party data from getConfig with the bid params, if present', () => { const context = { - keywords: ['e', 'f'], - rating: '4-star' + keywords: 'e,f', + rating: '4-star', + data: { + page: 'home' + } }; const user = { - keywords: ['d'], gender: 'M', yob: '1984', - geo: {country: 'ca'} + geo: {country: 'ca'}, + keywords: 'd', + data: { + age: 40 + } }; sandbox.stub(config, 'getConfig').callsFake(key => { @@ -849,14 +855,15 @@ describe('the rubicon adapter', function () { }); const expectedQuery = { - 'kw': 'a,b,c,d,e,f', + 'kw': 'a,b,c,d', 'tg_v.ucat': 'new', 'tg_v.lastsearch': 'iphone', 'tg_v.likes': 'sports,video games', 'tg_v.gender': 'M', + 'tg_v.age': '40', 'tg_v.yob': '1984', - 'tg_v.geo': '{"country":"ca"}', - 'tg_i.rating': '4-star', + 'tg_i.rating': '5-star', + 'tg_i.page': 'home', 'tg_i.prodtype': 'tech,mobile', }; @@ -1865,11 +1872,17 @@ describe('the rubicon adapter', function () { createVideoBidderRequest(); const context = { - keywords: ['e', 'f'], + data: { + page: 'home' + }, + keywords: 'e,f', rating: '4-star' }; const user = { - keywords: ['d'], + data: { + age: 31 + }, + keywords: 'd', gender: 'M', yob: '1984', geo: {country: 'ca'} @@ -1885,18 +1898,22 @@ describe('the rubicon adapter', function () { return utils.deepAccess(config, key); }); - const expected = [{ - bidders: ['rubicon'], - config: { - fpd: { - site: Object.assign({}, bidderRequest.bids[0].params.inventory, context), - user: Object.assign({}, bidderRequest.bids[0].params.visitor, user) - } - } - }]; - const [request] = spec.buildRequests(bidderRequest.bids, bidderRequest); - expect(request.data.ext.prebid.bidderconfig).to.deep.equal(expected); + + const expected = { + site: Object.assign({}, context, context.data, bidderRequest.bids[0].params.inventory), + user: Object.assign({}, user, user.data, bidderRequest.bids[0].params.visitor) + }; + + delete expected.site.data; + delete expected.user.data; + delete expected.site.keywords; + delete expected.user.keywords; + + expect(request.data.site.keywords).to.deep.equal('a,b,c'); + expect(request.data.user.keywords).to.deep.equal('d'); + expect(request.data.site.ext.data).to.deep.equal(expected.site); + expect(request.data.user.ext.data).to.deep.equal(expected.user); }); it('should include storedAuctionResponse in video bid request', function () { @@ -1935,7 +1952,7 @@ describe('the rubicon adapter', function () { createVideoBidderRequest(); bidderRequest.bids[0].fpd = { context: { - adserver: { + adServer: { adSlot: '1234567890', name: 'adServerName1' } @@ -2079,7 +2096,7 @@ describe('the rubicon adapter', function () { it('should not fail if keywords param is not an array', function () { bidderRequest.bids[0].params.keywords = 'a,b,c'; const slotParams = spec.createSlotParams(bidderRequest.bids[0], bidderRequest); - expect(slotParams.kw).to.equal(''); + expect(slotParams.kw).to.equal('a,b,c'); }); }); diff --git a/test/spec/modules/s2sTesting_spec.js b/test/spec/modules/s2sTesting_spec.js index 5c7f3004dee..273f6747e52 100644 --- a/test/spec/modules/s2sTesting_spec.js +++ b/test/spec/modules/s2sTesting_spec.js @@ -1,5 +1,4 @@ import s2sTesting from 'modules/s2sTesting.js'; -import { config } from 'src/config.js'; var expect = require('chai').expect; @@ -78,26 +77,16 @@ describe('s2sTesting', function () { beforeEach(function () { // set random number for testing s2sTesting.globalRand = 0.7; - }); - - it('does not work if testing is "false"', function () { - config.setConfig({s2sConfig: { - bidders: ['rubicon'], - testing: false, - bidderControl: {rubicon: {bidSource: {server: 1, client: 1}}} - }}); - expect(s2sTesting.getSourceBidderMap()).to.eql({ - server: [], - client: [] - }); + s2sTesting.bidSource = {}; }); it('sets one client bidder', function () { - config.setConfig({s2sConfig: { + const s2sConfig = { bidders: ['rubicon'], - testing: true, bidderControl: {rubicon: {bidSource: {server: 1, client: 1}}} - }}); + }; + + s2sTesting.calculateBidSources(s2sConfig); expect(s2sTesting.getSourceBidderMap()).to.eql({ server: [], client: ['rubicon'] @@ -105,11 +94,11 @@ describe('s2sTesting', function () { }); it('sets one server bidder', function () { - config.setConfig({s2sConfig: { + const s2sConfig = { bidders: ['rubicon'], - testing: true, bidderControl: {rubicon: {bidSource: {server: 4, client: 1}}} - }}); + } + s2sTesting.calculateBidSources(s2sConfig); expect(s2sTesting.getSourceBidderMap()).to.eql({ server: ['rubicon'], client: [] @@ -117,10 +106,10 @@ describe('s2sTesting', function () { }); it('defaults to server', function () { - config.setConfig({s2sConfig: { + const s2sConfig = { bidders: ['rubicon'], - testing: true - }}); + } + s2sTesting.calculateBidSources(s2sConfig); expect(s2sTesting.getSourceBidderMap()).to.eql({ server: ['rubicon'], client: [] @@ -128,13 +117,14 @@ describe('s2sTesting', function () { }); it('sets two bidders', function () { - config.setConfig({s2sConfig: { + const s2sConfig = { bidders: ['rubicon', 'appnexus'], - testing: true, bidderControl: { rubicon: {bidSource: {server: 3, client: 1}}, appnexus: {bidSource: {server: 1, client: 1}} - }}}); + } + } + s2sTesting.calculateBidSources(s2sConfig); var serverClientBidders = s2sTesting.getSourceBidderMap(); expect(serverClientBidders.server).to.eql(['rubicon']); expect(serverClientBidders.client).to.have.members(['appnexus']); @@ -143,33 +133,37 @@ describe('s2sTesting', function () { it('sends both bidders to same source when weights are the same', function () { s2sTesting.globalRand = 0.01; - config.setConfig({s2sConfig: { + const s2sConfig = { bidders: ['rubicon', 'appnexus'], - testing: true, bidderControl: { rubicon: {bidSource: {server: 1, client: 99}}, appnexus: {bidSource: {server: 1, client: 99}} - }}}); + } + } + s2sTesting.calculateBidSources(s2sConfig); expect(s2sTesting.getSourceBidderMap()).to.eql({ client: ['rubicon', 'appnexus'], server: [] }); + s2sTesting.calculateBidSources(s2sConfig); expect(s2sTesting.getSourceBidderMap()).to.eql({ client: ['rubicon', 'appnexus'], server: [] }); + s2sTesting.calculateBidSources(s2sConfig); expect(s2sTesting.getSourceBidderMap()).to.eql({ client: ['rubicon', 'appnexus'], server: [] }); - config.setConfig({s2sConfig: { + const s2sConfig2 = { bidders: ['rubicon', 'appnexus'], - testing: true, bidderControl: { rubicon: {bidSource: {server: 99, client: 1}}, appnexus: {bidSource: {server: 99, client: 1}} - }}}); + } + } + s2sTesting.calculateBidSources(s2sConfig2); expect(s2sTesting.getSourceBidderMap()).to.eql({ server: ['rubicon', 'appnexus'], client: [] @@ -186,11 +180,12 @@ describe('s2sTesting', function () { }); describe('setting source through adUnits', function () { + const s2sConfig3 = {testing: true}; + beforeEach(function () { - // reset s2sconfig bid sources - config.setConfig({s2sConfig: {testing: true}}); // set random number for testing s2sTesting.globalRand = 0.7; + s2sTesting.bidSource = {}; }); it('sets one bidder source from one adUnit', function () { @@ -199,7 +194,8 @@ describe('s2sTesting', function () { {bidder: 'rubicon', bidSource: {server: 4, client: 1}} ]} ]; - expect(s2sTesting.getSourceBidderMap(adUnits)).to.eql({ + + expect(s2sTesting.getSourceBidderMap(adUnits, [])).to.eql({ server: ['rubicon'], client: [] }); @@ -212,7 +208,7 @@ describe('s2sTesting', function () { {bidder: 'rubicon', bidSource: {server: 1, client: 1}} ]} ]; - expect(s2sTesting.getSourceBidderMap(adUnits)).to.eql({ + expect(s2sTesting.getSourceBidderMap(adUnits, [])).to.eql({ server: [], client: ['rubicon'] }); @@ -227,7 +223,7 @@ describe('s2sTesting', function () { {bidder: 'rubicon', bidSource: {}} ]} ]; - expect(s2sTesting.getSourceBidderMap(adUnits)).to.eql({ + expect(s2sTesting.getSourceBidderMap(adUnits, [])).to.eql({ server: [], client: ['rubicon'] }); @@ -243,7 +239,7 @@ describe('s2sTesting', function () { {bidder: 'appnexus', bidSource: {server: 3, client: 1}} ]} ]; - var serverClientBidders = s2sTesting.getSourceBidderMap(adUnits); + var serverClientBidders = s2sTesting.getSourceBidderMap(adUnits, []); expect(serverClientBidders.server).to.eql(['appnexus']); expect(serverClientBidders.client).to.have.members(['rubicon']); // should have saved the source on the bid @@ -264,7 +260,7 @@ describe('s2sTesting', function () { {bidder: 'bidder3', bidSource: {client: 1}} ]} ]; - var serverClientBidders = s2sTesting.getSourceBidderMap(adUnits); + var serverClientBidders = s2sTesting.getSourceBidderMap(adUnits, []); expect(serverClientBidders.server).to.have.members(['rubicon']); expect(serverClientBidders.server).to.not.have.members(['appnexus', 'bidder3']); expect(serverClientBidders.client).to.have.members(['rubicon', 'appnexus', 'bidder3']); @@ -287,7 +283,7 @@ describe('s2sTesting', function () { {bidder: 'bidder3', calcSource: 'server', bidSource: {client: 1}} ]} ]; - var serverClientBidders = s2sTesting.getSourceBidderMap(adUnits); + var serverClientBidders = s2sTesting.getSourceBidderMap(adUnits, []); expect(serverClientBidders.server).to.have.members(['appnexus', 'bidder3']); expect(serverClientBidders.server).to.not.have.members(['rubicon']); @@ -305,8 +301,6 @@ describe('s2sTesting', function () { describe('setting source through s2sconfig and adUnits', function () { beforeEach(function () { - // reset s2sconfig bid sources - config.setConfig({s2sConfig: {testing: true}}); // set random number for testing s2sTesting.globalRand = 0.7; }); @@ -321,15 +315,15 @@ describe('s2sTesting', function () { ]; // set rubicon: client and appnexus: server - config.setConfig({s2sConfig: { + const s2sConfig = { bidders: ['rubicon', 'appnexus'], testing: true, bidderControl: { rubicon: {bidSource: {server: 2, client: 1}}, appnexus: {bidSource: {server: 1}} } - }}); - + } + s2sTesting.calculateBidSources(s2sConfig); var serverClientBidders = s2sTesting.getSourceBidderMap(adUnits); expect(serverClientBidders.server).to.have.members(['rubicon', 'appnexus']); expect(serverClientBidders.client).to.have.members(['rubicon', 'appnexus']); diff --git a/test/spec/modules/sharedIdSystem_spec.js b/test/spec/modules/sharedIdSystem_spec.js new file mode 100644 index 00000000000..904d6fe1c78 --- /dev/null +++ b/test/spec/modules/sharedIdSystem_spec.js @@ -0,0 +1,55 @@ +import { + sharedIdSubmodule, +} from 'modules/sharedIdSystem.js'; +import { server } from 'test/mocks/xhr.js'; + +let expect = require('chai').expect; + +describe('SharedId System', function() { + const SHAREDID_RESPONSE = {sharedId: 'testsharedid'}; + + describe('Xhr Requests from getId()', function() { + let callbackSpy = sinon.spy(); + + beforeEach(function() { + callbackSpy.resetHistory(); + }); + + afterEach(function () { + + }); + + it('should call shared id endpoint without consent data and handle a valid response', function () { + let submoduleCallback = sharedIdSubmodule.getId(undefined, undefined).callback; + submoduleCallback(callbackSpy); + + let request = server.requests[0]; + expect(request.url).to.equal('https://id.sharedid.org/id'); + expect(request.withCredentials).to.be.true; + + request.respond(200, {}, JSON.stringify(SHAREDID_RESPONSE)); + + expect(callbackSpy.calledOnce).to.be.true; + expect(callbackSpy.lastCall.lastArg.id).to.equal(SHAREDID_RESPONSE.sharedId); + }); + + it('should call shared id endpoint with consent data and handle a valid response', function () { + let consentData = { + gdprApplies: true, + consentString: 'abc12345234', + }; + + let submoduleCallback = sharedIdSubmodule.getId(undefined, consentData).callback; + submoduleCallback(callbackSpy); + + let request = server.requests[0]; + expect(request.url).to.equal('https://id.sharedid.org/id?gdpr=1&gdpr_consent=abc12345234'); + expect(request.withCredentials).to.be.true; + + request.respond(200, {}, JSON.stringify(SHAREDID_RESPONSE)); + + expect(callbackSpy.calledOnce).to.be.true; + expect(callbackSpy.lastCall.lastArg.id).to.equal(SHAREDID_RESPONSE.sharedId); + }); + }); +}); diff --git a/test/spec/modules/sharethroughBidAdapter_spec.js b/test/spec/modules/sharethroughBidAdapter_spec.js index d45d1e977e6..b3451a09dde 100644 --- a/test/spec/modules/sharethroughBidAdapter_spec.js +++ b/test/spec/modules/sharethroughBidAdapter_spec.js @@ -1,6 +1,7 @@ import { expect } from 'chai'; import { sharethroughAdapterSpec, sharethroughInternal } from 'modules/sharethroughBidAdapter.js'; import { newBidder } from 'src/adapters/bidderFactory.js'; +import * as utils from '../../../src/utils.js'; const spec = newBidder(sharethroughAdapterSpec).getSpec(); const bidRequests = [ @@ -14,7 +15,21 @@ const bidRequests = [ }, userId: { tdid: 'fake-tdid', - pubcid: 'fake-pubcid' + pubcid: 'fake-pubcid', + idl_env: 'fake-identity-link', + id5id: { + uid: 'fake-id5id', + ext: { + linkType: 2 + } + }, + sharedid: { + id: 'fake-sharedid', + third: 'fake-sharedthird' + }, + lipb: { + lipbid: 'fake-lipbid' + } }, crumbs: { pubcid: 'fake-pubcid-in-crumbs-obj' @@ -40,12 +55,32 @@ const bidRequests = [ iframe: true, iframeSize: [500, 500] } - } + }, + { + bidder: 'sharethrough', + bidId: 'bidId4', + sizes: [[700, 400]], + placementCode: 'bar', + params: { + pkey: 'dddd4444', + badv: ['domain1.com', 'domain2.com'] + } + }, + { + bidder: 'sharethrough', + bidId: 'bidId5', + sizes: [[700, 400]], + placementCode: 'bar', + params: { + pkey: 'eeee5555', + bcat: ['IAB1-1', 'IAB1-2'] + } + }, ]; const prebidRequests = [ { - method: 'GET', + method: 'POST', url: 'https://btlr.sharethrough.com/WYu2BXv1/v1', data: { bidId: 'bidId', @@ -57,7 +92,7 @@ const prebidRequests = [ } }, { - method: 'GET', + method: 'POST', url: 'https://btlr.sharethrough.com/WYu2BXv1/v1', data: { bidId: 'bidId', @@ -69,7 +104,7 @@ const prebidRequests = [ } }, { - method: 'GET', + method: 'POST', url: 'https://btlr.sharethrough.com/WYu2BXv1/v1', data: { bidId: 'bidId', @@ -82,7 +117,7 @@ const prebidRequests = [ } }, { - method: 'GET', + method: 'POST', url: 'https://btlr.sharethrough.com/WYu2BXv1/v1', data: { bidId: 'bidId', @@ -94,7 +129,7 @@ const prebidRequests = [ } }, { - method: 'GET', + method: 'POST', url: 'https://btlr.sharethrough.com/WYu2BXv1/v1', data: { bidId: 'bidId', @@ -225,7 +260,7 @@ describe('sharethrough adapter spec', function() { expect(builtBidRequests[0].url).to.eq('https://btlr.sharethrough.com/WYu2BXv1/v1'); expect(builtBidRequests[1].url).to.eq('https://btlr.sharethrough.com/WYu2BXv1/v1'); - expect(builtBidRequests[0].method).to.eq('GET'); + expect(builtBidRequests[0].method).to.eq('POST'); }); it('should set the instant_play_capable parameter correctly based on browser userAgent string', function() { @@ -308,6 +343,15 @@ describe('sharethrough adapter spec', function() { expect(bidRequest.data.pubcid).to.eq('fake-pubcid'); }); + it('should add the pubcid parameter if a bid request contains a value for the Publisher Common ID Module in the' + + ' crumbs object of the bidrequest', function() { + const bidData = utils.deepClone(bidRequests); + delete bidData[0].userId.pubcid; + + const bidRequest = spec.buildRequests(bidData)[0]; + expect(bidRequest.data.pubcid).to.eq('fake-pubcid-in-crumbs-obj'); + }); + it('should add the pubcid parameter if a bid request contains a value for the Publisher Common ID Module in the' + ' crumbs object of the bidrequest', function() { const bidRequest = spec.buildRequests(bidRequests)[0]; @@ -315,6 +359,28 @@ describe('sharethrough adapter spec', function() { expect(bidRequest.data.pubcid).to.eq('fake-pubcid'); }); + it('should add the idluid parameter if a bid request contains a value for Identity Link from Live Ramp', function() { + const bidRequest = spec.buildRequests(bidRequests)[0]; + expect(bidRequest.data.idluid).to.eq('fake-identity-link'); + }); + + it('should add the id5uid parameter if a bid request contains a value for ID5', function() { + const bidRequest = spec.buildRequests(bidRequests)[0]; + expect(bidRequest.data.id5uid.id).to.eq('fake-id5id'); + expect(bidRequest.data.id5uid.linkType).to.eq(2); + }); + + it('should add the shduid parameter if a bid request contains a value for Shared ID', function() { + const bidRequest = spec.buildRequests(bidRequests)[0]; + expect(bidRequest.data.shduid.id).to.eq('fake-sharedid'); + expect(bidRequest.data.shduid.third).to.eq('fake-sharedthird'); + }); + + it('should add the liuid parameter if a bid request contains a value for LiveIntent ID', function() { + const bidRequest = spec.buildRequests(bidRequests)[0]; + expect(bidRequest.data.liuid).to.eq('fake-lipbid'); + }); + it('should add Sharethrough specific parameters', function() { const builtBidRequests = spec.buildRequests(bidRequests); expect(builtBidRequests[0]).to.deep.include({ @@ -346,6 +412,18 @@ describe('sharethrough adapter spec', function() { expect(builtBidRequest.data.schain).to.eq(JSON.stringify(bidRequest.schain)); }); + it('should add badv if provided', () => { + const builtBidRequest = spec.buildRequests([bidRequests[3]])[0]; + + expect(builtBidRequest.data.badv).to.have.members(['domain1.com', 'domain2.com']) + }); + + it('should add bcat if provided', () => { + const builtBidRequest = spec.buildRequests([bidRequests[4]])[0]; + + expect(builtBidRequest.data.bcat).to.have.members(['IAB1-1', 'IAB1-2']) + }); + it('should not add a supply chain parameter if schain is missing', function() { const bidRequest = spec.buildRequests(bidRequests)[0]; expect(bidRequest.data).to.not.include.any.keys('schain'); diff --git a/test/spec/modules/smartadserverBidAdapter_spec.js b/test/spec/modules/smartadserverBidAdapter_spec.js index e3bca240a47..749de43b9af 100644 --- a/test/spec/modules/smartadserverBidAdapter_spec.js +++ b/test/spec/modules/smartadserverBidAdapter_spec.js @@ -115,7 +115,41 @@ describe('Smart bid adapter tests', function () { ttl: 300, adUrl: 'http://awesome.fake.url', ad: '< --- awesome script --- >', - cSyncUrl: 'http://awesome.fake.csync.url' + cSyncUrl: 'http://awesome.fake.csync.url', + isNoAd: false + } + }; + + var BID_RESPONSE_IS_NO_AD = { + body: { + cpm: 12, + width: 300, + height: 250, + creativeId: 'zioeufg', + currency: 'GBP', + isNetCpm: true, + ttl: 300, + adUrl: 'http://awesome.fake.url', + ad: '< --- awesome script --- >', + cSyncUrl: 'http://awesome.fake.csync.url', + isNoAd: true + } + }; + + var BID_RESPONSE_IMAGE_SYNC = { + body: { + cpm: 12, + width: 300, + height: 250, + creativeId: 'zioeufg', + currency: 'GBP', + isNetCpm: true, + ttl: 300, + adUrl: 'http://awesome.fake.url', + ad: '< --- awesome script --- >', + cSyncUrl: 'http://awesome.fake.csync.url', + isNoAd: false, + dspPixels: ['pixelOne', 'pixelTwo', 'pixelThree'] } }; @@ -149,6 +183,18 @@ describe('Smart bid adapter tests', function () { expect(requestContent).to.have.property('ckid').and.to.equal(42); }); + it('Verify parse response with no ad', function () { + const request = spec.buildRequests(DEFAULT_PARAMS); + const bids = spec.interpretResponse(BID_RESPONSE_IS_NO_AD, request[0]); + expect(bids).to.have.lengthOf(0); + + expect(function () { + spec.interpretResponse(BID_RESPONSE_IS_NO_AD, { + data: 'invalid Json' + }) + }).to.not.throw(); + }); + it('Verify parse response', function () { const request = spec.buildRequests(DEFAULT_PARAMS); const bids = spec.interpretResponse(BID_RESPONSE, request[0]); @@ -258,6 +304,27 @@ describe('Smart bid adapter tests', function () { expect(syncs).to.have.lengthOf(0); }); + it('Verifies user sync using dspPixels', function () { + var syncs = spec.getUserSyncs({ + iframeEnabled: false, + pixelEnabled: true + }, [BID_RESPONSE_IMAGE_SYNC]); + expect(syncs).to.have.lengthOf(3); + expect(syncs[0].type).to.equal('image'); + + syncs = spec.getUserSyncs({ + iframeEnabled: false, + pixelEnabled: false + }, [BID_RESPONSE_IMAGE_SYNC]); + expect(syncs).to.have.lengthOf(0); + + syncs = spec.getUserSyncs({ + iframeEnabled: false, + pixelEnabled: true + }, []); + expect(syncs).to.have.lengthOf(0); + }); + describe('gdpr tests', function () { afterEach(function () { config.resetConfig(); diff --git a/test/spec/modules/smartxBidAdapter_spec.js b/test/spec/modules/smartxBidAdapter_spec.js index efc6abcc5fa..82c6642bd74 100644 --- a/test/spec/modules/smartxBidAdapter_spec.js +++ b/test/spec/modules/smartxBidAdapter_spec.js @@ -493,7 +493,7 @@ describe('The smartx adapter', function () { }; }); - it('should attempt to insert the EASI script', function () { + it('should attempt to insert the script', function () { var scriptTag; sinon.stub(window.document, 'getElementById').returns({ appendChild: sinon.stub().callsFake(function (script) { diff --git a/test/spec/modules/sovrnBidAdapter_spec.js b/test/spec/modules/sovrnBidAdapter_spec.js index 983ade4dd14..84a0069c0b4 100644 --- a/test/spec/modules/sovrnBidAdapter_spec.js +++ b/test/spec/modules/sovrnBidAdapter_spec.js @@ -4,11 +4,26 @@ import {newBidder} from 'src/adapters/bidderFactory.js'; const ENDPOINT = `https://ap.lijit.com/rtb/bid?src=$$REPO_AND_VERSION$$`; +const adUnitBidRequest = { + 'bidder': 'sovrn', + 'params': { + 'tagid': '403370' + }, + 'adUnitCode': 'adunit-code', + 'sizes': [ + [300, 250], + [300, 600] + ], + 'bidId': '30b31c1838de1e', + 'bidderRequestId': '22edbae2733bf6', + 'auctionId': '1d1a030790a475', +} + describe('sovrnBidAdapter', function() { const adapter = newBidder(spec); describe('isBidRequestValid', function () { - let bid = { + const bid = { 'bidder': 'sovrn', 'params': { 'tagid': '403370' @@ -276,6 +291,52 @@ describe('sovrnBidAdapter', function() { expect(data.user.ext.tpid[0].uid).to.equal('A_CRITEO_ID') expect(data.user.ext.prebid_criteoid).to.equal('A_CRITEO_ID') }); + + it('should ignore empty segments', function() { + const payload = JSON.parse(request.data) + expect(payload.imp[0].ext).to.be.undefined + }) + + it('should pass the segments param value as trimmed deal ids array', function() { + const segmentsRequests = [{ + 'bidder': 'sovrn', + 'params': { + 'segments': ' test1,test2 ' + }, + 'adUnitCode': 'adunit-code', + 'sizes': [ + [300, 250], + [300, 600] + ], + 'bidId': '30b31c1838de1e', + 'bidderRequestId': '22edbae2733bf6', + 'auctionId': '1d1a030790a475' + }] + const request = spec.buildRequests(segmentsRequests, bidderRequest) + const payload = JSON.parse(request.data) + expect(payload.imp[0].ext.deals[0]).to.equal('test1') + expect(payload.imp[0].ext.deals[1]).to.equal('test2') + }) + it('should use the floor provided from the floor module if present', function() { + const floorBid = {...adUnitBidRequest, getFloor: () => ({currency: 'USD', floor: 1.10})} + floorBid.params = { + tagid: 1234, + bidfloor: 2.00 + } + const request = spec.buildRequests([floorBid], bidderRequest) + const payload = JSON.parse(request.data) + expect(payload.imp[0].bidfloor).to.equal(1.10) + }) + it('should use the floor from the param if there is no floor from the floor module', function() { + const floorBid = {...adUnitBidRequest, getFloor: () => ({})} + floorBid.params = { + tagid: 1234, + bidfloor: 2.00 + } + const request = spec.buildRequests([floorBid], bidderRequest) + const payload = JSON.parse(request.data) + expect(payload.imp[0].bidfloor).to.equal(2.00) + }) }); describe('interpretResponse', function () { @@ -361,7 +422,7 @@ describe('sovrnBidAdapter', function() { }); it('should get correct bid response when ttl is set', function () { - response.body.seatbid[0].bid[0].ttl = 480; + response.body.seatbid[0].bid[0].ext = { 'ttl': 480 }; let expectedResponse = [{ 'requestId': '263c448586f5a1', diff --git a/test/spec/modules/spotxBidAdapter_spec.js b/test/spec/modules/spotxBidAdapter_spec.js index 798fb3eec10..927599ac986 100644 --- a/test/spec/modules/spotxBidAdapter_spec.js +++ b/test/spec/modules/spotxBidAdapter_spec.js @@ -204,9 +204,9 @@ describe('the spotx adapter', function () { eids: [{ source: 'id5-sync.com', uids: [{ - id: 'id5id_1' - }], - ext: {} + id: 'id5id_1', + ext: {} + }] }, { source: 'adserver.org', @@ -488,7 +488,7 @@ describe('the spotx adapter', function () { }); }); - describe('oustreamRender', function() { + describe('outstreamRender', function() { var serverResponse, bidderRequestObj; beforeEach(function() { @@ -545,7 +545,7 @@ describe('the spotx adapter', function () { it('should attempt to insert the EASI script', function() { var scriptTag; sinon.stub(window.document, 'getElementById').returns({ - appendChild: sinon.stub().callsFake(function(script) { scriptTag = script }) + appendChild: sinon.stub().callsFake(function(script) { scriptTag = script; }) }); var responses = spec.interpretResponse(serverResponse, bidderRequestObj); @@ -573,7 +573,7 @@ describe('the spotx adapter', function () { nodeName: 'IFRAME', contentDocument: { body: { - appendChild: sinon.stub().callsFake(function(script) { scriptTag = script }) + appendChild: sinon.stub().callsFake(function(script) { scriptTag = script; }) } } }); @@ -598,5 +598,42 @@ describe('the spotx adapter', function () { expect(scriptTag.getAttribute('data-spotx_content_height')).to.equal('300'); window.document.getElementById.restore(); }); + + it('should adjust width and height to match slot clientWidth if playersize_auto_adapt is used', function() { + var scriptTag; + sinon.stub(window.document, 'getElementById').returns({ + clientWidth: 200, + appendChild: sinon.stub().callsFake(function(script) { scriptTag = script; }) + }); + var responses = spec.interpretResponse(serverResponse, bidderRequestObj); + + responses[0].renderer.render(responses[0]); + + expect(scriptTag.getAttribute('type')).to.equal('text/javascript'); + expect(scriptTag.getAttribute('src')).to.equal('https://js.spotx.tv/easi/v1/12345.js'); + expect(scriptTag.getAttribute('data-spotx_content_width')).to.equal('200'); + expect(scriptTag.getAttribute('data-spotx_content_height')).to.equal('150'); + window.document.getElementById.restore(); + }); + + it('should use a default 4/3 ratio if playersize_auto_adapt is used and response does not contain width or height', function() { + delete serverResponse.body.seatbid[0].bid[0].w; + delete serverResponse.body.seatbid[0].bid[0].h; + + var scriptTag; + sinon.stub(window.document, 'getElementById').returns({ + clientWidth: 200, + appendChild: sinon.stub().callsFake(function(script) { scriptTag = script; }) + }); + var responses = spec.interpretResponse(serverResponse, bidderRequestObj); + + responses[0].renderer.render(responses[0]); + + expect(scriptTag.getAttribute('type')).to.equal('text/javascript'); + expect(scriptTag.getAttribute('src')).to.equal('https://js.spotx.tv/easi/v1/12345.js'); + expect(scriptTag.getAttribute('data-spotx_content_width')).to.equal('200'); + expect(scriptTag.getAttribute('data-spotx_content_height')).to.equal('150'); + window.document.getElementById.restore(); + }); }); }); diff --git a/test/spec/modules/sspBCAdapter_spec.js b/test/spec/modules/sspBCBidAdapter_spec.js similarity index 78% rename from test/spec/modules/sspBCAdapter_spec.js rename to test/spec/modules/sspBCBidAdapter_spec.js index 29718deb031..efbfa37f6ca 100644 --- a/test/spec/modules/sspBCAdapter_spec.js +++ b/test/spec/modules/sspBCBidAdapter_spec.js @@ -1,8 +1,6 @@ import { assert, expect } from 'chai'; -import { spec } from 'modules/sspBCAdapter.js'; +import { spec } from 'modules/sspBCBidAdapter.js'; import * as utils from 'src/utils.js'; -import * as sinon from 'sinon'; -import * as ajax from 'src/ajax.js'; const BIDDER_CODE = 'sspBC'; const BIDDER_URL = 'https://ssp.wp.pl/bidder/'; @@ -66,6 +64,28 @@ describe('SSPBC adapter', function () { transactionId, } ]; + const bid_OneCode = { + adUnitCode: 'test_wideboard', + bidder: BIDDER_CODE, + mediaTypes: { + banner: { + sizes: [ + [728, 90], + [750, 100], + [750, 200] + ] + } + }, + sizes: [ + [728, 90], + [750, 100], + [750, 200] + ], + auctionId, + bidderRequestId, + bidId: auctionId + '1', + transactionId, + }; const bids_timeouted = [{ adUnitCode: 'test_wideboard', bidder: BIDDER_CODE, @@ -144,6 +164,18 @@ describe('SSPBC adapter', function () { stack: ['https://test.site.pl/'], } }; + const bidRequestOneCode = { + auctionId, + bidderCode: BIDDER_CODE, + bidderRequestId, + bids: [bid_OneCode], + gdprConsent, + refererInfo: { + reachedTop: true, + referer: 'https://test.site.pl/', + stack: ['https://test.site.pl/'], + } + }; const bidRequestTest = { auctionId, bidderCode: BIDDER_CODE, @@ -174,6 +206,8 @@ describe('SSPBC adapter', function () { 'bid': [{ 'id': '3347324c-6889-46d2-a800-ae78a5214c06', 'impid': '003', + 'siteid': '8816', + 'slotid': '003', 'price': 1, 'adid': 'lxHWkB7OnZeso3QiN1N4', 'nurl': '', @@ -190,6 +224,8 @@ describe('SSPBC adapter', function () { 'bid': [{ 'id': '2d766853-ea07-4529-8299-5f0ebadc546a', 'impid': '005', + 'siteid': '8816', + 'slotid': '005', 'price': 2, 'adm': 'AD CODE 2', 'cid': '57744', @@ -210,6 +246,8 @@ describe('SSPBC adapter', function () { 'bid': [{ 'id': '3347324c-6889-46d2-a800-ae78a5214c06', 'impid': '003', + 'siteid': '8816', + 'slotid': '003', 'price': 1, 'adid': 'lxHWkB7OnZeso3QiN1N4', 'nurl': '', @@ -226,20 +264,50 @@ describe('SSPBC adapter', function () { 'cur': 'PLN' } }; + const serverResponseOneCode = { + 'body': { + 'id': auctionId, + 'seatbid': [{ + 'bid': [{ + 'id': '3347324c-6889-46d2-a800-ae78a5214c06', + 'impid': 'bidid-' + auctionId + '1', + 'price': 1, + 'adid': 'lxHWkB7OnZeso3QiN1N4', + 'nurl': '', + 'adm': 'AD CODE 1', + 'adomain': ['adomain.pl'], + 'cid': 'BZ4gAg21T5nNtxlUCDSW', + 'crid': 'lxHWkB7OnZeso3QiN1N4', + 'w': 728, + 'h': 90, + 'ext': { + 'siteid': '8816', + 'slotid': '003', + }, + }], + 'seat': 'dsp1', + 'group': 0 + }], + 'cur': 'PLN' + } + }; const emptyResponse = { 'body': { 'id': auctionId, } } return { + bid_OneCode, bids, bids_test, bids_timeouted, bidRequest, + bidRequestOneCode, bidRequestSingle, bidRequestTest, bidRequestTestNoGDPR, serverResponse, + serverResponseOneCode, serverResponseSingle, emptyResponse }; @@ -257,13 +325,10 @@ describe('SSPBC adapter', function () { const { bids } = prepareTestData(); let bid = bids[0]; - it('should return true when required params found', function () { + it('should always return true whether bid has params (standard) or not (OneCode)', function () { assert(spec.isBidRequestValid(bid)); - }); - - it('should return false when required params are missing', function () { bid.params.id = undefined; - assert.isFalse(spec.isBidRequestValid(bid)); + assert(spec.isBidRequestValid(bid)); }); }); @@ -308,9 +373,10 @@ describe('SSPBC adapter', function () { }); describe('interpretResponse', function () { - const { bids, emptyResponse, serverResponse, serverResponseSingle, bidRequest, bidRequestSingle } = prepareTestData(); + const { bid_OneCode, bids, emptyResponse, serverResponse, serverResponseOneCode, serverResponseSingle, bidRequest, bidRequestOneCode, bidRequestSingle } = prepareTestData(); const request = spec.buildRequests(bids, bidRequest); const requestSingle = spec.buildRequests([bids[0]], bidRequestSingle); + const requestOneCode = spec.buildRequests([bid_OneCode], bidRequestOneCode); it('should handle nobid responses', function () { let result = spec.interpretResponse(emptyResponse, request); @@ -326,6 +392,19 @@ describe('SSPBC adapter', function () { expect(resultSingle[0]).to.have.keys('ad', 'cpm', 'width', 'height', 'bidderCode', 'mediaType', 'meta', 'requestId', 'creativeId', 'currency', 'netRevenue', 'ttl'); }); + it('should create bid from OneCode (parameter-less) request, if response contains siteId', function () { + let resultOneCode = spec.interpretResponse(serverResponseOneCode, requestOneCode); + + expect(resultOneCode.length).to.equal(1); + expect(resultOneCode[0]).to.have.keys('ad', 'cpm', 'width', 'height', 'bidderCode', 'mediaType', 'meta', 'requestId', 'creativeId', 'currency', 'netRevenue', 'ttl'); + }); + + it('should not create bid from OneCode (parameter-less) request, if response does not contain siteId', function () { + let resultOneCodeNoMatch = spec.interpretResponse(serverResponse, requestOneCode); + + expect(resultOneCodeNoMatch.length).to.equal(0); + }); + it('should handle a partial response', function () { let resultPartial = spec.interpretResponse(serverResponseSingle, request); expect(resultPartial.length).to.equal(1); @@ -364,16 +443,16 @@ describe('SSPBC adapter', function () { expect(notificationPayload).to.be.undefined; }); - it('should generate notification with event name and request/site/slot data, if correct bid is provided', function () { + it('should generate notification with event name and request/adUnit data, if correct bid is provided. Should also contain site/slot data as arrays.', function () { const { bids } = prepareTestData(); let bid = bids[0]; - bid.params = [bid.params]; let notificationPayload = spec.onBidWon(bid); expect(notificationPayload).to.have.property('event').that.equals('bidWon'); expect(notificationPayload).to.have.property('requestId').that.equals(bid.auctionId); - expect(notificationPayload).to.have.property('siteId').that.deep.equals([bid.params[0].siteId]); - expect(notificationPayload).to.have.property('slotId').that.deep.equals([bid.params[0].id]); + expect(notificationPayload).to.have.property('adUnit').that.deep.equals([bid.adUnitCode]); + expect(notificationPayload).to.have.property('siteId').that.is.an('array'); + expect(notificationPayload).to.have.property('slotId').that.is.an('array'); }); }); @@ -388,13 +467,11 @@ describe('SSPBC adapter', function () { it('should generate single notification for any number of timeouted bids', function () { const { bids_timeouted } = prepareTestData(); - let notificationPayload = spec.onTimeout(bids_timeouted); expect(notificationPayload).to.have.property('event').that.equals('timeout'); expect(notificationPayload).to.have.property('requestId').that.equals(bids_timeouted[0].auctionId); - expect(notificationPayload).to.have.property('siteId').that.deep.equals([bids_timeouted[0].params[0].siteId]); - expect(notificationPayload).to.have.property('slotId').that.deep.equals([bids_timeouted[0].params[0].id, bids_timeouted[1].params[0].id]); + expect(notificationPayload).to.have.property('adUnit').that.deep.equals([bids_timeouted[0].adUnitCode, bids_timeouted[1].adUnitCode]); }); }); }); diff --git a/test/spec/modules/stroeerCoreBidAdapter_spec.js b/test/spec/modules/stroeerCoreBidAdapter_spec.js index dd02ebb7c8e..4b028d563d3 100644 --- a/test/spec/modules/stroeerCoreBidAdapter_spec.js +++ b/test/spec/modules/stroeerCoreBidAdapter_spec.js @@ -2,6 +2,7 @@ import {assert} from 'chai'; import {spec} from 'modules/stroeerCoreBidAdapter.js'; import * as utils from 'src/utils.js'; import {BANNER, VIDEO} from '../../../src/mediaTypes.js'; +import find from 'core-js-pure/features/array/find.js'; describe('stroeerCore bid adapter', function () { let sandbox; @@ -123,7 +124,7 @@ describe('stroeerCore bid adapter', function () { } }, referrer, - getElementById: id => placementElements.find(el => el.id === id) + getElementById: id => find(placementElements, el => el.id === id) } }; diff --git a/test/spec/modules/sublimeBidAdapter_spec.js b/test/spec/modules/sublimeBidAdapter_spec.js index 008f24730bc..a0765a0d396 100644 --- a/test/spec/modules/sublimeBidAdapter_spec.js +++ b/test/spec/modules/sublimeBidAdapter_spec.js @@ -1,5 +1,5 @@ import { expect } from 'chai'; -import { spec, sendEvent, log, setState, state } from 'modules/sublimeBidAdapter.js'; +import { spec } from 'modules/sublimeBidAdapter.js'; import { newBidder } from 'src/adapters/bidderFactory.js'; let utils = require('src/utils'); @@ -9,6 +9,16 @@ describe('Sublime Adapter', function() { describe('sendEvent', function() { let sandbox; + const triggeredPixelProperties = [ + 't', + 'tse', + 'z', + 'e', + 'src', + 'puid', + 'trId', + 'pbav', + ]; beforeEach(function () { sandbox = sinon.sandbox.create(); @@ -16,8 +26,10 @@ describe('Sublime Adapter', function() { it('should trigger pixel', function () { sandbox.spy(utils, 'triggerPixel'); - sendEvent('test', true); + spec.sendEvent('test'); expect(utils.triggerPixel.called).to.equal(true); + const params = utils.parseUrl(utils.triggerPixel.args[0][0]).search; + expect(Object.keys(params)).to.have.members(triggeredPixelProperties); }); afterEach(function () { @@ -94,8 +106,9 @@ describe('Sublime Adapter', function() { }); it('should contains a request id equals to the bid id', function() { - expect(request[0].data.requestId).to.equal(bidRequests[0].bidId); - expect(request[1].data.requestId).to.equal(bidRequests[1].bidId); + for (let i = 0; i < request.length; i = i + 1) { + expect(JSON.parse(request[i].data).requestId).to.equal(bidRequests[i].bidId); + } }); it('should have an url that contains bid keyword', function() { @@ -149,7 +162,7 @@ describe('Sublime Adapter', function() { currency: 'USD', netRevenue: true, ttl: 600, - pbav: '0.6.0', + pbav: '0.7.0', ad: '', }, ]; @@ -191,7 +204,7 @@ describe('Sublime Adapter', function() { netRevenue: true, ttl: 600, ad: '', - pbav: '0.6.0', + pbav: '0.7.0', }; expect(result[0]).to.deep.equal(expectedResponse); @@ -241,7 +254,7 @@ describe('Sublime Adapter', function() { netRevenue: true, ttl: 600, ad: '', - pbav: '0.6.0', + pbav: '0.7.0', }; expect(result[0]).to.deep.equal(expectedResponse); @@ -279,4 +292,24 @@ describe('Sublime Adapter', function() { }); }); }); + + describe('onBidWon', function() { + let sandbox; + let bid = { foo: 'bar' }; + + beforeEach(function () { + sandbox = sinon.sandbox.create(); + }); + + it('should trigger "bidwon" pixel', function () { + sandbox.spy(utils, 'triggerPixel'); + spec.onBidWon(bid); + const params = utils.parseUrl(utils.triggerPixel.args[0][0]).search; + expect(params.e).to.equal('bidwon'); + }); + + afterEach(function () { + sandbox.restore(); + }); + }) }); diff --git a/test/spec/modules/synacormediaBidAdapter_spec.js b/test/spec/modules/synacormediaBidAdapter_spec.js index dd40e634723..5ec8700ce89 100644 --- a/test/spec/modules/synacormediaBidAdapter_spec.js +++ b/test/spec/modules/synacormediaBidAdapter_spec.js @@ -11,12 +11,19 @@ describe('synacormediaBidAdapter ', function () { sizes: [300, 250], params: { seatId: 'prebid', - placementId: '1234' + tagId: '1234' } }; }); it('should return true when params placementId and seatId are truthy', function () { + bid.params.placementId = bid.params.tagId; + delete bid.params.tagId; + assert(spec.isBidRequestValid(bid)); + }); + + it('should return true when params tagId and seatId are truthy', function () { + delete bid.params.placementId; assert(spec.isBidRequestValid(bid)); }); @@ -35,8 +42,9 @@ describe('synacormediaBidAdapter ', function () { assert.isFalse(spec.isBidRequestValid(bid)); }); - it('should return false when placementId param is missing', function () { + it('should return false when both placementId param and tagId param are missing', function () { delete bid.params.placementId; + delete bid.params.tagId; assert.isFalse(spec.isBidRequestValid(bid)); }); @@ -53,7 +61,7 @@ describe('synacormediaBidAdapter ', function () { sizes: [[300, 250], [300, 600]], params: { seatId: 'prebid', - placementId: '1234', + tagId: '1234', bidfloor: '0.50' } }; @@ -63,7 +71,7 @@ describe('synacormediaBidAdapter ', function () { sizes: [[300, 250], [300, 600]], params: { seatId: 'prebid', - placementId: '1234', + tagId: '1234', bidfloor: '0.50' }, mediaTypes: { @@ -84,7 +92,7 @@ describe('synacormediaBidAdapter ', function () { sizes: [[640, 480]], params: { seatId: 'prebid', - placementId: '1234', + tagId: '1234', bidfloor: '0.50' }, mediaTypes: { @@ -110,7 +118,7 @@ describe('synacormediaBidAdapter ', function () { bidder: 'synacormedia', params: { seatId: 'prebid', - placementId: '1234', + tagId: '1234', video: { minduration: 30 } @@ -163,7 +171,7 @@ describe('synacormediaBidAdapter ', function () { sizes: [[300, 250], [300, 600]], params: { seatId: 'prebid', - placementId: '1234', + tagId: '1234', bidfloor: '0.50' } }; @@ -230,7 +238,7 @@ describe('synacormediaBidAdapter ', function () { sizes: [[300, 600]], params: { seatId: validBidRequest.params.seatId, - placementId: '5678', + tagId: '5678', bidfloor: '0.50' } }; @@ -262,7 +270,7 @@ describe('synacormediaBidAdapter ', function () { sizes: [[300, 250]], params: { seatId: 'somethingelse', - placementId: '5678', + tagId: '5678', bidfloor: '0.50' } }; @@ -295,7 +303,7 @@ describe('synacormediaBidAdapter ', function () { sizes: [[300, 250]], params: { seatId: 'prebid', - placementId: '1234', + tagId: '1234', bidfloor: 'abcd' } }; @@ -327,7 +335,7 @@ describe('synacormediaBidAdapter ', function () { sizes: [[300, 250]], params: { seatId: 'prebid', - placementId: '1234' + tagId: '1234' } }; let req = spec.buildRequests([badFloorBidRequest], bidderRequest); @@ -358,7 +366,7 @@ describe('synacormediaBidAdapter ', function () { sizes: [[300, 250]], params: { seatId: 'prebid', - placementId: '1234', + tagId: '1234', pos: 1 } }; @@ -390,7 +398,7 @@ describe('synacormediaBidAdapter ', function () { sizes: [[300, 250]], params: { seatId: 'prebid', - placementId: '1234', + tagId: '1234', } }; let req = spec.buildRequests([newPosBidRequest], bidderRequest); @@ -425,7 +433,7 @@ describe('synacormediaBidAdapter ', function () { sizes: [], params: { seatId: 'prebid', - placementId: '1234', + tagId: '1234', bidfloor: '0.50' } }; @@ -435,7 +443,7 @@ describe('synacormediaBidAdapter ', function () { sizes: [[300]], params: { seatId: 'prebid', - placementId: '1234', + tagId: '1234', bidfloor: '0.50' } }; @@ -457,7 +465,7 @@ describe('synacormediaBidAdapter ', function () { bidder: 'synacormedia', params: { seatId: 'prebid', - placementId: '1234', + tagId: '1234', video: { minduration: 30, maxduration: 45, @@ -515,7 +523,7 @@ describe('synacormediaBidAdapter ', function () { bidder: 'synacormedia', params: { seatId: 'prebid', - placementId: '1234', + tagId: '1234', video: { minduration: 30, maxduration: 45, @@ -582,12 +590,74 @@ describe('synacormediaBidAdapter ', function () { }) }); + describe('Bid Requests with placementId should be backward compatible ', function () { + let validVideoBidReq = { + bidder: 'synacormedia', + params: { + seatId: 'prebid', + placementId: 'demo1', + pos: 1, + video: {} + }, + renderer: { + url: '../syncOutstreamPlayer.js' + }, + mediaTypes: { + video: { + playerSize: [[300, 250]], + context: 'outstream' + } + }, + adUnitCode: 'div-1', + transactionId: '0869f34e-090b-4b20-84ee-46ff41405a39', + sizes: [[300, 250]], + bidId: '22b3a2268d9f0e', + bidderRequestId: '1d195910597e13', + auctionId: '3375d336-2aea-4ee7-804c-6d26b621ad20', + src: 'client', + bidRequestsCount: 1, + bidderRequestsCount: 1, + bidderWinsCount: 0 + }; + + let validBannerBidRequest = { + bidId: '9876abcd', + sizes: [[300, 250]], + params: { + seatId: 'prebid', + placementId: '1234', + } + }; + + let bidderRequest = { + refererInfo: { + referer: 'http://localhost:9999/' + }, + bidderCode: 'synacormedia', + auctionId: 'f8a75621-d672-4cbb-9275-3db7d74fb110' + }; + + it('should return valid bid request for banner impression', function () { + let req = spec.buildRequests([validBannerBidRequest], bidderRequest); + expect(req).to.have.property('method', 'POST'); + expect(req).to.have.property('url'); + expect(req.url).to.contain('//prebid.technoratimedia.com/openrtb/bids/prebid?src=$$REPO_AND_VERSION$$'); + }); + + it('should return valid bid request for video impression', function () { + let req = spec.buildRequests([validVideoBidReq], bidderRequest); + expect(req).to.have.property('method', 'POST'); + expect(req).to.have.property('url'); + expect(req.url).to.contain('//prebid.technoratimedia.com/openrtb/bids/prebid?src=$$REPO_AND_VERSION$$'); + }); + }); + describe('Bid Requests with schain object ', function() { let validBidReq = { bidder: 'synacormedia', params: { seatId: 'prebid', - placementId: 'demo1', + tagId: 'demo1', pos: 1, video: {} }, @@ -634,7 +704,7 @@ describe('synacormediaBidAdapter ', function () { bidder: 'synacormedia', params: { seatId: 'prebid', - placementId: 'demo1', + tagId: 'demo1', pos: 1, video: {} }, @@ -914,8 +984,8 @@ describe('synacormediaBidAdapter ', function () { }); let resp = spec.interpretResponse(serverRespVideo, bidRequest); - sandbox.restore(); - expect(resp[0].videoCacheKey).to.not.exist; + sandbox.restore(); + expect(resp[0].videoCacheKey).to.not.exist; }); it('should use video bid request height and width if not present in response', function () { diff --git a/test/spec/modules/tapadIdSystem_spec.js b/test/spec/modules/tapadIdSystem_spec.js new file mode 100644 index 00000000000..bc31f1d37ba --- /dev/null +++ b/test/spec/modules/tapadIdSystem_spec.js @@ -0,0 +1,65 @@ +import { tapadIdSubmodule, graphUrl } from 'modules/tapadIdSystem.js'; +import * as utils from 'src/utils.js'; + +import { server } from 'test/mocks/xhr.js'; + +describe('TapadIdSystem', function () { + describe('getId', function() { + const config = { params: { companyId: 12345 } }; + it('should call to real time graph endpoint and handle valid response', function() { + const callbackSpy = sinon.spy(); + const callback = tapadIdSubmodule.getId(config).callback; + callback(callbackSpy); + + const request = server.requests[0]; + expect(request.url).to.eq(`${graphUrl}?company_id=12345&tapad_id_type=TAPAD_ID`); + + request.respond(200, { 'Content-Type': 'application/json' }, JSON.stringify({ tapadId: 'your-tapad-id' })); + expect(callbackSpy.lastCall.lastArg).to.eq('your-tapad-id'); + }); + + it('should remove stored tapadId if not found', function() { + const callbackSpy = sinon.spy(); + const callback = tapadIdSubmodule.getId(config).callback; + callback(callbackSpy); + + const request = server.requests[0]; + + request.respond(404); + expect(callbackSpy.lastCall.lastArg).to.be.undefined; + }); + + it('should log message with invalid company id', function() { + const logMessageSpy = sinon.spy(utils, 'logMessage'); + const callbackSpy = sinon.spy(); + const callback = tapadIdSubmodule.getId(config).callback; + callback(callbackSpy); + + const request = server.requests[0]; + + request.respond(403); + expect(logMessageSpy.lastCall.lastArg).to.eq('Invalid Company Id. Contact prebid@tapad.com for assistance.'); + logMessageSpy.restore(); + }); + + it('should log message if company id not given', function() { + const logMessageSpy = sinon.spy(utils, 'logMessage'); + const callbackSpy = sinon.spy(); + const callback = tapadIdSubmodule.getId({}).callback; + callback(callbackSpy); + + expect(logMessageSpy.lastCall.lastArg).to.eq('Please provide a valid Company Id. Contact prebid@tapad.com for assistance.'); + logMessageSpy.restore(); + }); + + it('should log message if company id is incorrect format', function() { + const logMessageSpy = sinon.spy(utils, 'logMessage'); + const callbackSpy = sinon.spy(); + const callback = tapadIdSubmodule.getId({ params: { companyId: 'notANumber' } }).callback; + callback(callbackSpy); + + expect(logMessageSpy.lastCall.lastArg).to.eq('Please provide a valid Company Id. Contact prebid@tapad.com for assistance.'); + logMessageSpy.restore(); + }); + }); +}) diff --git a/test/spec/modules/tappxBidAdapter_spec.js b/test/spec/modules/tappxBidAdapter_spec.js new file mode 100644 index 00000000000..c4410d8ce5e --- /dev/null +++ b/test/spec/modules/tappxBidAdapter_spec.js @@ -0,0 +1,86 @@ +import { assert } from 'chai'; +import {spec} from 'modules/tappxBidAdapter'; + +describe('Tappx adapter tests', function () { + describe('isBidRequestValid', function () { + let bid = { bidder: 'tappx', params: { host: 'testing.ssp.tappx.com', tappxkey: 'pub-1234-test-1234', endpoint: 'ZZ1234PBJS', bidfloor: 0.005 } }; + + it('should return true when required params found', function () { + assert(spec.isBidRequestValid(bid)); + }); + + it('should return false when required params are missing', function () { + const bid = { + host: 'testing.ssp.tappx.com' + }; + assert.isFalse(spec.isBidRequestValid(bid)); + }); + }); + + describe('buildRequest', function () { + let consentString = 'BOJ8RZsOJ8RZsABAB8AAAAAZ+A=='; + // Web Test + let validBidRequests = [{'bidder': 'tappx', 'params': {'host': 'testing.ssp.tappx.com', 'tappxkey': 'pub-1234-android-1234', 'endpoint': 'ZZ1234PBJS', 'bidfloor': 0.005, 'api': [3, 5]}, 'crumbs': {'pubcid': 'df2144f7-673f-4440-83f5-cd4a73642d99'}, 'fpd': {'context': {'adServer': {'name': 'gam', 'adSlot': '/19968336/header-bid-tag-0'}, 'pbAdSlot': '/19968336/header-bid-tag-0'}}, 'mediaTypes': {'banner': {'sizes': [[320, 480]]}}, 'adUnitCode': 'div-1', 'transactionId': '713f2c01-61e3-45b5-9e4e-2b163033f3d6', 'sizes': [[320, 480]], 'bidId': '27818d05971607', 'bidderRequestId': '1320551a307df5', 'auctionId': '3f1281d3-9860-4657-808d-3c1d42231ef3', 'src': 'client', 'bidRequestsCount': 1, 'bidderRequestsCount': 1, 'bidderWinsCount': 0}] + // App Test + let validAppBidRequests = [{'bidder': 'tappx', 'params': {'host': 'testing.ssp.tappx.com', 'tappxkey': 'pub-1234-android-1234', 'endpoint': 'ZZ1234PBJS', 'bidfloor': 0.005, 'api': [3, 5], 'app': {'name': 'Tappx Test', 'bundle': 'com.test.tappx', 'domain': 'tappx.com', 'publisher': { 'name': 'Tappx', 'domain': 'tappx.com' }}}, 'crumbs': {'pubcid': 'df2144f7-673f-4440-83f5-cd4a73642d99'}, 'fpd': {'context': {'adServer': {'name': 'gam', 'adSlot': '/19968336/header-bid-tag-0'}, 'pbAdSlot': '/19968336/header-bid-tag-0'}}, 'mediaTypes': {'banner': {'sizes': [[320, 50]]}}, 'adUnitCode': 'div-1', 'transactionId': '713f2c01-61e3-45b5-9e4e-2b163033f3d6', 'sizes': [[320, 50]], 'bidId': '27818d05971607', 'bidderRequestId': '1320551a307df5', 'auctionId': '3f1281d3-9860-4657-808d-3c1d42231ef3', 'src': 'client', 'bidRequestsCount': 1, 'bidderRequestsCount': 1, 'bidderWinsCount': 0}] + let bidderRequest = {'bidderCode': 'tappx', 'auctionId': '6cca2192-2262-468b-8a3c-d00c58a5d911', 'bidderRequestId': '1ae5c6a02684df', 'bids': [{'bidder': 'tappx', 'params': {'host': 'tests.tappx.com', 'tappxkey': 'pub-1234-test-1234', 'endpoint': 'ZZ1234PBJS', 'bidfloor': 0.005}, 'fpd': {'context': {'adServer': {'name': 'gam', 'adSlot': '/19968336/header-bid-tag-0'}, 'pbAdSlot': '/19968336/header-bid-tag-0'}}, 'mediaTypes': {'banner': {'sizes': [[320, 480]]}}, 'adUnitCode': 'banner-ad-div', 'transactionId': 'c44cdbde-ab6d-47a0-8dde-6b4ff7909a35', 'sizes': [[320, 50]], 'bidId': '2e3a5feb30cfe4', 'bidderRequestId': '1ae5c6a02684df', 'auctionId': '6cca2192-2262-468b-8a3c-d00c58a5d911', 'src': 'client', 'bidRequestsCount': 1, 'bidderRequestsCount': 1, 'bidderWinsCount': 0}], 'auctionStart': 1611308859094, 'timeout': 700, 'refererInfo': {'referer': 'http://tappx.local:9999/integrationExamples/gpt/gdpr_hello_world.html?pbjs_debug=true', 'reachedTop': true, 'isAmp': false, 'numIframes': 0, 'stack': ['http://tappx.local:9999/integrationExamples/gpt/gdpr_hello_world.html?pbjs_debug=true'], 'canonicalUrl': null}, 'gdprConsent': {'consentString': consentString, 'vendorData': {'metadata': 'BO-JeiTPABAOkAAABAENABA', 'gdprApplies': true, 'hasGlobalScope': false, 'cookieVersion': 1, 'created': '2020-12-09T09:22:09.900Z', 'lastUpdated': '2021-01-14T15:44:03.600Z', 'cmpId': 0, 'cmpVersion': 1, 'consentScreen': 0, 'consentLanguage': 'EN', 'vendorListVersion': 1, 'maxVendorId': 0, 'purposeConsents': {}, 'vendorConsents': {}}, 'gdprApplies': true, 'apiVersion': 1}, 'uspConsent': '1YCC', 'start': 1611308859099}; + + it('should add gdpr/usp consent information to the request', function () { + const request = spec.buildRequests(validBidRequests, bidderRequest); + const payload = JSON.parse(request[0].data); + + expect(payload.regs.gdpr).to.exist.and.to.be.true; + expect(payload.regs.consent).to.exist.and.to.equal(consentString); + expect(payload.regs.ext.us_privacy).to.exist; + }); + + it('should properly build a banner request', function () { + const request = spec.buildRequests(validBidRequests, bidderRequest); + expect(request[0].url).to.match(/^(http|https):\/\/(.*)\.tappx\.com\/.+/); + expect(request[0].method).to.equal('POST'); + + const data = JSON.parse(request[0].data); + expect(data.site).to.not.equal(null); + expect(data.imp).to.have.lengthOf(1); + expect(data.imp[0].bidfloor, data).to.not.be.null; + expect(data.imp[0].banner).to.not.equal(null); + expect(data.imp[0].banner.w).to.be.oneOf([320, 50, 250, 480]); + expect(data.imp[0].banner.h).to.be.oneOf([320, 50, 250, 480]); + }); + + it('should properly build a banner request with app params', function () { + const request = spec.buildRequests(validAppBidRequests, bidderRequest); + expect(request[0].url).to.match(/^(http|https):\/\/(.*)\.tappx\.com\/.+/); + expect(request[0].method).to.equal('POST'); + + const data = JSON.parse(request[0].data); + expect(data.site).to.not.equal(null); + expect(data.imp).to.have.lengthOf(1); + expect(data.imp[0].bidfloor, data).to.not.be.null; + expect(data.imp[0].banner).to.not.equal(null); + expect(data.imp[0].banner.w).to.be.oneOf([320, 50, 250, 480]); + expect(data.imp[0].banner.h).to.be.oneOf([320, 50, 250, 480]); + }); + }); + + describe('interpretResponse', function () { + const bidRequest = { + data: {}, + bids: [ {'bidder': 'tappx', 'params': {'host': 'testing.ssp.tappx.com', 'tappxkey': 'pub-1234-android-1234', 'endpoint': 'ZZ1234PBJS', 'bidfloor': 0.05, 'api': [3, 5]}, 'crumbs': {'pubcid': 'df2144f7-673f-4440-83f5-cd4a73642d99'}, 'fpd': {'context': {'adServer': {'name': 'gam', 'adSlot': '/19968336/header-bid-tag-0'}, 'pbAdSlot': '/19968336/header-bid-tag-0'}}, 'mediaTypes': {'banner': {'sizes': [[320, 480]]}}, 'adUnitCode': 'div-1', 'transactionId': '47dd44e8-e7db-417c-a8f1-621a2e1a117d', 'sizes': [[320, 480]], 'bidId': '2170932097e505', 'bidderRequestId': '140ba7a1ab7aeb', 'auctionId': '1c54b4f1-645f-44e6-b8ae-5d43c923ef1c', 'src': 'client', 'bidRequestsCount': 1, 'bidderRequestsCount': 1, 'bidderWinsCount': 0} ] + }; + + const serverResponse = {'body': {'id': '1c54b4f1-645f-44e6-b8ae-5d43c923ef1c', 'bidid': 'bid3811165568213389257', 'seatbid': [{'seat': '1', 'group': 0, 'bid': [{'id': '3811165568213389257', 'impid': 1, 'price': 0.05, 'adm': "\t", 'w': 320, 'h': 480, 'lurl': 'http://testing.ssp.tappx.com/rtb/RTBv2Loss?id=3811165568213389257&ep=ZZ1234PBJS&au=test&bu=localhost&sz=320x480&pu=0.005&pt=0.01&cid=&crid=&adv=&aid=${AUCTION_ID}&bidid=${AUCTION_BID_ID}&impid=${AUCTION_IMP_ID}&sid=${AUCTION_SEAT_ID}&adid=${AUCTION_AD_ID}&ap=${AUCTION_PRICE}&cur=${AUCTION_CURRENCY}&mbr=${AUCTION_MBR}&l=${AUCTION_LOSS}', 'cid': '01744fbb521e9fb10ffea926190effea', 'crid': 'a13cf884e66e7c660afec059c89d98b6', 'adomain': []}]}], 'cur': 'USD'}, 'headers': {}}; + it('receive reponse with single placement', function () { + const bids = spec.interpretResponse(serverResponse, bidRequest); + const bid = bids[0]; + expect(bid.cpm).to.exist; + expect(bid.ad).to.match(/^', + 'ttl': 700, + 'ad': '' + } + ]; + let request = spec.buildRequests(bidRequests)[0]; + let result = spec.interpretResponse({body: response}, request); + expect(Object.keys(result[0])).to.have.members(Object.keys(expectedResponse[0])); + expect(result[0].cpm).to.not.equal(null); + expect(result[0].creativeId).to.not.equal(null); + expect(result[0].ad).to.not.equal(null); + expect(result[0].currency).to.equal('TRY'); + expect(result[0].netRevenue).to.equal(false); + }); + }) +}) diff --git a/test/spec/modules/trionBidAdapter_spec.js b/test/spec/modules/trionBidAdapter_spec.js index 596e8a3e2d9..ae329b4a028 100644 --- a/test/spec/modules/trionBidAdapter_spec.js +++ b/test/spec/modules/trionBidAdapter_spec.js @@ -146,47 +146,47 @@ describe('Trion adapter tests', function () { expect(bidUrlParams).to.include(getPublisherUrl()); }); - describe('webdriver', function () { - let originalWD; - - beforeEach(function () { - originalWD = window.navigator.webdriver; - }); - - afterEach(function () { - window.navigator['__defineGetter__']('webdriver', function () { - return originalWD; - }); - }); - - describe('is present', function () { - beforeEach(function () { - window.navigator['__defineGetter__']('webdriver', function () { - return 1; - }); - }); - - it('when there is non human traffic', function () { - let bidRequests = spec.buildRequests(TRION_BID_REQUEST); - let bidUrlParams = bidRequests[0].data; - expect(bidUrlParams).to.include('tr_wd=1'); - }); - }); - - describe('is not present', function () { - beforeEach(function () { - window.navigator['__defineGetter__']('webdriver', function () { - return 0; - }); - }); - - it('when there is not non human traffic', function () { - let bidRequests = spec.buildRequests(TRION_BID_REQUEST); - let bidUrlParams = bidRequests[0].data; - expect(bidUrlParams).to.include('tr_wd=0'); - }); - }); - }); + // describe('webdriver', function () { + // let originalWD; + + // beforeEach(function () { + // originalWD = window.navigator.webdriver; + // }); + + // afterEach(function () { + // window.navigator['__defineGetter__']('webdriver', function () { + // return originalWD; + // }); + // }); + + // describe('is present', function () { + // beforeEach(function () { + // window.navigator['__defineGetter__']('webdriver', function () { + // return 1; + // }); + // }); + + // it('when there is non human traffic', function () { + // let bidRequests = spec.buildRequests(TRION_BID_REQUEST); + // let bidUrlParams = bidRequests[0].data; + // expect(bidUrlParams).to.include('tr_wd=1'); + // }); + // }); + + // describe('is not present', function () { + // beforeEach(function () { + // window.navigator['__defineGetter__']('webdriver', function () { + // return 0; + // }); + // }); + + // it('when there is not non human traffic', function () { + // let bidRequests = spec.buildRequests(TRION_BID_REQUEST); + // let bidUrlParams = bidRequests[0].data; + // expect(bidUrlParams).to.include('tr_wd=0'); + // }); + // }); + // }); describe('document', function () { let originalHD; diff --git a/test/spec/modules/tripleliftBidAdapter_spec.js b/test/spec/modules/tripleliftBidAdapter_spec.js index b417876f276..eb410c2525d 100644 --- a/test/spec/modules/tripleliftBidAdapter_spec.js +++ b/test/spec/modules/tripleliftBidAdapter_spec.js @@ -143,6 +143,14 @@ describe('triplelift adapter', function () { auctionId: '1d1a030790a475', userId: {}, schain, + fpd: { + context: { + pbAdSlot: 'homepage-top-rect', + data: { + adUnitSpecificAttribute: 123 + } + } + } }, { bidder: 'triplelift', @@ -498,15 +506,16 @@ describe('triplelift adapter', function () { }); }); - it('should add user ids from multiple bid requests', function () { + it('should consolidate user ids from multiple bid requests', function () { const tdidId = '6bca7f6b-a98a-46c0-be05-6020f7604598'; const idlEnvId = 'XY6104gr0njcH9UDIR7ysFFJcm2XNpqeJTYslleJ_cMlsFOfZI'; const criteoId = '53e30ea700424f7bbdd793b02abc5d7'; + const pubcid = '3261d8ad-435d-481d-abd1-9f1a9ec99f0e'; const bidRequestsMultiple = [ - { ...bidRequests[0], userId: { tdid: tdidId } }, - { ...bidRequests[0], userId: { idl_env: idlEnvId } }, - { ...bidRequests[0], userId: { criteoId: criteoId } } + { ...bidRequests[0], userId: { tdid: tdidId, idl_env: idlEnvId, criteoId, pubcid } }, + { ...bidRequests[0], userId: { tdid: tdidId, idl_env: idlEnvId, criteoId, pubcid } }, + { ...bidRequests[0], userId: { tdid: tdidId, idl_env: idlEnvId, criteoId, pubcid } } ]; const request = tripleliftAdapterSpec.buildRequests(bidRequestsMultiple, bidderRequest); @@ -541,10 +550,22 @@ describe('triplelift adapter', function () { ext: { rtiPartner: 'criteoId' } } ] + }, + { + source: 'pubcid.org', + uids: [ + { + id: '3261d8ad-435d-481d-abd1-9f1a9ec99f0e', + ext: { rtiPartner: 'pubcid' } + } + ] } ] } }); + + expect(payload.user.ext.eids).to.be.an('array'); + expect(payload.user.ext.eids).to.have.lengthOf(4); }); it('should return a query string for TL call', function () { @@ -589,25 +610,68 @@ describe('triplelift adapter', function () { expect(payload.ext).to.deep.equal(undefined); }); it('should get floor from floors module if available', function() { - const floorInfo = { - currency: 'USD', - floor: 1.99 - }; + let floorInfo; bidRequests[0].getFloor = () => floorInfo; - const request = tripleliftAdapterSpec.buildRequests(bidRequests, bidderRequest); + + // standard float response; expected functionality of floors module + floorInfo = { currency: 'USD', floor: 1.99 }; + let request = tripleliftAdapterSpec.buildRequests(bidRequests, bidderRequest); + expect(request.data.imp[0].floor).to.equal(1.99); + + // if string response, convert to float + floorInfo = { currency: 'USD', floor: '1.99' }; + request = tripleliftAdapterSpec.buildRequests(bidRequests, bidderRequest); expect(request.data.imp[0].floor).to.equal(1.99); }); - it('should send fpd on root level ext if kvps are available', function() { + it('should call getFloor with the correct parameters based on mediaType', function() { + bidRequests.forEach(request => { + request.getFloor = () => {}; + sinon.spy(request, 'getFloor') + }); + + tripleliftAdapterSpec.buildRequests(bidRequests, bidderRequest); + + // banner + expect(bidRequests[0].getFloor.calledWith({ + currency: 'USD', + mediaType: 'banner', + size: '*' + })).to.be.true; + + // instream + expect(bidRequests[1].getFloor.calledWith({ + currency: 'USD', + mediaType: 'video', + size: '*' + })).to.be.true; + + // banner and incomplete video (POST will only include banner) + expect(bidRequests[3].getFloor.calledWith({ + currency: 'USD', + mediaType: 'banner', + size: '*' + })).to.be.true; + + // banner and instream (POST will only include video) + expect(bidRequests[5].getFloor.calledWith({ + currency: 'USD', + mediaType: 'video', + size: '*' + })).to.be.true; + }); + it('should send global config fpd if kvps are available', function() { const sens = null; const category = ['news', 'weather', 'hurricane']; const pmp_elig = 'true'; const fpd = { context: { - pmp_elig, - category, + pmp_elig: pmp_elig, + data: { + category: category + } }, user: { - sens, + sens: sens, } } sandbox.stub(config, 'getConfig').callsFake(key => { @@ -618,9 +682,16 @@ describe('triplelift adapter', function () { }); const request = tripleliftAdapterSpec.buildRequests(bidRequests, bidderRequest); const { data: payload } = request; - expect(payload.ext.fpd).to.not.haveOwnProperty('sens'); - expect(payload.ext.fpd).to.haveOwnProperty('category'); - expect(payload.ext.fpd).to.haveOwnProperty('pmp_elig'); + expect(payload.ext.fpd.user).to.not.exist; + expect(payload.ext.fpd.context.data).to.haveOwnProperty('category'); + expect(payload.ext.fpd.context).to.haveOwnProperty('pmp_elig'); + }); + it('should send ad unit fpd if kvps are available', function() { + const request = tripleliftAdapterSpec.buildRequests(bidRequests, bidderRequest); + expect(request.data.imp[0].fpd.context).to.haveOwnProperty('pbAdSlot'); + expect(request.data.imp[0].fpd.context).to.haveOwnProperty('data'); + expect(request.data.imp[0].fpd.context.data).to.haveOwnProperty('adUnitSpecificAttribute'); + expect(request.data.imp[1].fpd).to.not.exist; }); }); diff --git a/test/spec/modules/ucfunnelBidAdapter_spec.js b/test/spec/modules/ucfunnelBidAdapter_spec.js index b5f29287f81..a2939eb95c3 100644 --- a/test/spec/modules/ucfunnelBidAdapter_spec.js +++ b/test/spec/modules/ucfunnelBidAdapter_spec.js @@ -1,7 +1,6 @@ import { expect } from 'chai'; import { spec } from 'modules/ucfunnelBidAdapter.js'; import {BANNER, VIDEO, NATIVE} from 'src/mediaTypes.js'; - const URL = 'https://hb.aralego.com/header'; const BIDDER_CODE = 'ucfunnel'; @@ -9,6 +8,17 @@ const bidderRequest = { uspConsent: '1YNN' }; +const userId = { + 'criteoId': 'vYlICF9oREZlTHBGRVdrJTJCUUJnc3U2ckNVaXhrV1JWVUZVSUxzZmJlcnJZR0ZxbVhFRnU5bDAlMkJaUWwxWTlNcmdEeHFrJTJGajBWVlV4T3lFQ0FyRVcxNyUyQlIxa0lLSlFhcWJpTm9PSkdPVkx0JTJCbzlQRTQlM0Q', + 'id5id': {'uid': 'ID5-8ekgswyBTQqnkEKy0ErmeQ1GN5wV4pSmA-RE4eRedA'}, + 'netId': 'fH5A3n2O8_CZZyPoJVD-eabc6ECb7jhxCicsds7qSg', + 'parrableId': {'eid': '01.1608624401.fe44bca9b96873084a0d4e9d0ac5729f13790ba8f8e58fa4707b6b3c096df91c6b5f254992bdad4ab1dd4a89919081e9b877d7a039ac3183709277665bac124f28e277d109f0ff965058'}, + 'pubcid': 'd8aa10fa-d86c-451d-aad8-5f16162a9e64', + 'sharedid': {'id': '01ESHXW4HD29KMF387T63JQ9H5', 'third': '01ESHXW4HD29KMF387T63JQ9H5'}, + 'tdid': 'D6885E90-2A7A-4E0F-87CB-7734ED1B99A3', + 'haloId': {} +} + const validBannerBidReq = { bidder: BIDDER_CODE, params: { @@ -18,6 +28,7 @@ const validBannerBidReq = { sizes: [[300, 250]], bidId: '263be71e91dd9d', auctionId: '9ad1fa8d-2297-4660-a018-b39945054746', + userId: userId, 'schain': { 'ver': '1.0', 'complete': 1, @@ -50,7 +61,8 @@ const validBannerBidRes = { adm: '
', cpm: 1.01, height: 250, - width: 300 + width: 300, + crid: 'test-crid' }; const invalidBannerBidRes = ''; @@ -112,6 +124,13 @@ const validNativeBidRes = { width: 1 }; +const gdprConsent = { + consentString: 'CO9rhBTO9rhBTAcABBENBCCsAP_AAH_AACiQHItf_X_fb3_j-_59_9t0eY1f9_7_v20zjgeds-8Nyd_X_L8X42M7vB36pq4KuR4Eu3LBIQdlHOHcTUmw6IkVqTPsbk2Mr7NKJ7PEinMbe2dYGH9_n9XTuZKY79_s___z__-__v__7_f_r-3_3_vp9V---3YHIgEmGpfARZiWOBJNGlUKIEIVxIdACACihGFomsICVwU7K4CP0EDABAagIwIgQYgoxZBAAAAAElEQEgB4IBEARAIAAQAqQEIACNAEFgBIGAQACgGhYARQBCBIQZHBUcpgQESLRQTyVgCUXexhhCGUUANAg4AA.YAAAAAAAAAAA', + vendorData: {}, + gdprApplies: true, + apiVersion: 2 +}; + describe('ucfunnel Adapter', function () { describe('request', function () { it('should validate bid request', function () { @@ -253,18 +272,18 @@ describe('ucfunnel Adapter', function () { describe('cookie sync', function () { describe('cookie sync iframe', function () { - const result = spec.getUserSyncs({'iframeEnabled': true}); + const result = spec.getUserSyncs({'iframeEnabled': true}, null, gdprConsent); it('should return cookie sync iframe info', function () { expect(result[0].type).to.equal('iframe'); - expect(result[0].url).to.equal('https://cdn.aralego.net/ucfad/cookie/sync.html'); + expect(result[0].url).to.equal('https://cdn.aralego.net/ucfad/cookie/sync.html?gdpr=1&euconsent-v2=CO9rhBTO9rhBTAcABBENBCCsAP_AAH_AACiQHItf_X_fb3_j-_59_9t0eY1f9_7_v20zjgeds-8Nyd_X_L8X42M7vB36pq4KuR4Eu3LBIQdlHOHcTUmw6IkVqTPsbk2Mr7NKJ7PEinMbe2dYGH9_n9XTuZKY79_s___z__-__v__7_f_r-3_3_vp9V---3YHIgEmGpfARZiWOBJNGlUKIEIVxIdACACihGFomsICVwU7K4CP0EDABAagIwIgQYgoxZBAAAAAElEQEgB4IBEARAIAAQAqQEIACNAEFgBIGAQACgGhYARQBCBIQZHBUcpgQESLRQTyVgCUXexhhCGUUANAg4AA.YAAAAAAAAAAA&'); }); }); describe('cookie sync image', function () { - const result = spec.getUserSyncs({'pixelEnabled': true}); + const result = spec.getUserSyncs({'pixelEnabled': true}, null, gdprConsent); it('should return cookie sync image info', function () { expect(result[0].type).to.equal('image'); - expect(result[0].url).to.equal('https://sync.aralego.com/idSync'); + expect(result[0].url).to.equal('https://sync.aralego.com/idSync?gdpr=1&euconsent-v2=CO9rhBTO9rhBTAcABBENBCCsAP_AAH_AACiQHItf_X_fb3_j-_59_9t0eY1f9_7_v20zjgeds-8Nyd_X_L8X42M7vB36pq4KuR4Eu3LBIQdlHOHcTUmw6IkVqTPsbk2Mr7NKJ7PEinMbe2dYGH9_n9XTuZKY79_s___z__-__v__7_f_r-3_3_vp9V---3YHIgEmGpfARZiWOBJNGlUKIEIVxIdACACihGFomsICVwU7K4CP0EDABAagIwIgQYgoxZBAAAAAElEQEgB4IBEARAIAAQAqQEIACNAEFgBIGAQACgGhYARQBCBIQZHBUcpgQESLRQTyVgCUXexhhCGUUANAg4AA.YAAAAAAAAAAA&'); }); }); }); diff --git a/test/spec/modules/unicornBidAdapter_spec.js b/test/spec/modules/unicornBidAdapter_spec.js index 4c56c37700b..dcd446b2bb0 100644 --- a/test/spec/modules/unicornBidAdapter_spec.js +++ b/test/spec/modules/unicornBidAdapter_spec.js @@ -11,12 +11,12 @@ const bidRequests = [ }, mediaTypes: { banner: { - sizes: [[300, 250]] + sizes: [[300, 250], [336, 280]] } }, adUnitCode: '/19968336/header-bid-tag-0', transactionId: 'ea0aa332-a6e1-4474-8180-83720e6b87bc', - sizes: [[300, 250]], + sizes: [[300, 250], [336, 280]], bidId: '226416e6e6bf41', bidderRequestId: '1f41cbdcbe58d5', auctionId: '77987c3a-9be9-4e43-985a-26fc91d84724', @@ -81,12 +81,12 @@ const validBidRequests = [ }, mediaTypes: { banner: { - sizes: [[300, 250]] + sizes: [[300, 250], [336, 280]] } }, adUnitCode: '/19968336/header-bid-tag-0', transactionId: 'fbf94ccf-f377-4201-a662-32c2feb8ab6d', - sizes: [[300, 250]], + sizes: [[300, 250], [336, 280]], bidId: '2fb90842443e24', bidderRequestId: '123ae4cc3eeb7e', auctionId: 'c594a888-6744-46c6-8b0e-d188e40e83ef', @@ -156,12 +156,12 @@ const bidderRequest = { }, mediaTypes: { banner: { - sizes: [[300, 250]] + sizes: [[300, 250], [336, 280]] } }, adUnitCode: '/19968336/header-bid-tag-0', transactionId: 'fbf94ccf-f377-4201-a662-32c2feb8ab6d', - sizes: [[300, 250]], + sizes: [[300, 250], [336, 280]], bidId: '2fb90842443e24', bidderRequestId: '123ae4cc3eeb7e', auctionId: 'c594a888-6744-46c6-8b0e-d188e40e83ef', @@ -234,8 +234,18 @@ const openRTBRequest = { { id: '216255f234b602', banner: { - w: '300', - h: '250' + w: 300, + h: 250, + format: [ + { + w: 300, + h: 250 + }, + { + w: 336, + h: 280 + } + ] }, secure: 1, bidfloor: 0, @@ -244,8 +254,14 @@ const openRTBRequest = { { id: '31e2b28ced2475', banner: { - w: '300', - h: '250' + w: 300, + h: 250, + format: [ + { + w: 300, + h: 250 + } + ] }, secure: 1, bidfloor: 0, @@ -254,8 +270,14 @@ const openRTBRequest = { { id: '40a333e047a9bd', banner: { - w: '300', - h: '250' + w: 300, + h: 250, + format: [ + { + w: 300, + h: 250 + } + ] }, secure: 1, bidfloor: 0, @@ -287,7 +309,8 @@ const openRTBRequest = { source: { ext: { stype: 'prebid_uncn', - bidder: 'unicorn' + bidder: 'unicorn', + prebid_version: '1.0' } } }; @@ -378,7 +401,7 @@ const request = { method: 'POST', url: 'https://ds.uncn.jp/pb/0/bid.json', data: - '{"id":"5ebea288-f13a-4754-be6d-4ade66c68877","at":1,"imp":[{"id":"216255f234b602","banner":{"w":"300","h":"250"},"secure":1,"bidfloor":0,"tagid":"/19968336/header-bid-tag-0"},{"id":"31e2b28ced2475","banner":{"w":"300","h":"250"},"secure":1,"bidfloor":0"tagid":"/19968336/header-bid-tag-1"},{"id":"40a333e047a9bd","banner":{"w":"300","h":"250"},"secure":1,"bidfloor":0,"tagid":"/19968336/header-bid-tag-2"}],"cur":"JPY","site":{"id":"uni-corn.net","publisher":{"id":12345},"domain":"uni-corn.net","page":"https://uni-corn.net/","ref":"https://uni-corn.net/"},"device":{"language":"ja","ua":"Mozilla/5.0 (Linux; Android 8.0.0; ONEPLUS A5000) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.93 Mobile Safari/537.36"},"user":{"id":"69d9e1c2-801e-4901-a665-fad467550fec"},"bcat":[],"source":{"ext":{"stype":"prebid_uncn","bidder":"unicorn"}}}' + '{"id":"5ebea288-f13a-4754-be6d-4ade66c68877","at":1,"imp":[{"id":"216255f234b602","banner":{"w":300,"h":250},"format":[{"w":300,"h":250},{"w":336,"h":280}],"secure":1,"bidfloor":0,"tagid":"/19968336/header-bid-tag-0"},{"id":"31e2b28ced2475","banner":{"w":"300","h":"250"},"format":[{"w":"300","h":"250"}],"secure":1,"bidfloor":0"tagid":"/19968336/header-bid-tag-1"},{"id":"40a333e047a9bd","banner":{"w":300,"h":250},"format":[{"w":300,"h":250}],"secure":1,"bidfloor":0,"tagid":"/19968336/header-bid-tag-2"}],"cur":"JPY","site":{"id":"uni-corn.net","publisher":{"id":12345},"domain":"uni-corn.net","page":"https://uni-corn.net/","ref":"https://uni-corn.net/"},"device":{"language":"ja","ua":"Mozilla/5.0 (Linux; Android 8.0.0; ONEPLUS A5000) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.93 Mobile Safari/537.36"},"user":{"id":"69d9e1c2-801e-4901-a665-fad467550fec"},"bcat":[],"source":{"ext":{"stype":"prebid_uncn","bidder":"unicorn","prebid_version":"1.0"}}}' }; const interpretedBids = [ diff --git a/test/spec/modules/userId_spec.js b/test/spec/modules/userId_spec.js index 981ebb5f50e..8a7be41b2b5 100644 --- a/test/spec/modules/userId_spec.js +++ b/test/spec/modules/userId_spec.js @@ -8,7 +8,8 @@ import { setStoredValue, setSubmoduleRegistry, syncDelay, - PBJS_USER_ID_OPTOUT_NAME + PBJS_USER_ID_OPTOUT_NAME, + findRootDomain, } from 'modules/userId/index.js'; import {createEidsArray} from 'modules/userId/eids.js'; import {config} from 'src/config.js'; @@ -22,6 +23,7 @@ import { setConsentConfig } from 'modules/consentManagement.js'; import {server} from 'test/mocks/xhr.js'; +import find from 'core-js-pure/features/array/find.js'; import {unifiedIdSubmodule} from 'modules/unifiedIdSystem.js'; import {pubCommonIdSubmodule} from 'modules/pubCommonIdSystem.js'; import {britepoolIdSubmodule} from 'modules/britepoolIdSystem.js'; @@ -36,6 +38,8 @@ import {sharedIdSubmodule} from 'modules/sharedIdSystem.js'; import {haloIdSubmodule} from 'modules/haloIdSystem.js'; import {pubProvidedIdSubmodule} from 'modules/pubProvidedIdSystem.js'; import {criteoIdSubmodule} from 'modules/criteoIdSystem.js'; +import {tapadIdSubmodule} from 'modules/tapadIdSystem.js'; +import {getPrebidInternal} from 'src/utils.js'; let assert = require('chai').assert; let expect = require('chai').expect; @@ -67,7 +71,7 @@ describe('User ID', function () { code, mediaTypes: {banner: {}, native: {}}, sizes: [[300, 200], [300, 600]], - bids: [{bidder: 'sampleBidder', params: {placementId: 'banner-only-bidder'}}] + bids: [{bidder: 'sampleBidder', params: {placementId: 'banner-only-bidder'}}, {bidder: 'anotherSampleBidder', params: {placementId: 'banner-only-bidder'}}] }; } @@ -86,7 +90,7 @@ describe('User ID', function () { } function findEid(eids, source) { - return eids.find((eid) => { + return find(eids, (eid) => { if (eid.source === source) { return true; } }); } @@ -454,7 +458,7 @@ describe('User ID', function () { }); it('handles config with no usersync object', function () { - setSubmoduleRegistry([pubCommonIdSubmodule, unifiedIdSubmodule, id5IdSubmodule, identityLinkSubmodule, merkleIdSubmodule, netIdSubmodule, sharedIdSubmodule, intentIqIdSubmodule, zeotapIdPlusSubmodule, pubProvidedIdSubmodule, criteoIdSubmodule]); + setSubmoduleRegistry([pubCommonIdSubmodule, unifiedIdSubmodule, id5IdSubmodule, identityLinkSubmodule, merkleIdSubmodule, netIdSubmodule, sharedIdSubmodule, intentIqIdSubmodule, zeotapIdPlusSubmodule, pubProvidedIdSubmodule, criteoIdSubmodule, tapadIdSubmodule]); init(config); config.setConfig({}); // usersync is undefined, and no logInfo message for 'User ID - usersync config updated' @@ -462,14 +466,14 @@ describe('User ID', function () { }); it('handles config with empty usersync object', function () { - setSubmoduleRegistry([pubCommonIdSubmodule, unifiedIdSubmodule, id5IdSubmodule, identityLinkSubmodule, merkleIdSubmodule, netIdSubmodule, sharedIdSubmodule, intentIqIdSubmodule, zeotapIdPlusSubmodule, pubProvidedIdSubmodule, criteoIdSubmodule]); + setSubmoduleRegistry([pubCommonIdSubmodule, unifiedIdSubmodule, id5IdSubmodule, identityLinkSubmodule, merkleIdSubmodule, netIdSubmodule, sharedIdSubmodule, intentIqIdSubmodule, zeotapIdPlusSubmodule, pubProvidedIdSubmodule, criteoIdSubmodule, tapadIdSubmodule]); init(config); config.setConfig({userSync: {}}); expect(typeof utils.logInfo.args[0]).to.equal('undefined'); }); it('handles config with usersync and userIds that are empty objs', function () { - setSubmoduleRegistry([pubCommonIdSubmodule, unifiedIdSubmodule, id5IdSubmodule, identityLinkSubmodule, merkleIdSubmodule, netIdSubmodule, sharedIdSubmodule, intentIqIdSubmodule, zeotapIdPlusSubmodule, pubProvidedIdSubmodule, criteoIdSubmodule]); + setSubmoduleRegistry([pubCommonIdSubmodule, unifiedIdSubmodule, id5IdSubmodule, identityLinkSubmodule, merkleIdSubmodule, netIdSubmodule, sharedIdSubmodule, intentIqIdSubmodule, zeotapIdPlusSubmodule, pubProvidedIdSubmodule, criteoIdSubmodule, tapadIdSubmodule]); init(config); config.setConfig({ userSync: { @@ -480,7 +484,7 @@ describe('User ID', function () { }); it('handles config with usersync and userIds with empty names or that dont match a submodule.name', function () { - setSubmoduleRegistry([pubCommonIdSubmodule, unifiedIdSubmodule, id5IdSubmodule, identityLinkSubmodule, merkleIdSubmodule, netIdSubmodule, sharedIdSubmodule, intentIqIdSubmodule, zeotapIdPlusSubmodule, pubProvidedIdSubmodule, criteoIdSubmodule]); + setSubmoduleRegistry([pubCommonIdSubmodule, unifiedIdSubmodule, id5IdSubmodule, identityLinkSubmodule, merkleIdSubmodule, netIdSubmodule, sharedIdSubmodule, intentIqIdSubmodule, zeotapIdPlusSubmodule, pubProvidedIdSubmodule, criteoIdSubmodule, tapadIdSubmodule]); init(config); config.setConfig({ userSync: { @@ -497,15 +501,15 @@ describe('User ID', function () { }); it('config with 1 configurations should create 1 submodules', function () { - setSubmoduleRegistry([pubCommonIdSubmodule, unifiedIdSubmodule, id5IdSubmodule, identityLinkSubmodule, netIdSubmodule, sharedIdSubmodule, intentIqIdSubmodule, zeotapIdPlusSubmodule, pubProvidedIdSubmodule, criteoIdSubmodule]); + setSubmoduleRegistry([pubCommonIdSubmodule, unifiedIdSubmodule, id5IdSubmodule, identityLinkSubmodule, netIdSubmodule, sharedIdSubmodule, intentIqIdSubmodule, zeotapIdPlusSubmodule, pubProvidedIdSubmodule, criteoIdSubmodule, tapadIdSubmodule]); init(config); config.setConfig(getConfigMock(['unifiedId', 'unifiedid', 'cookie'])); expect(utils.logInfo.args[0][0]).to.exist.and.to.contain('User ID - usersync config updated for 1 submodules'); }); - it('config with 13 configurations should result in 13 submodules add', function () { - setSubmoduleRegistry([pubCommonIdSubmodule, unifiedIdSubmodule, id5IdSubmodule, identityLinkSubmodule, liveIntentIdSubmodule, britepoolIdSubmodule, netIdSubmodule, sharedIdSubmodule, intentIqIdSubmodule, zeotapIdPlusSubmodule, haloIdSubmodule, pubProvidedIdSubmodule, criteoIdSubmodule]); + it('config with 14 configurations should result in 14 submodules add', function () { + setSubmoduleRegistry([pubCommonIdSubmodule, unifiedIdSubmodule, id5IdSubmodule, identityLinkSubmodule, liveIntentIdSubmodule, britepoolIdSubmodule, netIdSubmodule, sharedIdSubmodule, intentIqIdSubmodule, zeotapIdPlusSubmodule, haloIdSubmodule, pubProvidedIdSubmodule, criteoIdSubmodule, tapadIdSubmodule]); init(config); config.setConfig({ userSync: { @@ -545,14 +549,17 @@ describe('User ID', function () { name: 'zeotapIdPlus' }, { name: 'criteo' + }, { + name: 'tapadId', + storage: {name: 'tapad_id', type: 'cookie'} }] } }); - expect(utils.logInfo.args[0][0]).to.exist.and.to.contain('User ID - usersync config updated for 13 submodules'); + expect(utils.logInfo.args[0][0]).to.exist.and.to.contain('User ID - usersync config updated for 14 submodules'); }); it('config syncDelay updates module correctly', function () { - setSubmoduleRegistry([pubCommonIdSubmodule, unifiedIdSubmodule, id5IdSubmodule, identityLinkSubmodule, netIdSubmodule, sharedIdSubmodule, intentIqIdSubmodule, zeotapIdPlusSubmodule, haloIdSubmodule, pubProvidedIdSubmodule, criteoIdSubmodule]); + setSubmoduleRegistry([pubCommonIdSubmodule, unifiedIdSubmodule, id5IdSubmodule, identityLinkSubmodule, netIdSubmodule, sharedIdSubmodule, intentIqIdSubmodule, zeotapIdPlusSubmodule, haloIdSubmodule, pubProvidedIdSubmodule, criteoIdSubmodule, tapadIdSubmodule]); init(config); config.setConfig({ userSync: { @@ -567,7 +574,7 @@ describe('User ID', function () { }); it('config auctionDelay updates module correctly', function () { - setSubmoduleRegistry([pubCommonIdSubmodule, unifiedIdSubmodule, id5IdSubmodule, identityLinkSubmodule, netIdSubmodule, sharedIdSubmodule, intentIqIdSubmodule, zeotapIdPlusSubmodule, haloIdSubmodule, pubProvidedIdSubmodule, criteoIdSubmodule]); + setSubmoduleRegistry([pubCommonIdSubmodule, unifiedIdSubmodule, id5IdSubmodule, identityLinkSubmodule, netIdSubmodule, sharedIdSubmodule, intentIqIdSubmodule, zeotapIdPlusSubmodule, haloIdSubmodule, pubProvidedIdSubmodule, criteoIdSubmodule, tapadIdSubmodule]); init(config); config.setConfig({ userSync: { @@ -582,7 +589,7 @@ describe('User ID', function () { }); it('config auctionDelay defaults to 0 if not a number', function () { - setSubmoduleRegistry([pubCommonIdSubmodule, unifiedIdSubmodule, id5IdSubmodule, identityLinkSubmodule, netIdSubmodule, sharedIdSubmodule, intentIqIdSubmodule, zeotapIdPlusSubmodule, haloIdSubmodule, pubProvidedIdSubmodule, criteoIdSubmodule]); + setSubmoduleRegistry([pubCommonIdSubmodule, unifiedIdSubmodule, id5IdSubmodule, identityLinkSubmodule, netIdSubmodule, sharedIdSubmodule, intentIqIdSubmodule, zeotapIdPlusSubmodule, haloIdSubmodule, pubProvidedIdSubmodule, criteoIdSubmodule, tapadIdSubmodule]); init(config); config.setConfig({ userSync: { @@ -984,6 +991,29 @@ describe('User ID', function () { }, {adUnits}); }); + it('test hook from tapadIdModule cookie', function (done) { + coreStorage.setCookie('tapad_id', 'test-tapad-id', (new Date(Date.now() + 100000).toUTCString())); + + setSubmoduleRegistry([tapadIdSubmodule]); + init(config); + config.setConfig(getConfigMock(['tapadId', 'tapad_id', 'cookie'])); + + requestBidsHook(function () { + adUnits.forEach(unit => { + unit.bids.forEach(bid => { + expect(bid).to.have.deep.nested.property('userId.tapadId'); + expect(bid.userId.tapadId).to.equal('test-tapad-id'); + expect(bid.userIdAsEids[0]).to.deep.equal({ + source: 'tapad.com', + uids: [{id: 'test-tapad-id', atype: 1}] + }); + }); + }) + coreStorage.setCookie('tapad_id', '', EXPIRED_COOKIE_DATE); + done(); + }, {adUnits}); + }); + it('test hook from liveIntentId html5', function (done) { // simulate existing browser local storage values localStorage.setItem('_li_pbid', JSON.stringify({'unifiedId': 'random-ls-identifier'})); @@ -1167,6 +1197,133 @@ describe('User ID', function () { }, {adUnits}); }); + it('eidPermissions fun with bidders', function (done) { + coreStorage.setCookie('sharedid', JSON.stringify({ + 'id': 'test222', + 'ts': 1590525289611 + }), (new Date(Date.now() + 5000).toUTCString())); + + setSubmoduleRegistry([sharedIdSubmodule]); + let eidPermissions; + getPrebidInternal().setEidPermissions = function (newEidPermissions) { + eidPermissions = newEidPermissions; + } + init(config); + config.setConfig({ + userSync: { + syncDelay: 0, + userIds: [ + { + name: 'sharedId', + bidders: [ + 'sampleBidder' + ], + storage: { + type: 'cookie', + name: 'sharedid', + expires: 28 + } + } + ] + } + }); + + requestBidsHook(function () { + expect(eidPermissions).to.deep.equal( + [ + { + bidders: [ + 'sampleBidder' + ], + source: 'sharedid.org' + } + ] + ); + adUnits.forEach(unit => { + unit.bids.forEach(bid => { + if (bid.bidder === 'sampleBidder') { + expect(bid).to.have.deep.nested.property('userId.sharedid'); + expect(bid.userId.sharedid.id).to.equal('test222'); + expect(bid.userIdAsEids[0]).to.deep.equal({ + source: 'sharedid.org', + uids: [ + { + id: 'test222', + atype: 1, + ext: { + third: 'test222' + } + } + ] + }); + } + if (bid.bidder === 'anotherSampleBidder') { + expect(bid).to.not.have.deep.nested.property('userId.sharedid'); + expect(bid).to.not.have.property('userIdAsEids'); + } + }); + }); + coreStorage.setCookie('sharedid', '', EXPIRED_COOKIE_DATE); + getPrebidInternal().setEidPermissions = undefined; + done(); + }, {adUnits}); + }); + + it('eidPermissions fun without bidders', function (done) { + coreStorage.setCookie('sharedid', JSON.stringify({ + 'id': 'test222', + 'ts': 1590525289611 + }), (new Date(Date.now() + 5000).toUTCString())); + + setSubmoduleRegistry([sharedIdSubmodule]); + let eidPermissions; + getPrebidInternal().setEidPermissions = function (newEidPermissions) { + eidPermissions = newEidPermissions; + } + init(config); + config.setConfig({ + userSync: { + syncDelay: 0, + userIds: [ + { + name: 'sharedId', + storage: { + type: 'cookie', + name: 'sharedid', + expires: 28 + } + } + ] + } + }); + + requestBidsHook(function () { + expect(eidPermissions).to.deep.equal( + [] + ); + adUnits.forEach(unit => { + unit.bids.forEach(bid => { + expect(bid).to.have.deep.nested.property('userId.sharedid'); + expect(bid.userId.sharedid.id).to.equal('test222'); + expect(bid.userIdAsEids[0]).to.deep.equal({ + source: 'sharedid.org', + uids: [ + { + id: 'test222', + atype: 1, + ext: { + third: 'test222' + } + }] + }); + }); + }); + getPrebidInternal().setEidPermissions = undefined; + coreStorage.setCookie('sharedid', '', EXPIRED_COOKIE_DATE); + done(); + }, {adUnits}); + }); + it('test hook from pubProvidedId config params', function (done) { setSubmoduleRegistry([pubProvidedIdSubmodule]); init(config); @@ -1481,7 +1638,7 @@ describe('User ID', function () { coreStorage.setCookie('haloId', JSON.stringify({'haloId': 'testHaloId'}), (new Date(Date.now() + 5000).toUTCString())); coreStorage.setCookie('storage_criteo', JSON.stringify({'criteoId': 'test_bidid'}), (new Date(Date.now() + 5000).toUTCString())); - setSubmoduleRegistry([pubCommonIdSubmodule, unifiedIdSubmodule, id5IdSubmodule, identityLinkSubmodule, britepoolIdSubmodule, netIdSubmodule, sharedIdSubmodule, intentIqIdSubmodule, zeotapIdPlusSubmodule, haloIdSubmodule, criteoIdSubmodule]); + setSubmoduleRegistry([pubCommonIdSubmodule, unifiedIdSubmodule, id5IdSubmodule, identityLinkSubmodule, britepoolIdSubmodule, netIdSubmodule, sharedIdSubmodule, intentIqIdSubmodule, zeotapIdPlusSubmodule, haloIdSubmodule, criteoIdSubmodule, tapadIdSubmodule]); init(config); config.setConfig(getConfigMock(['pubCommonId', 'pubcid', 'cookie'], ['unifiedId', 'unifiedid', 'cookie'], @@ -1493,7 +1650,8 @@ describe('User ID', function () { ['intentIqId', 'intentIqId', 'cookie'], ['zeotapIdPlus', 'IDP', 'cookie'], ['haloId', 'haloId', 'cookie'], - ['criteo', 'storage_criteo', 'cookie'])); + ['criteo', 'storage_criteo', 'cookie'], + ['tapadId', 'tapad_id', 'cookie'])); requestBidsHook(function () { adUnits.forEach(unit => { @@ -1586,6 +1744,7 @@ describe('User ID', function () { attachIdSystem(zeotapIdPlusSubmodule); attachIdSystem(haloIdSubmodule); attachIdSystem(criteoIdSubmodule); + attachIdSystem(tapadIdSubmodule); config.setConfig(getConfigMock(['pubCommonId', 'pubcid', 'cookie'], ['unifiedId', 'unifiedid', 'cookie'], @@ -1597,7 +1756,8 @@ describe('User ID', function () { ['intentIqId', 'intentIqId', 'cookie'], ['zeotapIdPlus', 'IDP', 'cookie'], ['haloId', 'haloId', 'cookie'], - ['criteo', 'storage_criteo', 'cookie'])); + ['criteo', 'storage_criteo', 'cookie'], + ['tapadId', 'tapad_id', 'cookie'])); requestBidsHook(function () { adUnits.forEach(unit => { @@ -2005,6 +2165,8 @@ describe('User ID', function () { coreStorage.setCookie('pubcid_sharedid', '', EXPIRED_COOKIE_DATE); coreStorage.setCookie('unifiedid', '', EXPIRED_COOKIE_DATE); coreStorage.setCookie('_parrable_eid', '', EXPIRED_COOKIE_DATE); + resetConsentData(); + delete window.__tcfapi; }); it('pubcid callback with url', function () { @@ -2139,12 +2301,65 @@ describe('User ID', function () { expect(server.requests[0].url).to.equal('https://id.sharedid.org/id'); expect(coreStorage.getCookie('pubcid_sharedid')).to.be.null; }); + + it('verify sharedid called with consent data when gdpr applies', function () { + let adUnits = [getAdUnitMock()]; + let customCfg = getConfigMock(['pubCommonId', 'pubcid', 'cookie']); + let consentConfig = { + cmpApi: 'iab', + timeout: 7500, + allowAuctionWithoutConsent: false + }; + customCfg = addConfig(customCfg, 'params', {pixelUrl: '/any/pubcid/url', enableSharedId: true}); + + server.respondWith('https://id.sharedid.org/id?gdpr=1&gdpr_consent=abc12345234', function(xhr) { + xhr.respond(200, {}, '{"sharedId":"testsharedid"}'); + }); + server.respondImmediately = true; + + let testConsentData = { + tcString: 'abc12345234', + gdprApplies: true, + purposeOneTreatment: false, + eventStatus: 'tcloaded', + vendor: {consents: {887: true}}, + purpose: { + consents: { + 1: true + } + } + }; + + window.__tcfapi = function () { }; + sinon.stub(window, '__tcfapi').callsFake((...args) => { + args[2](testConsentData, true); + }); + + setSubmoduleRegistry([pubCommonIdSubmodule]); + init(config); + config.setConfig(customCfg); + setConsentConfig(consentConfig); + + consentManagementRequestBidsHook(() => { + }, {}); + requestBidsHook((config) => { + }, {adUnits}); + + expect(utils.triggerPixel.called).to.be.false; + events.emit(CONSTANTS.EVENTS.AUCTION_END, {}); + expect(utils.triggerPixel.getCall(0).args[0]).to.include('/any/pubcid/url'); + + expect(server.requests[0].url).to.equal('https://id.sharedid.org/id?gdpr=1&gdpr_consent=abc12345234'); + expect(coreStorage.getCookie('pubcid_sharedid')).to.equal('testsharedid'); + }); }); describe('Set cookie behavior', function () { let coreStorageSpy; beforeEach(function () { coreStorageSpy = sinon.spy(coreStorage, 'setCookie'); + setSubmoduleRegistry([pubCommonIdSubmodule]); + init(config); }); afterEach(function () { coreStorageSpy.restore(); @@ -2360,5 +2575,46 @@ describe('User ID', function () { sinon.assert.calledOnce(mockExtendId); }); }); + + describe('findRootDomain', function () { + let sandbox; + + beforeEach(function () { + setSubmoduleRegistry([pubCommonIdSubmodule]); + init(config); + config.setConfig({ + userSync: { + syncDelay: 0, + userIds: [ + { + name: 'pubCommonId', + value: { pubcid: '11111' }, + }, + ], + }, + }); + sandbox = sinon.createSandbox(); + sandbox + .stub(coreStorage, 'getCookie') + .onFirstCall() + .returns(null) // .co.uk + .onSecondCall() + .returns('writeable'); // realdomain.co.uk; + }); + + afterEach(function () { + sandbox.restore(); + }); + + it('should just find the root domain', function () { + var domain = findRootDomain('sub.realdomain.co.uk'); + expect(domain).to.be.eq('realdomain.co.uk'); + }); + + it('should find the full domain when no subdomain is present', function () { + var domain = findRootDomain('realdomain.co.uk'); + expect(domain).to.be.eq('realdomain.co.uk'); + }); + }); }); }); diff --git a/test/spec/modules/vdoaiBidAdapter_spec.js b/test/spec/modules/vdoaiBidAdapter_spec.js index c9d826d8dc8..d78a5ce04f6 100644 --- a/test/spec/modules/vdoaiBidAdapter_spec.js +++ b/test/spec/modules/vdoaiBidAdapter_spec.js @@ -8,7 +8,7 @@ describe('vdoaiBidAdapter', function () { const adapter = newBidder(spec); describe('isBidRequestValid', function () { let bid = { - 'bidder': 'vdo.ai', + 'bidder': 'vdoai', 'params': { placementId: 'testPlacementId' }, @@ -27,7 +27,7 @@ describe('vdoaiBidAdapter', function () { describe('buildRequests', function () { let bidRequests = [ { - 'bidder': 'vdo.ai', + 'bidder': 'vdoai', 'params': { placementId: 'testPlacementId' }, @@ -37,6 +37,7 @@ describe('vdoaiBidAdapter', function () { 'bidId': '23beaa6af6cdde', 'bidderRequestId': '19c0c1efdf37e7', 'auctionId': '61466567-d482-4a16-96f0-fe5f25ffbdf1', + 'mediaTypes': 'banner' } ]; @@ -101,5 +102,40 @@ describe('vdoaiBidAdapter', function () { let result = spec.interpretResponse(serverResponse, bidRequest[0]); expect(Object.keys(result)).to.deep.equal(Object.keys(expectedResponse)); }); + + it('handles instream video responses', function () { + let serverResponse = { + body: { + 'vdoCreative': '', + 'price': 4.2, + 'adid': '12345asdfg', + 'currency': 'EUR', + 'statusMessage': 'Bid available', + 'requestId': 'bidId123', + 'width': 300, + 'height': 250, + 'netRevenue': true, + 'mediaType': 'video' + } + }; + let bidRequest = [ + { + 'method': 'POST', + 'url': ENDPOINT_URL, + 'data': { + 'placementId': 'testPlacementId', + 'width': '300', + 'height': '200', + 'bidId': 'bidId123', + 'referer': 'www.example.com', + 'mediaType': 'video' + } + } + ]; + + let result = spec.interpretResponse(serverResponse, bidRequest[0]); + expect(result[0]).to.have.property('vastXml'); + expect(result[0]).to.have.property('mediaType', 'video'); + }); }); }); diff --git a/test/spec/modules/verizonMediaIdSystem_spec.js b/test/spec/modules/verizonMediaIdSystem_spec.js index a30be5a2569..10054b5674c 100644 --- a/test/spec/modules/verizonMediaIdSystem_spec.js +++ b/test/spec/modules/verizonMediaIdSystem_spec.js @@ -87,7 +87,7 @@ describe('Verizon Media ID Submodule', () => { pixelId: PIXEL_ID, '1p': '0', gdpr: '1', - euconsent: consentData.gdpr.consentString, + gdpr_consent: consentData.gdpr.consentString, us_privacy: consentData.uspConsent }; const requestQueryParams = utils.parseQS(ajaxStub.firstCall.args[0].split('?')[1]); @@ -107,7 +107,7 @@ describe('Verizon Media ID Submodule', () => { he: HASHED_EMAIL, '1p': '0', gdpr: '1', - euconsent: consentData.gdpr.consentString, + gdpr_consent: consentData.gdpr.consentString, us_privacy: consentData.uspConsent }; const requestQueryParams = utils.parseQS(ajaxStub.firstCall.args[0].split('?')[1]); @@ -134,7 +134,7 @@ describe('Verizon Media ID Submodule', () => { const requestQueryParams = utils.parseQS(ajaxStub.firstCall.args[0].split('?')[1]); expect(requestQueryParams.gdpr).to.equal('1'); - expect(requestQueryParams.euconsent).to.equal(consentData.gdpr.consentString); + expect(requestQueryParams.gdpr_consent).to.equal(consentData.gdpr.consentString); }); it('sets GDPR consent data flag correctly when call is NOT under GDPR jurisdiction.', () => { @@ -147,7 +147,7 @@ describe('Verizon Media ID Submodule', () => { const requestQueryParams = utils.parseQS(ajaxStub.firstCall.args[0].split('?')[1]); expect(requestQueryParams.gdpr).to.equal('0'); - expect(requestQueryParams.euconsent).to.equal(''); + expect(requestQueryParams.gdpr_consent).to.equal(''); }); [1, '1', true].forEach(firstPartyParamValue => { @@ -165,12 +165,34 @@ describe('Verizon Media ID Submodule', () => { }); describe('decode()', () => { - const VALID_API_RESPONSE = { - vmuid: '1234' - }; - it('should return a newly constructed object with the vmuid property', () => { - expect(verizonMediaIdSubmodule.decode(VALID_API_RESPONSE)).to.deep.equal(VALID_API_RESPONSE); - expect(verizonMediaIdSubmodule.decode(VALID_API_RESPONSE)).to.not.equal(VALID_API_RESPONSE); + const VALID_API_RESPONSES = [{ + key: 'vmiud', + expected: '1234', + payload: { + vmuid: '1234' + } + }, + { + key: 'connectid', + expected: '4567', + payload: { + connectid: '4567' + } + }, + { + key: 'both', + expected: '4567', + payload: { + vmuid: '1234', + connectid: '4567' + } + }]; + VALID_API_RESPONSES.forEach(responseData => { + it('should return a newly constructed object with the connectid for a payload with ${responseData.key} key(s)', () => { + expect(verizonMediaIdSubmodule.decode(responseData.payload)).to.deep.equal( + {connectid: responseData.expected} + ); + }); }); [{}, '', {foo: 'bar'}].forEach((response) => { diff --git a/test/spec/modules/voxBidAdapter_spec.js b/test/spec/modules/voxBidAdapter_spec.js new file mode 100644 index 00000000000..c6221cba9e5 --- /dev/null +++ b/test/spec/modules/voxBidAdapter_spec.js @@ -0,0 +1,320 @@ +import { expect } from 'chai' +import { spec } from 'modules/voxBidAdapter.js' + +function getSlotConfigs(mediaTypes, params) { + return { + params: params, + sizes: [], + bidId: '2df8c0733f284e', + bidder: 'vox', + mediaTypes: mediaTypes, + transactionId: '31a58515-3634-4e90-9c96-f86196db1459' + } +} + +describe('VOX Adapter', function() { + const PLACE_ID = '5af45ad34d506ee7acad0c26'; + const bidderRequest = { + refererInfo: { referer: 'referer' } + } + const bannerMandatoryParams = { + placementId: PLACE_ID, + placement: 'banner' + } + const videoMandatoryParams = { + placementId: PLACE_ID, + placement: 'video' + } + const inImageMandatoryParams = { + placementId: PLACE_ID, + placement: 'inImage', + imageUrl: 'https://hybrid.ai/images/image.jpg' + } + const validBidRequests = [ + getSlotConfigs({ banner: {} }, bannerMandatoryParams), + getSlotConfigs({ video: {playerSize: [[640, 480]], context: 'outstream'} }, videoMandatoryParams), + getSlotConfigs({ banner: {sizes: [0, 0]} }, inImageMandatoryParams) + ] + describe('isBidRequestValid method', function() { + describe('returns true', function() { + describe('when banner slot config has all mandatory params', () => { + describe('and banner placement has the correct value', function() { + const slotConfig = getSlotConfigs( + {banner: {}}, + { + placementId: PLACE_ID, + placement: 'banner' + } + ) + const isBidRequestValid = spec.isBidRequestValid(slotConfig) + expect(isBidRequestValid).to.equal(true) + }) + describe('and In-Image placement has the correct value', function() { + const slotConfig = getSlotConfigs( + { + banner: { + sizes: [[0, 0]] + } + }, + { + placementId: PLACE_ID, + placement: 'inImage', + imageUrl: 'imageUrl' + } + ) + const isBidRequestValid = spec.isBidRequestValid(slotConfig) + expect(isBidRequestValid).to.equal(true) + }) + describe('when video slot has all mandatory params.', function() { + it('should return true, when video mediatype object are correct.', function() { + const slotConfig = getSlotConfigs( + { + video: { + context: 'instream', + playerSize: [[640, 480]] + } + }, + { + placementId: PLACE_ID, + placement: 'video' + } + ) + const isBidRequestValid = spec.isBidRequestValid(slotConfig) + expect(isBidRequestValid).to.equal(true) + }) + }) + }) + }) + describe('returns false', function() { + describe('when params are not correct', function() { + function createSlotconfig(params) { + return getSlotConfigs({ banner: {} }, params) + } + it('does not have the placementId.', function() { + const isBidRequestValid = spec.isBidRequestValid( + createSlotconfig({ + placement: 'banner' + }) + ) + expect(isBidRequestValid).to.equal(false) + }) + it('does not have the placement.', function() { + const isBidRequestValid = spec.isBidRequestValid( + createSlotconfig({ + placementId: PLACE_ID + }) + ) + expect(isBidRequestValid).to.equal(false) + }) + it('does not have the imageUrl.', function() { + const isBidRequestValid = spec.isBidRequestValid( + createSlotconfig({ + placementId: PLACE_ID, + placement: 'inImage' + }) + ) + expect(isBidRequestValid).to.equal(false) + }) + it('does not have a the correct placement.', function() { + const isBidRequestValid = spec.isBidRequestValid( + createSlotconfig({ + placementId: PLACE_ID, + placement: 'something' + }) + ) + expect(isBidRequestValid).to.equal(false) + }) + }) + describe('when video mediaType object is not correct.', function() { + function createVideoSlotconfig(mediaType) { + return getSlotConfigs(mediaType, { + placementId: PLACE_ID, + placement: 'video' + }) + } + it('is a void object', function() { + const isBidRequestValid = spec.isBidRequestValid( + createVideoSlotconfig({ video: {} }) + ) + expect(isBidRequestValid).to.equal(false) + }) + it('does not have playerSize.', function() { + const isBidRequestValid = spec.isBidRequestValid( + createVideoSlotconfig({ video: { context: 'instream' } }) + ) + expect(isBidRequestValid).to.equal(false) + }) + it('does not have context', function() { + const isBidRequestValid = spec.isBidRequestValid( + createVideoSlotconfig({ + video: { + playerSize: [[640, 480]] + } + }) + ) + expect(isBidRequestValid).to.equal(false) + }) + }) + }) + }) + it('Url params should be correct ', function() { + const request = spec.buildRequests(validBidRequests, bidderRequest) + expect(request.method).to.equal('POST') + expect(request.url).to.equal('https://ssp.hybrid.ai/auction/prebid') + }) + + describe('buildRequests method', function() { + it('Common data request should be correct', function() { + const request = spec.buildRequests(validBidRequests, bidderRequest) + const data = JSON.parse(request.data) + expect(Array.isArray(data.bidRequests)).to.equal(true) + expect(data.url).to.equal('referer') + data.bidRequests.forEach(bid => { + expect(bid.bidId).to.equal('2df8c0733f284e') + expect(bid.placeId).to.equal(PLACE_ID) + expect(bid.transactionId).to.equal('31a58515-3634-4e90-9c96-f86196db1459') + }) + }) + + describe('GDPR params', function() { + describe('when there are not consent management platform', function() { + it('cmp should be false', function() { + const request = spec.buildRequests(validBidRequests, bidderRequest) + const data = JSON.parse(request.data) + expect(data.cmp).to.equal(false) + }) + }) + describe('when there are consent management platform', function() { + it('cmps should be true and ga should not sended, when gdprApplies is undefined', function() { + bidderRequest['gdprConsent'] = { + gdprApplies: undefined, + consentString: 'consentString' + } + const request = spec.buildRequests(validBidRequests, bidderRequest) + const data = JSON.parse(request.data) + expect(data.cmp).to.equal(true) + expect(Object.keys(data).indexOf('data')).to.equal(-1) + expect(data.cs).to.equal('consentString') + }) + it('cmps should be true and all gdpr parameters should be sended, when there are gdprApplies', function() { + bidderRequest['gdprConsent'] = { + gdprApplies: true, + consentString: 'consentString' + } + const request = spec.buildRequests(validBidRequests, bidderRequest) + const data = JSON.parse(request.data) + expect(data.cmp).to.equal(true) + expect(data.ga).to.equal(true) + expect(data.cs).to.equal('consentString') + }) + }) + }) + }) + + describe('interpret response method', function() { + it('should return a void array, when the server response are not correct.', function() { + const request = { data: JSON.stringify({}) } + const serverResponse = { + body: {} + } + const bids = spec.interpretResponse(serverResponse, request) + expect(typeof bids).to.equal('object') + expect(bids.length).to.equal(0) + }) + it('should return a void array, when the server response have not got bids.', function() { + const request = { data: JSON.stringify({}) } + const serverResponse = { body: { bids: [] } } + const bids = spec.interpretResponse(serverResponse, request) + expect(typeof bids).to.equal('object') + expect(bids.length).to.equal(0) + }) + describe('when the server response return a bid', function() { + describe('the bid is a banner', function() { + it('should return a banner bid', function() { + const request = spec.buildRequests([validBidRequests[0]], bidderRequest) + const serverResponse = { + body: { + bids: [ + { + bidId: '2df8c0733f284e', + price: 0.5, + currency: 'USD', + content: { + content: 'html', + width: 100, + height: 100 + } + } + ] + } + } + const bids = spec.interpretResponse(serverResponse, request) + expect(bids.length).to.equal(1) + expect(bids[0].requestId).to.equal('2df8c0733f284e') + expect(bids[0].mediaType).to.equal(spec.supportedMediaTypes[0]) + expect(bids[0].cpm).to.equal(0.5) + expect(bids[0].width).to.equal(100) + expect(bids[0].height).to.equal(100) + expect(bids[0].currency).to.equal('USD') + expect(bids[0].netRevenue).to.equal(true) + expect(typeof bids[0].ad).to.equal('string') + }) + it('should return a In-Image bid', function() { + const request = spec.buildRequests([validBidRequests[2]], bidderRequest) + const serverResponse = { + body: { + bids: [ + { + bidId: '2df8c0733f284e', + price: 0.5, + currency: 'USD', + content: { + content: 'html', + width: 100, + height: 100 + }, + ttl: 360 + } + ] + } + } + const bids = spec.interpretResponse(serverResponse, request) + expect(bids.length).to.equal(1) + expect(bids[0].requestId).to.equal('2df8c0733f284e') + expect(bids[0].cpm).to.equal(0.5) + expect(bids[0].width).to.equal(100) + expect(bids[0].height).to.equal(100) + expect(bids[0].currency).to.equal('USD') + expect(bids[0].netRevenue).to.equal(true) + expect(typeof bids[0].ad).to.equal('string') + }) + }) + describe('the bid is a video', function() { + it('should return a video bid', function() { + const request = spec.buildRequests([validBidRequests[1]], bidderRequest) + const serverResponse = { + body: { + bids: [ + { + bidId: '2df8c0733f284e', + price: 0.5, + currency: 'USD', + content: 'html', + transactionId: '31a58515-3634-4e90-9c96-f86196db1459' + } + ] + } + } + const bids = spec.interpretResponse(serverResponse, request) + expect(bids.length).to.equal(1) + expect(bids[0].requestId).to.equal('2df8c0733f284e') + expect(bids[0].mediaType).to.equal(spec.supportedMediaTypes[1]) + expect(bids[0].cpm).to.equal(0.5) + expect(bids[0].currency).to.equal('USD') + expect(bids[0].netRevenue).to.equal(true) + expect(typeof bids[0].vastXml).to.equal('string') + }) + }) + }) + }) +}) diff --git a/test/spec/modules/waardexBidAdapter_spec.js b/test/spec/modules/waardexBidAdapter_spec.js index 8732b2bd51f..73094dd72a0 100644 --- a/test/spec/modules/waardexBidAdapter_spec.js +++ b/test/spec/modules/waardexBidAdapter_spec.js @@ -1,51 +1,201 @@ import {expect} from 'chai'; import {spec} from '../../../modules/waardexBidAdapter.js'; -import { auctionManager } from 'src/auctionManager.js'; -import { deepClone } from 'src/utils.js'; +import {auctionManager} from 'src/auctionManager.js'; +import {deepClone} from 'src/utils.js'; describe('waardexBidAdapter', () => { - const validBid = { - bidId: '112435ry', - bidder: 'waardex', - params: { - placementId: 1, - traffic: 'banner', - zoneId: 1, - } - }; - describe('isBidRequestValid', () => { - it('Should return true. bidId and params such as placementId and zoneId are present', () => { - expect(spec.isBidRequestValid(validBid)).to.be.true; + it('should return true. bidId and params such as placementId and zoneId are present', () => { + const result = spec.isBidRequestValid({ + bidId: '112435ry', + bidder: 'waardex', + params: { + placementId: 1, + traffic: 'banner', + zoneId: 1, + } + }); + + expect(result).to.be.true; + }); + + it('should return false. bidId is not present in bid object', () => { + const result = spec.isBidRequestValid({ + bidder: 'waardex', + params: { + placementId: 1, + traffic: 'banner', + zoneId: 1, + } + }); + + expect(result).to.be.false; + }); + + it('should return false. zoneId is not present in bid.params object', () => { + const result = spec.isBidRequestValid({ + bidId: '112435ry', + bidder: 'waardex', + params: { + placementId: 1, + traffic: 'banner', + } + }); + + expect(result).to.be.false; + }); + + it('should return true when mediaTypes field is empty', () => { + const result = spec.isBidRequestValid({ + bidId: '112435ry', + bidder: 'waardex', + params: { + placementId: 1, + traffic: 'banner', + zoneId: 1, + } + }); + + expect(result).to.be.true; }); - it('Should return false. bidId is not present in bid object', () => { - const invalidBid = deepClone(validBid); - delete invalidBid.bidId; - expect(spec.isBidRequestValid(invalidBid)).to.be.false; + + it('should return false when mediaTypes.video.playerSize field is empty', () => { + const result = spec.isBidRequestValid({ + bidId: '112435ry', + bidder: 'waardex', + params: { + placementId: 1, + traffic: 'banner', + zoneId: 1, + }, + mediaTypes: { + video: {} + } + }); + + expect(result).to.be.false; }); - it('Should return false. zoneId is not present in bid.params object', () => { - const invalidBid = deepClone(validBid); - delete invalidBid.params.zoneId; - expect(spec.isBidRequestValid(invalidBid)).to.be.false; + + it('should return false when mediaTypes.video.playerSize field is not an array', () => { + const result = spec.isBidRequestValid({ + bidId: '112435ry', + bidder: 'waardex', + params: { + placementId: 1, + traffic: 'banner', + zoneId: 1, + }, + mediaTypes: { + video: { + playerSize: 'not-array' + } + } + }); + + expect(result).to.be.false; + }); + + it('should return false when mediaTypes.video.playerSize field is an empty array', () => { + const result = spec.isBidRequestValid({ + bidId: '112435ry', + bidder: 'waardex', + params: { + placementId: 1, + traffic: 'banner', + zoneId: 1, + }, + mediaTypes: { + video: { + playerSize: [] + } + } + }); + + expect(result).to.be.false; + }); + + it('should return false when mediaTypes.video.playerSize field is empty array', () => { + const result = spec.isBidRequestValid({ + bidId: '112435ry', + bidder: 'waardex', + params: { + placementId: 1, + traffic: 'banner', + zoneId: 1, + }, + mediaTypes: { + video: { + playerSize: [] + } + } + }); + + expect(result).to.be.false; + }); + + it('should return true when mediaTypes.video.playerSize field is non-empty array', () => { + const result = spec.isBidRequestValid({ + bidId: '112435ry', + bidder: 'waardex', + params: { + placementId: 1, + traffic: 'banner', + zoneId: 1, + }, + mediaTypes: { + video: { + playerSize: [[640, 400]] + } + } + }); + + expect(result).to.be.true; }); }); describe('buildRequests', () => { let getAdUnitsStub; - const validBidRequests = [{ - bidId: 'fergr675ujgh', - mediaTypes: { - banner: { - sizes: [[300, 600], [300, 250]] - } - }, - params: { - bidfloor: 1.5, - position: 1, - instl: 1, - zoneId: 100 + const validBidRequests = [ + { + bidId: 'fergr675ujgh', + mediaTypes: { + banner: { + sizes: [[300, 600], [300, 250]] + } + }, + params: { + bidfloor: 1.5, + position: 1, + instl: 1, + zoneId: 100 + }, }, - }]; + { + bidId: 'unique-bid-id-2', + mediaTypes: { + video: { + context: 'outstream', + playerSize: [[640, 400]] + } + }, + params: { + mimes: ['video/x-ms-wmv', 'video/mp4'], + minduration: 2, + maxduration: 10, + protocols: ['VAST 1.0', 'VAST 2.0'], + startdelay: -1, + placement: 1, + skip: 1, + skipafter: 2, + minbitrate: 0, + maxbitrate: 0, + delivery: [1, 2, 3], + playbackmethod: [1, 2], + api: [1, 2, 3, 4, 5, 6], + linearity: 1, + } + } + ]; const bidderRequest = { refererInfo: { @@ -57,12 +207,13 @@ describe('waardexBidAdapter', () => { afterEach(() => getAdUnitsStub.restore()); it('should return valid build request object', () => { - const request = spec.buildRequests(validBidRequests, bidderRequest); const { data: payload, url, method, - } = request; + } = spec.buildRequests(validBidRequests, bidderRequest); + + const ENDPOINT = `https://hb.justbidit.xyz:8843/prebid?pubId=${validBidRequests[0].params.zoneId}`; expect(payload.bidRequests[0]).deep.equal({ bidId: validBidRequests[0].bidId, @@ -82,7 +233,6 @@ describe('waardexBidAdapter', () => { ], } }); - const ENDPOINT = `https://hb.justbidit.xyz:8843/prebid?pubId=${validBidRequests[0].params.zoneId}`; expect(url).to.equal(ENDPOINT); expect(method).to.equal('POST'); }); @@ -91,44 +241,88 @@ describe('waardexBidAdapter', () => { describe('interpretResponse', () => { const serverResponse = { body: { - seatbid: [{ - bid: [{ - id: 'someId', - price: 3.3, - w: 250, - h: 300, - crid: 'dspCreativeIdHere', - adm: 'html markup here', - dealId: '123456789', - cid: 'dsp campaign id', - adomain: 'advertisers domain', - ext: { - mediaType: 'banner', - }, - }], - }], + seatbid: [ + { + bid: [ + { + 'id': 'ec90d4ee25433c0875c56f59acc12109', + 'adm': 'banner should be here', + 'w': 300, + 'h': 250, + 'price': 0.15827000000000002, + 'nurl': 'http://n63.justbidit.xyz?w=n&p=${AUCTION_PRICE}&t=b&uq=eb31018b3da8965dde414c0006b1fb31', + 'impid': 'unique-bid-id-1', + 'adomain': [ + 'www.kraeuterhaus.de' + ], + 'cat': [ + 'IAB24' + ], + 'attr': [ + 4 + ], + 'iurl': 'https://rtb.bsmartdata.com/preview.php?id=13', + 'cid': '286557fda4585dc1c7f98b773de92509', + 'crid': 'dd2b5a1f3f1fc5e63b042ebf4b00ca80' + }, + ], + } + ], }, }; + const request = { + 'ua': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_4) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/86.0.4240.198 Safari/537.36', + 'language': 'en', + 'referer': 'https%3A%2F%2Fwww.google.com%2F%3Fsome_param%3Dsome_value', + 'coppa': false, + 'data': { + 'bidRequests': [ + { + 'bidId': 'unique-bid-id-1', + 'bidfloor': 0.1, + 'position': 1, + 'instl': 1, + 'banner': { + 'sizes': [ + { + 'width': 300, + 'height': 600 + }, + { + 'width': 300, + 'height': 250 + } + ] + } + }, + ] + } + }; + it('bid response is valid', () => { - const result = spec.interpretResponse(serverResponse); - const expected = [{ - requestId: serverResponse.body.seatbid[0].bid[0].id, - cpm: serverResponse.body.seatbid[0].bid[0].price, - currency: 'USD', - width: serverResponse.body.seatbid[0].bid[0].w, - height: serverResponse.body.seatbid[0].bid[0].h, - creativeId: serverResponse.body.seatbid[0].bid[0].crid, - netRevenue: true, - ttl: 3000, - ad: serverResponse.body.seatbid[0].bid[0].adm, - dealId: serverResponse.body.seatbid[0].bid[0].dealid, - meta: { - cid: serverResponse.body.seatbid[0].bid[0].cid, - adomain: serverResponse.body.seatbid[0].bid[0].adomain, - mediaType: serverResponse.body.seatbid[0].bid[0].ext.mediaType, - }, - }]; + const result = spec.interpretResponse(serverResponse, request); + + const expected = [ + { + requestId: 'unique-bid-id-1', + cpm: serverResponse.body.seatbid[0].bid[0].price, + currency: 'USD', + width: serverResponse.body.seatbid[0].bid[0].w, + height: serverResponse.body.seatbid[0].bid[0].h, + mediaType: 'banner', + creativeId: serverResponse.body.seatbid[0].bid[0].crid, + netRevenue: true, + ttl: 3000, + ad: serverResponse.body.seatbid[0].bid[0].adm, + dealId: serverResponse.body.seatbid[0].bid[0].dealid, + meta: { + cid: serverResponse.body.seatbid[0].bid[0].cid, + adomain: serverResponse.body.seatbid[0].bid[0].adomain, + mediaType: (serverResponse.body.seatbid[0].bid[0].ext || {}).mediaType, + }, + } + ]; expect(result).deep.equal(expected); }); diff --git a/test/spec/modules/yieldlabBidAdapter_spec.js b/test/spec/modules/yieldlabBidAdapter_spec.js index cd2c46a5664..6dde671578c 100644 --- a/test/spec/modules/yieldlabBidAdapter_spec.js +++ b/test/spec/modules/yieldlabBidAdapter_spec.js @@ -7,7 +7,6 @@ const REQUEST = { 'params': { 'adslotId': '1111', 'supplyId': '2222', - 'adSize': '728x90', 'targeting': { 'key1': 'value1', 'key2': 'value2', @@ -57,6 +56,7 @@ const RESPONSE = { id: 1111, price: 1, pid: 2222, + adsize: '728x90', adtype: 'BANNER' } @@ -88,8 +88,7 @@ describe('yieldlabBidAdapter', function () { const request = { 'params': { 'adslotId': '1111', - 'supplyId': '2222', - 'adSize': '728x90' + 'supplyId': '2222' } } expect(spec.isBidRequestValid(request)).to.equal(true) @@ -180,7 +179,7 @@ describe('yieldlabBidAdapter', function () { expect(result[0].netRevenue).to.equal(false) expect(result[0].ttl).to.equal(300) expect(result[0].referrer).to.equal('') - expect(result[0].ad).to.include('' + javascriptTrackers: '', + ext: { + foo: 'foo-value', + baz: 'baz-value' + } } }; @@ -36,7 +40,11 @@ const bidWithUndefinedFields = { clickUrl: 'https://www.link.example', clickTrackers: ['https://tracker.example'], impressionTrackers: ['https://impression.example'], - javascriptTrackers: '' + javascriptTrackers: '', + ext: { + foo: 'foo-value', + baz: undefined + } } }; @@ -59,14 +67,21 @@ describe('native.js', function () { expect(targeting[CONSTANTS.NATIVE_KEYS.title]).to.equal(bid.native.title); expect(targeting[CONSTANTS.NATIVE_KEYS.body]).to.equal(bid.native.body); expect(targeting[CONSTANTS.NATIVE_KEYS.clickUrl]).to.equal(bid.native.clickUrl); + expect(targeting.hb_native_foo).to.equal(bid.native.foo); }); it('sends placeholders for configured assets', function () { const bidRequest = { - mediaTypes: { - native: { - body: { sendId: true }, - clickUrl: { sendId: true }, + nativeParams: { + body: { sendId: true }, + clickUrl: { sendId: true }, + ext: { + foo: { + sendId: false + }, + baz: { + sendId: true + } } } }; @@ -75,16 +90,171 @@ describe('native.js', function () { expect(targeting[CONSTANTS.NATIVE_KEYS.title]).to.equal(bid.native.title); expect(targeting[CONSTANTS.NATIVE_KEYS.body]).to.equal('hb_native_body:123'); expect(targeting[CONSTANTS.NATIVE_KEYS.clickUrl]).to.equal('hb_native_linkurl:123'); + expect(targeting.hb_native_foo).to.equal(bid.native.ext.foo); + expect(targeting.hb_native_baz).to.equal('hb_native_baz:123'); }); it('should only include native targeting keys with values', function () { - const targeting = getNativeTargeting(bidWithUndefinedFields); + const bidRequest = { + nativeParams: { + body: { sendId: true }, + clickUrl: { sendId: true }, + ext: { + foo: { + required: false + }, + baz: { + required: false + } + } + } + }; + + const targeting = getNativeTargeting(bidWithUndefinedFields, bidRequest); + + expect(Object.keys(targeting)).to.deep.equal([ + CONSTANTS.NATIVE_KEYS.title, + CONSTANTS.NATIVE_KEYS.sponsoredBy, + CONSTANTS.NATIVE_KEYS.clickUrl, + 'hb_native_foo' + ]); + }); + + it('should only include targeting that has sendTargetingKeys set to true', function () { + const bidRequest = { + nativeParams: { + image: { + required: true, + sizes: [150, 50] + }, + title: { + required: true, + len: 80, + sendTargetingKeys: true + }, + sendTargetingKeys: false, + } + + }; + const targeting = getNativeTargeting(bid, bidRequest); + + expect(Object.keys(targeting)).to.deep.equal([ + CONSTANTS.NATIVE_KEYS.title + ]); + }); + + it('should only include targeting if sendTargetingKeys not set to false', function () { + const bidRequest = { + nativeParams: { + image: { + required: true, + sizes: [150, 50] + }, + title: { + required: true, + len: 80 + }, + body: { + required: true + }, + clickUrl: { + required: true + }, + icon: { + required: false, + sendTargetingKeys: false + }, + cta: { + required: false, + sendTargetingKeys: false + }, + sponsoredBy: { + required: false, + sendTargetingKeys: false + }, + ext: { + foo: { + required: false, + sendTargetingKeys: true + } + } + } + + }; + const targeting = getNativeTargeting(bid, bidRequest); + + expect(Object.keys(targeting)).to.deep.equal([ + CONSTANTS.NATIVE_KEYS.title, + CONSTANTS.NATIVE_KEYS.body, + CONSTANTS.NATIVE_KEYS.image, + CONSTANTS.NATIVE_KEYS.clickUrl, + 'hb_native_foo' + ]); + }); + + it('should copy over rendererUrl to bid object and include it in targeting', function () { + const bidRequest = { + nativeParams: { + image: { + required: true, + sizes: [150, 50] + }, + title: { + required: true, + len: 80, + }, + rendererUrl: { + url: 'https://www.renderer.com/' + } + } + + }; + const targeting = getNativeTargeting(bid, bidRequest); + + expect(Object.keys(targeting)).to.deep.equal([ + CONSTANTS.NATIVE_KEYS.title, + CONSTANTS.NATIVE_KEYS.body, + CONSTANTS.NATIVE_KEYS.cta, + CONSTANTS.NATIVE_KEYS.image, + CONSTANTS.NATIVE_KEYS.icon, + CONSTANTS.NATIVE_KEYS.sponsoredBy, + CONSTANTS.NATIVE_KEYS.clickUrl, + CONSTANTS.NATIVE_KEYS.rendererUrl + ]); + + expect(bid.native.rendererUrl).to.deep.equal('https://www.renderer.com/'); + delete bid.native.rendererUrl; + }); + + it('should copy over adTemplate to bid object and include it in targeting', function () { + const bidRequest = { + nativeParams: { + image: { + required: true, + sizes: [150, 50] + }, + title: { + required: true, + len: 80, + }, + adTemplate: '

##hb_native_body##<\/p><\/div>' + } + + }; + const targeting = getNativeTargeting(bid, bidRequest); expect(Object.keys(targeting)).to.deep.equal([ CONSTANTS.NATIVE_KEYS.title, + CONSTANTS.NATIVE_KEYS.body, + CONSTANTS.NATIVE_KEYS.cta, + CONSTANTS.NATIVE_KEYS.image, + CONSTANTS.NATIVE_KEYS.icon, CONSTANTS.NATIVE_KEYS.sponsoredBy, CONSTANTS.NATIVE_KEYS.clickUrl ]); + + expect(bid.native.adTemplate).to.deep.equal('

##hb_native_body##<\/p><\/div>'); + delete bid.native.adTemplate; }); it('fires impression trackers', function () { @@ -125,6 +295,82 @@ describe('native.js', function () { value: bid.native.clickUrl }); }); + + it('creates native all asset message', function() { + const messageRequest = { + message: 'Prebid Native', + action: 'allAssetRequest', + adId: '123', + }; + + const message = getAllAssetsMessage(messageRequest, bid); + + expect(message.assets.length).to.equal(9); + expect(message.assets).to.deep.include({ + key: 'body', + value: bid.native.body + }); + expect(message.assets).to.deep.include({ + key: 'image', + value: bid.native.image.url + }); + expect(message.assets).to.deep.include({ + key: 'clickUrl', + value: bid.native.clickUrl + }); + expect(message.assets).to.deep.include({ + key: 'title', + value: bid.native.title + }); + expect(message.assets).to.deep.include({ + key: 'icon', + value: bid.native.icon.url + }); + expect(message.assets).to.deep.include({ + key: 'cta', + value: bid.native.cta + }); + expect(message.assets).to.deep.include({ + key: 'sponsoredBy', + value: bid.native.sponsoredBy + }); + expect(message.assets).to.deep.include({ + key: 'foo', + value: bid.native.ext.foo + }); + expect(message.assets).to.deep.include({ + key: 'baz', + value: bid.native.ext.baz + }); + }); + + it('creates native all asset message with only defined fields', function() { + const messageRequest = { + message: 'Prebid Native', + action: 'allAssetRequest', + adId: '123', + }; + + const message = getAllAssetsMessage(messageRequest, bidWithUndefinedFields); + + expect(message.assets.length).to.equal(4); + expect(message.assets).to.deep.include({ + key: 'clickUrl', + value: bid.native.clickUrl + }); + expect(message.assets).to.deep.include({ + key: 'title', + value: bid.native.title + }); + expect(message.assets).to.deep.include({ + key: 'sponsoredBy', + value: bid.native.sponsoredBy + }); + expect(message.assets).to.deep.include({ + key: 'foo', + value: bid.native.ext.foo + }); + }); }); describe('validate native', function () { diff --git a/test/spec/unit/core/adapterManager_spec.js b/test/spec/unit/core/adapterManager_spec.js index 9ccdd6aef59..25b2307b943 100644 --- a/test/spec/unit/core/adapterManager_spec.js +++ b/test/spec/unit/core/adapterManager_spec.js @@ -1,5 +1,5 @@ import { expect } from 'chai'; -import adapterManager, { gdprDataHandler } from 'src/adapterManager.js'; +import adapterManager, { allS2SBidders, clientTestAdapters, gdprDataHandler } from 'src/adapterManager.js'; import { getAdUnits, getServerTestingConfig, @@ -25,6 +25,17 @@ const CONFIG = { bidders: ['appnexus'], accountId: 'abc' }; + +const CONFIG2 = { + enabled: true, + endpoint: 'https://prebid-server.rubiconproject.com/openrtb2/auction', + timeout: 1000, + maxBids: 1, + adapter: 'prebidServer', + bidders: ['pubmatic'], + accountId: 'def' +} + var prebidServerAdapterMock = { bidder: 'prebidServer', callBids: sinon.stub() @@ -43,6 +54,11 @@ var rubiconAdapterMock = { callBids: sinon.stub() }; +var pubmaticAdapterMock = { + bidder: 'rubicon', + callBids: sinon.stub() +}; + var badAdapterMock = { bidder: 'badBidder', callBids: sinon.stub().throws(Error('some fake error')) @@ -82,6 +98,7 @@ describe('adapterManager tests', function () { adapterManager.bidderRegistry['appnexus'] = appnexusAdapterMock; adapterManager.bidderRegistry['rubicon'] = rubiconAdapterMock; adapterManager.bidderRegistry['badBidder'] = badAdapterMock; + adapterManager.bidderRegistry['badBidder'] = badAdapterMock; }); afterEach(function () { @@ -387,6 +404,38 @@ describe('adapterManager tests', function () { }); }); // end onSetTargeting + describe('onBidViewable', function () { + var criteoSpec = { onBidViewable: sinon.stub() } + var criteoAdapter = { + bidder: 'criteo', + getSpec: function() { return criteoSpec; } + } + before(function () { + config.setConfig({s2sConfig: { enabled: false }}); + }); + + beforeEach(function () { + adapterManager.bidderRegistry['criteo'] = criteoAdapter; + }); + + afterEach(function () { + delete adapterManager.bidderRegistry['criteo']; + }); + + it('should call spec\'s onBidViewable callback when callBidViewableBidder is called', function () { + const bids = [ + {bidder: 'criteo', params: {placementId: 'id'}}, + ]; + const adUnits = [{ + code: 'adUnit-code', + sizes: [[728, 90]], + bids + }]; + adapterManager.callBidViewableBidder(bids[0].bidder, bids[0]); + sinon.assert.called(criteoSpec.onBidViewable); + }); + }); // end onBidViewable + describe('S2S tests', function () { beforeEach(function () { config.setConfig({s2sConfig: CONFIG}); @@ -394,144 +443,144 @@ describe('adapterManager tests', function () { prebidServerAdapterMock.callBids.reset(); }); - it('invokes callBids on the S2S adapter', function () { - let bidRequests = [{ - 'bidderCode': 'appnexus', - 'auctionId': '1863e370099523', - 'bidderRequestId': '2946b569352ef2', - 'tid': '34566b569352ef2', - 'timeout': 1000, - 'src': 's2s', - 'adUnitsS2SCopy': [ - { - 'code': '/19968336/header-bid-tag1', - 'sizes': [ - { - 'w': 728, - 'h': 90 + const bidRequests = [{ + 'bidderCode': 'appnexus', + 'auctionId': '1863e370099523', + 'bidderRequestId': '2946b569352ef2', + 'tid': '34566b569352ef2', + 'timeout': 1000, + 'src': 's2s', + 'adUnitsS2SCopy': [ + { + 'code': '/19968336/header-bid-tag1', + 'sizes': [ + { + 'w': 728, + 'h': 90 + }, + { + 'w': 970, + 'h': 90 + } + ], + 'bids': [ + { + 'bidder': 'appnexus', + 'params': { + 'placementId': '543221', + 'test': 'me' }, - { - 'w': 970, - 'h': 90 - } - ], - 'bids': [ - { - 'bidder': 'appnexus', - 'params': { - 'placementId': '543221', - 'test': 'me' - }, - 'adUnitCode': '/19968336/header-bid-tag1', - 'sizes': [ - [ - 728, - 90 - ], - [ - 970, - 90 - ] + 'adUnitCode': '/19968336/header-bid-tag1', + 'sizes': [ + [ + 728, + 90 ], - 'bidId': '68136e1c47023d', - 'bidderRequestId': '55e24a66bed717', - 'auctionId': '1ff753bd4ae5cb', - 'startTime': 1463510220995, - 'status': 1, - 'bid_id': '68136e1c47023d' - } - ] - }, - { - 'code': '/19968336/header-bid-tag-0', - 'sizes': [ - { - 'w': 300, - 'h': 250 + [ + 970, + 90 + ] + ], + 'bidId': '68136e1c47023d', + 'bidderRequestId': '55e24a66bed717', + 'auctionId': '1ff753bd4ae5cb', + 'startTime': 1463510220995, + 'status': 1, + 'bid_id': '68136e1c47023d' + } + ] + }, + { + 'code': '/19968336/header-bid-tag-0', + 'sizes': [ + { + 'w': 300, + 'h': 250 + }, + { + 'w': 300, + 'h': 600 + } + ], + 'bids': [ + { + 'bidder': 'appnexus', + 'params': { + 'placementId': '5324321' }, - { - 'w': 300, - 'h': 600 - } - ], - 'bids': [ - { - 'bidder': 'appnexus', - 'params': { - 'placementId': '5324321' - }, - 'adUnitCode': '/19968336/header-bid-tag-0', - 'sizes': [ - [ - 300, - 250 - ], - [ - 300, - 600 - ] + 'adUnitCode': '/19968336/header-bid-tag-0', + 'sizes': [ + [ + 300, + 250 ], - 'bidId': '7e5d6af25ed188', - 'bidderRequestId': '55e24a66bed717', - 'auctionId': '1ff753bd4ae5cb', - 'startTime': 1463510220996, - 'bid_id': '7e5d6af25ed188' - } - ] - } - ], - 'bids': [ - { - 'bidder': 'appnexus', - 'params': { - 'placementId': '4799418', - 'test': 'me' - }, - 'adUnitCode': '/19968336/header-bid-tag1', - 'sizes': [ - [ - 728, - 90 + [ + 300, + 600 + ] ], - [ - 970, - 90 - ] + 'bidId': '7e5d6af25ed188', + 'bidderRequestId': '55e24a66bed717', + 'auctionId': '1ff753bd4ae5cb', + 'startTime': 1463510220996, + 'bid_id': '7e5d6af25ed188' + } + ] + } + ], + 'bids': [ + { + 'bidder': 'appnexus', + 'params': { + 'placementId': '4799418', + 'test': 'me' + }, + 'adUnitCode': '/19968336/header-bid-tag1', + 'sizes': [ + [ + 728, + 90 ], - 'bidId': '392b5a6b05d648', - 'bidderRequestId': '2946b569352ef2', - 'auctionId': '1863e370099523', - 'startTime': 1462918897462, - 'status': 1, - 'transactionId': 'fsafsa' + [ + 970, + 90 + ] + ], + 'bidId': '392b5a6b05d648', + 'bidderRequestId': '2946b569352ef2', + 'auctionId': '1863e370099523', + 'startTime': 1462918897462, + 'status': 1, + 'transactionId': 'fsafsa' + }, + { + 'bidder': 'appnexus', + 'params': { + 'placementId': '4799418' }, - { - 'bidder': 'appnexus', - 'params': { - 'placementId': '4799418' - }, - 'adUnitCode': '/19968336/header-bid-tag-0', - 'sizes': [ - [ - 300, - 250 - ], - [ - 300, - 600 - ] + 'adUnitCode': '/19968336/header-bid-tag-0', + 'sizes': [ + [ + 300, + 250 ], - 'bidId': '4dccdc37746135', - 'bidderRequestId': '2946b569352ef2', - 'auctionId': '1863e370099523', - 'startTime': 1462918897463, - 'status': 1, - 'transactionId': 'fsafsa' - } - ], - 'start': 1462918897460 - }]; + [ + 300, + 600 + ] + ], + 'bidId': '4dccdc37746135', + 'bidderRequestId': '2946b569352ef2', + 'auctionId': '1863e370099523', + 'startTime': 1462918897463, + 'status': 1, + 'transactionId': 'fsafsa' + } + ], + 'start': 1462918897460 + }]; + it('invokes callBids on the S2S adapter', function () { adapterManager.callBids( getAdUnits(), bidRequests, @@ -559,143 +608,6 @@ describe('adapterManager tests', function () { ] }); - let bidRequests = [{ - 'bidderCode': 'appnexus', - 'auctionId': '1863e370099523', - 'bidderRequestId': '2946b569352ef2', - 'tid': '34566b569352ef2', - 'src': 's2s', - 'timeout': 1000, - 'adUnitsS2SCopy': [ - { - 'code': '/19968336/header-bid-tag1', - 'sizes': [ - { - 'w': 728, - 'h': 90 - }, - { - 'w': 970, - 'h': 90 - } - ], - 'bids': [ - { - 'bidder': 'appnexus', - 'params': { - 'placementId': '543221', - 'test': 'me' - }, - 'adUnitCode': '/19968336/header-bid-tag1', - 'sizes': [ - [ - 728, - 90 - ], - [ - 970, - 90 - ] - ], - 'bidId': '68136e1c47023d', - 'bidderRequestId': '55e24a66bed717', - 'auctionId': '1ff753bd4ae5cb', - 'startTime': 1463510220995, - 'status': 1, - 'bid_id': '378a8914450b334' - } - ] - }, - { - 'code': '/19968336/header-bid-tag-0', - 'sizes': [ - { - 'w': 300, - 'h': 250 - }, - { - 'w': 300, - 'h': 600 - } - ], - 'bids': [ - { - 'bidder': 'appnexus', - 'params': { - 'placementId': '5324321' - }, - 'adUnitCode': '/19968336/header-bid-tag-0', - 'sizes': [ - [ - 300, - 250 - ], - [ - 300, - 600 - ] - ], - 'bidId': '7e5d6af25ed188', - 'bidderRequestId': '55e24a66bed717', - 'auctionId': '1ff753bd4ae5cb', - 'startTime': 1463510220996, - 'bid_id': '387d9d9c32ca47c' - } - ] - } - ], - 'bids': [ - { - 'bidder': 'appnexus', - 'params': { - 'placementId': '4799418', - 'test': 'me' - }, - 'adUnitCode': '/19968336/header-bid-tag1', - 'sizes': [ - [ - 728, - 90 - ], - [ - 970, - 90 - ] - ], - 'bidId': '392b5a6b05d648', - 'bidderRequestId': '2946b569352ef2', - 'auctionId': '1863e370099523', - 'startTime': 1462918897462, - 'status': 1, - 'transactionId': 'fsafsa' - }, - { - 'bidder': 'appnexus', - 'params': { - 'placementId': '4799418' - }, - 'adUnitCode': '/19968336/header-bid-tag-0', - 'sizes': [ - [ - 300, - 250 - ], - [ - 300, - 600 - ] - ], - 'bidId': '4dccdc37746135', - 'bidderRequestId': '2946b569352ef2', - 'auctionId': '1863e370099523', - 'startTime': 1462918897463, - 'status': 1, - 'transactionId': 'fsafsa' - } - ], - 'start': 1462918897460 - }]; - adapterManager.callBids( adUnits, bidRequests, @@ -749,133 +661,764 @@ describe('adapterManager tests', function () { }); }); // end s2s tests - describe('s2sTesting', function () { - let doneStub = sinon.stub(); - let ajaxStub = sinon.stub(); - - function getTestAdUnits() { - // copy adUnits - // return JSON.parse(JSON.stringify(getAdUnits())); - return utils.deepClone(getAdUnits()).map(adUnit => { - adUnit.bids = adUnit.bids.filter(bid => includes(['adequant', 'appnexus', 'rubicon'], bid.bidder)); - return adUnit; - }) - } - - function callBids(adUnits = getTestAdUnits()) { - let bidRequests = adapterManager.makeBidRequests(adUnits, 1111, 2222, 1000); - adapterManager.callBids(adUnits, bidRequests, doneStub, ajaxStub); - } - - function checkServerCalled(numAdUnits, numBids) { - sinon.assert.calledOnce(prebidServerAdapterMock.callBids); - let requestObj = prebidServerAdapterMock.callBids.firstCall.args[0]; - expect(requestObj.ad_units.length).to.equal(numAdUnits); - for (let i = 0; i < numAdUnits; i++) { - expect(requestObj.ad_units[i].bids.filter((bid) => { - return bid.bidder === 'appnexus' || bid.bidder === 'adequant'; - }).length).to.equal(numBids); - } - } - - function checkClientCalled(adapter, numBids) { - sinon.assert.calledOnce(adapter.callBids); - expect(adapter.callBids.firstCall.args[0].bids.length).to.equal(numBids); - } - - let TESTING_CONFIG = utils.deepClone(CONFIG); - Object.assign(TESTING_CONFIG, { - bidders: ['appnexus', 'adequant'], - testing: true - }); - let stubGetSourceBidderMap; - + describe('Multiple S2S tests', function () { beforeEach(function () { - config.setConfig({s2sConfig: TESTING_CONFIG}); + config.setConfig({s2sConfig: [CONFIG, CONFIG2]}); adapterManager.bidderRegistry['prebidServer'] = prebidServerAdapterMock; - adapterManager.bidderRegistry['adequant'] = adequantAdapterMock; - adapterManager.bidderRegistry['appnexus'] = appnexusAdapterMock; - adapterManager.bidderRegistry['rubicon'] = rubiconAdapterMock; - - stubGetSourceBidderMap = sinon.stub(s2sTesting, 'getSourceBidderMap'); - prebidServerAdapterMock.callBids.reset(); - adequantAdapterMock.callBids.reset(); - appnexusAdapterMock.callBids.reset(); - rubiconAdapterMock.callBids.reset(); }); afterEach(function () { - config.setConfig({s2sConfig: {}}); - s2sTesting.getSourceBidderMap.restore(); - }); - - it('calls server adapter if no sources defined', function () { - stubGetSourceBidderMap.returns({[s2sTesting.CLIENT]: [], [s2sTesting.SERVER]: []}); - callBids(); - - // server adapter - checkServerCalled(2, 2); - - // appnexus - sinon.assert.notCalled(appnexusAdapterMock.callBids); - - // adequant - sinon.assert.notCalled(adequantAdapterMock.callBids); - }); - - it('calls client adapter if one client source defined', function () { - stubGetSourceBidderMap.returns({[s2sTesting.CLIENT]: ['appnexus'], [s2sTesting.SERVER]: []}); - callBids(); - - // server adapter - checkServerCalled(2, 2); - - // appnexus - checkClientCalled(appnexusAdapterMock, 2); - - // adequant - sinon.assert.notCalled(adequantAdapterMock.callBids); + allS2SBidders.length = 0; }); - it('calls client adapters if client sources defined', function () { - stubGetSourceBidderMap.returns({[s2sTesting.CLIENT]: ['appnexus', 'adequant'], [s2sTesting.SERVER]: []}); - callBids(); - - // server adapter - checkServerCalled(2, 2); - - // appnexus - checkClientCalled(appnexusAdapterMock, 2); - - // adequant - checkClientCalled(adequantAdapterMock, 2); + const bidRequests = [{ + 'bidderCode': 'appnexus', + 'auctionId': '1863e370099523', + 'bidderRequestId': '2946b569352ef2', + 'tid': '34566b569352ef2', + 'timeout': 1000, + 'src': 's2s', + 'adUnitsS2SCopy': [ + { + 'code': '/19968336/header-bid-tag1', + 'sizes': [ + { + 'w': 728, + 'h': 90 + }, + { + 'w': 970, + 'h': 90 + } + ], + 'bids': [ + { + 'bidder': 'appnexus', + 'params': { + 'placementId': '543221', + 'test': 'me' + }, + 'adUnitCode': '/19968336/header-bid-tag1', + 'sizes': [ + [ + 728, + 90 + ], + [ + 970, + 90 + ] + ], + 'bidId': '68136e1c47023d', + 'bidderRequestId': '55e24a66bed717', + 'auctionId': '1ff753bd4ae5cb', + 'startTime': 1463510220995, + 'status': 1, + 'bid_id': '68136e1c47023d' + } + ] + }, + { + 'code': '/19968336/header-bid-tag-0', + 'sizes': [ + { + 'w': 300, + 'h': 250 + }, + { + 'w': 300, + 'h': 600 + } + ], + 'bids': [ + { + 'bidder': 'appnexus', + 'params': { + 'placementId': '5324321' + }, + 'adUnitCode': '/19968336/header-bid-tag-0', + 'sizes': [ + [ + 300, + 250 + ], + [ + 300, + 600 + ] + ], + 'bidId': '7e5d6af25ed188', + 'bidderRequestId': '55e24a66bed717', + 'auctionId': '1ff753bd4ae5cb', + 'startTime': 1463510220996, + 'bid_id': '7e5d6af25ed188' + } + ] + } + ], + 'bids': [ + { + 'bidder': 'appnexus', + 'params': { + 'placementId': '4799418', + 'test': 'me' + }, + 'adUnitCode': '/19968336/header-bid-tag1', + 'sizes': [ + [ + 728, + 90 + ], + [ + 970, + 90 + ] + ], + 'bidId': '392b5a6b05d648', + 'bidderRequestId': '2946b569352ef2', + 'auctionId': '1863e370099523', + 'startTime': 1462918897462, + 'status': 1, + 'transactionId': 'fsafsa' + }, + { + 'bidder': 'appnexus', + 'params': { + 'placementId': '4799418' + }, + 'adUnitCode': '/19968336/header-bid-tag-0', + 'sizes': [ + [ + 300, + 250 + ], + [ + 300, + 600 + ] + ], + 'bidId': '4dccdc37746135', + 'bidderRequestId': '2946b569352ef2', + 'auctionId': '1863e370099523', + 'startTime': 1462918897463, + 'status': 1, + 'transactionId': 'fsafsa' + } + ], + 'start': 1462918897460 + }, + { + 'bidderCode': 'pubmatic', + 'auctionId': '1863e370099523', + 'bidderRequestId': '2946b569352ef2', + 'tid': '2342342342lfi23', + 'timeout': 1000, + 'src': 's2s', + 'adUnitsS2SCopy': [ + { + 'code': '/19968336/header-bid-tag1', + 'sizes': [ + { + 'w': 728, + 'h': 90 + }, + { + 'w': 970, + 'h': 90 + } + ], + 'bids': [ + { + 'bidder': 'pubmatic', + 'params': { + 'placementId': '543221', + 'test': 'me' + }, + 'adUnitCode': '/19968336/header-bid-tag1', + 'sizes': [ + [ + 728, + 90 + ], + [ + 970, + 90 + ] + ], + 'bidId': '68136e1c47023d', + 'bidderRequestId': '55e24a66bed717', + 'auctionId': '1ff753bd4ae5cb', + 'startTime': 1463510220995, + 'status': 1, + 'bid_id': '68136e1c47023d' + } + ] + }, + { + 'code': '/19968336/header-bid-tag-0', + 'sizes': [ + { + 'w': 300, + 'h': 250 + }, + { + 'w': 300, + 'h': 600 + } + ], + 'bids': [ + { + 'bidder': 'pubmatic', + 'params': { + 'placementId': '5324321' + }, + 'adUnitCode': '/19968336/header-bid-tag-0', + 'sizes': [ + [ + 300, + 250 + ], + [ + 300, + 600 + ] + ], + 'bidId': '7e5d6af25ed188', + 'bidderRequestId': '55e24a66bed717', + 'auctionId': '1ff753bd4ae5cb', + 'startTime': 1463510220996, + 'bid_id': '7e5d6af25ed188' + } + ] + } + ], + 'bids': [ + { + 'bidder': 'pubmatic', + 'params': { + 'placementId': '4799418', + 'test': 'me' + }, + 'adUnitCode': '/19968336/header-bid-tag1', + 'sizes': [ + [ + 728, + 90 + ], + [ + 970, + 90 + ] + ], + 'bidId': '392b5a6b05d648', + 'bidderRequestId': '2946b569352ef2', + 'auctionId': '1863e370099523', + 'startTime': 1462918897462, + 'status': 1, + 'transactionId': '4r42r23r23' + }, + { + 'bidder': 'pubmatic', + 'params': { + 'placementId': '4799418' + }, + 'adUnitCode': '/19968336/header-bid-tag-0', + 'sizes': [ + [ + 300, + 250 + ], + [ + 300, + 600 + ] + ], + 'bidId': '4dccdc37746135', + 'bidderRequestId': '2946b569352ef2', + 'auctionId': '1863e370099523', + 'startTime': 1462918897463, + 'status': 1, + 'transactionId': '4r42r23r23' + } + ], + 'start': 1462918897460 + }]; + + it('invokes callBids on the S2S adapter', function () { + adapterManager.callBids( + getAdUnits(), + bidRequests, + () => {}, + () => () => {} + ); + sinon.assert.calledTwice(prebidServerAdapterMock.callBids); + }); + + // Enable this test when prebidServer adapter is made 1.0 compliant + it('invokes callBids with only s2s bids', function () { + const adUnits = getAdUnits(); + // adUnit without appnexus bidder + adUnits.push({ + 'code': '123', + 'sizes': [300, 250], + 'bids': [ + { + 'bidder': 'adequant', + 'params': { + 'publisher_id': '1234567', + 'bidfloor': 0.01 + } + } + ] + }); + + adapterManager.callBids( + adUnits, + bidRequests, + () => {}, + () => () => {} + ); + const requestObj = prebidServerAdapterMock.callBids.firstCall.args[0]; + expect(requestObj.ad_units.length).to.equal(2); + sinon.assert.calledTwice(prebidServerAdapterMock.callBids); + }); + + describe('BID_REQUESTED event', function () { + // function to count BID_REQUESTED events + let cnt, count = () => cnt++; + + beforeEach(function () { + prebidServerAdapterMock.callBids.reset(); + cnt = 0; + events.on(CONSTANTS.EVENTS.BID_REQUESTED, count); + }); + + afterEach(function () { + events.off(CONSTANTS.EVENTS.BID_REQUESTED, count); + }); + + it('should fire for s2s requests', function () { + let adUnits = utils.deepClone(getAdUnits()).map(adUnit => { + adUnit.bids = adUnit.bids.filter(bid => includes(['appnexus', 'pubmatic'], bid.bidder)); + return adUnit; + }) + let bidRequests = adapterManager.makeBidRequests(adUnits, 1111, 2222, 1000); + adapterManager.callBids(adUnits, bidRequests, () => {}, () => {}); + expect(cnt).to.equal(2); + sinon.assert.calledTwice(prebidServerAdapterMock.callBids); + }); + + it('should fire for simultaneous s2s and client requests', function () { + adapterManager.bidderRegistry['adequant'] = adequantAdapterMock; + let adUnits = utils.deepClone(getAdUnits()).map(adUnit => { + adUnit.bids = adUnit.bids.filter(bid => includes(['adequant', 'appnexus', 'pubmatic'], bid.bidder)); + return adUnit; + }) + let bidRequests = adapterManager.makeBidRequests(adUnits, 1111, 2222, 1000); + adapterManager.callBids(adUnits, bidRequests, () => {}, () => {}); + expect(cnt).to.equal(3); + sinon.assert.calledTwice(prebidServerAdapterMock.callBids); + sinon.assert.calledOnce(adequantAdapterMock.callBids); + adequantAdapterMock.callBids.reset(); + delete adapterManager.bidderRegistry['adequant']; + }); + }); + }); // end multiple s2s tests + + describe('s2sTesting', function () { + let doneStub = sinon.stub(); + let ajaxStub = sinon.stub(); + + function getTestAdUnits() { + // copy adUnits + // return JSON.parse(JSON.stringify(getAdUnits())); + return utils.deepClone(getAdUnits()).map(adUnit => { + adUnit.bids = adUnit.bids.filter(bid => includes(['adequant', 'appnexus', 'rubicon'], bid.bidder)); + return adUnit; + }) + } + + function callBids(adUnits = getTestAdUnits()) { + let bidRequests = adapterManager.makeBidRequests(adUnits, 1111, 2222, 1000); + adapterManager.callBids(adUnits, bidRequests, doneStub, ajaxStub); + } + + function checkServerCalled(numAdUnits, numBids) { + sinon.assert.calledOnce(prebidServerAdapterMock.callBids); + let requestObj = prebidServerAdapterMock.callBids.firstCall.args[0]; + expect(requestObj.ad_units.length).to.equal(numAdUnits); + for (let i = 0; i < numAdUnits; i++) { + expect(requestObj.ad_units[i].bids.filter((bid) => { + return bid.bidder === 'appnexus' || bid.bidder === 'adequant'; + }).length).to.equal(numBids); + } + } + + function checkClientCalled(adapter, numBids) { + sinon.assert.calledOnce(adapter.callBids); + expect(adapter.callBids.firstCall.args[0].bids.length).to.equal(numBids); + } + + let TESTING_CONFIG = utils.deepClone(CONFIG); + Object.assign(TESTING_CONFIG, { + bidders: ['appnexus', 'adequant'], + testing: true + }); + let stubGetSourceBidderMap; + + beforeEach(function () { + config.setConfig({s2sConfig: TESTING_CONFIG}); + adapterManager.bidderRegistry['prebidServer'] = prebidServerAdapterMock; + adapterManager.bidderRegistry['adequant'] = adequantAdapterMock; + adapterManager.bidderRegistry['appnexus'] = appnexusAdapterMock; + adapterManager.bidderRegistry['rubicon'] = rubiconAdapterMock; + + stubGetSourceBidderMap = sinon.stub(s2sTesting, 'getSourceBidderMap'); + + prebidServerAdapterMock.callBids.reset(); + adequantAdapterMock.callBids.reset(); + appnexusAdapterMock.callBids.reset(); + rubiconAdapterMock.callBids.reset(); + }); + + afterEach(function () { + s2sTesting.getSourceBidderMap.restore(); + }); + + it('calls server adapter if no sources defined', function () { + stubGetSourceBidderMap.returns({[s2sTesting.CLIENT]: [], [s2sTesting.SERVER]: []}); + callBids(); + + // server adapter + checkServerCalled(2, 2); + + // appnexus + sinon.assert.notCalled(appnexusAdapterMock.callBids); + + // adequant + sinon.assert.notCalled(adequantAdapterMock.callBids); + }); + + it('calls client adapter if one client source defined', function () { + stubGetSourceBidderMap.returns({[s2sTesting.CLIENT]: ['appnexus'], [s2sTesting.SERVER]: []}); + callBids(); + + // server adapter + checkServerCalled(2, 2); + + // appnexus + checkClientCalled(appnexusAdapterMock, 2); + + // adequant + sinon.assert.notCalled(adequantAdapterMock.callBids); + }); + + it('calls client adapters if client sources defined', function () { + stubGetSourceBidderMap.returns({[s2sTesting.CLIENT]: ['appnexus', 'adequant'], [s2sTesting.SERVER]: []}); + callBids(); + + // server adapter + checkServerCalled(2, 2); + + // appnexus + checkClientCalled(appnexusAdapterMock, 2); + + // adequant + checkClientCalled(adequantAdapterMock, 2); + }); + + it('does not call server adapter for bidders that go to client', function () { + stubGetSourceBidderMap.returns({[s2sTesting.CLIENT]: ['appnexus', 'adequant'], [s2sTesting.SERVER]: []}); + var adUnits = getTestAdUnits(); + adUnits[0].bids[0].finalSource = s2sTesting.CLIENT; + adUnits[0].bids[1].finalSource = s2sTesting.CLIENT; + adUnits[1].bids[0].finalSource = s2sTesting.CLIENT; + adUnits[1].bids[1].finalSource = s2sTesting.CLIENT; + callBids(adUnits); + + // server adapter + sinon.assert.notCalled(prebidServerAdapterMock.callBids); + + // appnexus + checkClientCalled(appnexusAdapterMock, 2); + + // adequant + checkClientCalled(adequantAdapterMock, 2); + }); + + it('does not call client adapters for bidders that go to server', function () { + stubGetSourceBidderMap.returns({[s2sTesting.CLIENT]: ['appnexus', 'adequant'], [s2sTesting.SERVER]: []}); + var adUnits = getTestAdUnits(); + adUnits[0].bids[0].finalSource = s2sTesting.SERVER; + adUnits[0].bids[1].finalSource = s2sTesting.SERVER; + adUnits[1].bids[0].finalSource = s2sTesting.SERVER; + adUnits[1].bids[1].finalSource = s2sTesting.SERVER; + callBids(adUnits); + + // server adapter + checkServerCalled(2, 2); + + // appnexus + sinon.assert.notCalled(appnexusAdapterMock.callBids); + + // adequant + sinon.assert.notCalled(adequantAdapterMock.callBids); + }); + + it('calls client and server adapters for bidders that go to both', function () { + stubGetSourceBidderMap.returns({[s2sTesting.CLIENT]: ['appnexus', 'adequant'], [s2sTesting.SERVER]: []}); + var adUnits = getTestAdUnits(); + // adUnits[0].bids[0].finalSource = s2sTesting.BOTH; + // adUnits[0].bids[1].finalSource = s2sTesting.BOTH; + // adUnits[1].bids[0].finalSource = s2sTesting.BOTH; + // adUnits[1].bids[1].finalSource = s2sTesting.BOTH; + callBids(adUnits); + + // server adapter + checkServerCalled(2, 2); + + // appnexus + checkClientCalled(appnexusAdapterMock, 2); + + // adequant + checkClientCalled(adequantAdapterMock, 2); + }); + + it('makes mixed client/server adapter calls for mixed bidder sources', function () { + stubGetSourceBidderMap.returns({[s2sTesting.CLIENT]: ['appnexus', 'adequant'], [s2sTesting.SERVER]: []}); + var adUnits = getTestAdUnits(); + adUnits[0].bids[0].finalSource = s2sTesting.CLIENT; + adUnits[0].bids[1].finalSource = s2sTesting.CLIENT; + adUnits[1].bids[0].finalSource = s2sTesting.SERVER; + adUnits[1].bids[1].finalSource = s2sTesting.SERVER; + callBids(adUnits); + + // server adapter + checkServerCalled(1, 2); + + // appnexus + checkClientCalled(appnexusAdapterMock, 1); + + // adequant + checkClientCalled(adequantAdapterMock, 1); + }); + }); + + describe('Multiple Server s2sTesting', function () { + let doneStub = sinon.stub(); + let ajaxStub = sinon.stub(); + + function getTestAdUnits() { + // copy adUnits + return utils.deepClone(getAdUnits()).map(adUnit => { + adUnit.bids = adUnit.bids.filter(bid => { + return includes(['adequant', 'appnexus', 'pubmatic', 'rubicon'], + bid.bidder); + }); + return adUnit; + }) + } + + function callBids(adUnits = getTestAdUnits()) { + let bidRequests = adapterManager.makeBidRequests(adUnits, 1111, 2222, 1000); + adapterManager.callBids(adUnits, bidRequests, doneStub, ajaxStub); + } + + function checkServerCalled(numAdUnits, firstConfigNumBids, secondConfigNumBids) { + let requestObjects = []; + let configBids; + if (firstConfigNumBids === 0 || secondConfigNumBids === 0) { + configBids = Math.max(firstConfigNumBids, secondConfigNumBids) + sinon.assert.calledOnce(prebidServerAdapterMock.callBids); + let requestObj1 = prebidServerAdapterMock.callBids.firstCall.args[0]; + requestObjects.push(requestObj1) + } else { + sinon.assert.calledTwice(prebidServerAdapterMock.callBids); + let requestObj1 = prebidServerAdapterMock.callBids.firstCall.args[0]; + let requestObj2 = prebidServerAdapterMock.callBids.secondCall.args[0]; + requestObjects.push(requestObj1, requestObj2); + } + + requestObjects.forEach((requestObj, index) => { + const numBids = configBids !== undefined ? configBids : index === 0 ? firstConfigNumBids : secondConfigNumBids + expect(requestObj.ad_units.length).to.equal(numAdUnits); + for (let i = 0; i < numAdUnits; i++) { + expect(requestObj.ad_units[i].bids.filter((bid) => { + return bid.bidder === 'appnexus' || bid.bidder === 'adequant' || bid.bidder === 'pubmatic'; + }).length).to.equal(numBids); + } + }) + } + + function checkClientCalled(adapter, numBids) { + sinon.assert.calledOnce(adapter.callBids); + expect(adapter.callBids.firstCall.args[0].bids.length).to.equal(numBids); + } + + beforeEach(function () { + allS2SBidders.length = 0; + clientTestAdapters.length = 0 + + adapterManager.bidderRegistry['prebidServer'] = prebidServerAdapterMock; + adapterManager.bidderRegistry['adequant'] = adequantAdapterMock; + adapterManager.bidderRegistry['appnexus'] = appnexusAdapterMock; + adapterManager.bidderRegistry['rubicon'] = rubiconAdapterMock; + adapterManager.bidderRegistry['pubmatic'] = pubmaticAdapterMock; + + prebidServerAdapterMock.callBids.reset(); + adequantAdapterMock.callBids.reset(); + appnexusAdapterMock.callBids.reset(); + rubiconAdapterMock.callBids.reset(); + pubmaticAdapterMock.callBids.reset(); + }); + + it('calls server adapter if no sources defined for config where testing is true, ' + + 'calls client adapter for second config where testing is false', function () { + let TEST_CONFIG = utils.deepClone(CONFIG); + Object.assign(TEST_CONFIG, { + bidders: ['appnexus', 'adequant'], + testing: true, + }); + let TEST_CONFIG2 = utils.deepClone(CONFIG2); + Object.assign(TEST_CONFIG2, { + bidders: ['pubmatic'], + testing: true + }); + + config.setConfig({s2sConfig: [TEST_CONFIG, TEST_CONFIG2]}); + + callBids(); + + // server adapter + checkServerCalled(2, 2, 1); + + // appnexus + sinon.assert.notCalled(appnexusAdapterMock.callBids); + + // adequant + sinon.assert.notCalled(adequantAdapterMock.callBids); + + // pubmatic + sinon.assert.notCalled(pubmaticAdapterMock.callBids); + + // rubicon + sinon.assert.called(rubiconAdapterMock.callBids); + }); + + it('calls client adapter if one client source defined for config where testing is true, ' + + 'calls client adapter for second config where testing is false', function () { + let TEST_CONFIG = utils.deepClone(CONFIG); + Object.assign(TEST_CONFIG, { + bidders: ['appnexus', 'adequant'], + bidderControl: { + appnexus: { + bidSource: { server: 0, client: 100 }, + includeSourceKvp: true, + }, + }, + testing: true, + }); + let TEST_CONFIG2 = utils.deepClone(CONFIG2); + Object.assign(TEST_CONFIG2, { + bidders: ['pubmatic'], + testing: true + }); + + config.setConfig({s2sConfig: [TEST_CONFIG, TEST_CONFIG2]}); + callBids(); + + // server adapter + checkServerCalled(2, 1, 1); + + // appnexus + checkClientCalled(appnexusAdapterMock, 2); + + // adequant + sinon.assert.notCalled(adequantAdapterMock.callBids); + + // pubmatic + sinon.assert.notCalled(pubmaticAdapterMock.callBids); + + // rubicon + checkClientCalled(rubiconAdapterMock, 1); }); - it('calls client adapters if client sources defined', function () { - stubGetSourceBidderMap.returns({[s2sTesting.CLIENT]: ['appnexus', 'adequant'], [s2sTesting.SERVER]: []}); + it('calls client adapters if client sources defined in first config and server in second config', function () { + let TEST_CONFIG = utils.deepClone(CONFIG); + Object.assign(TEST_CONFIG, { + bidders: ['appnexus', 'adequant'], + bidderControl: { + appnexus: { + bidSource: { server: 0, client: 100 }, + includeSourceKvp: true, + }, + adequant: { + bidSource: { server: 0, client: 100 }, + includeSourceKvp: true, + }, + }, + testing: true, + }); + + let TEST_CONFIG2 = utils.deepClone(CONFIG2); + Object.assign(TEST_CONFIG2, { + bidders: ['pubmatic'], + testing: true + }); + + config.setConfig({s2sConfig: [TEST_CONFIG, TEST_CONFIG2]}); + callBids(); // server adapter - checkServerCalled(2, 2); + checkServerCalled(2, 0, 1); // appnexus checkClientCalled(appnexusAdapterMock, 2); // adequant checkClientCalled(adequantAdapterMock, 2); + + // pubmatic + sinon.assert.notCalled(pubmaticAdapterMock.callBids); + + // rubicon + checkClientCalled(rubiconAdapterMock, 1); }); - it('does not call server adapter for bidders that go to client', function () { - stubGetSourceBidderMap.returns({[s2sTesting.CLIENT]: ['appnexus', 'adequant'], [s2sTesting.SERVER]: []}); - var adUnits = getTestAdUnits(); - adUnits[0].bids[0].finalSource = s2sTesting.CLIENT; - adUnits[0].bids[1].finalSource = s2sTesting.CLIENT; - adUnits[1].bids[0].finalSource = s2sTesting.CLIENT; - adUnits[1].bids[1].finalSource = s2sTesting.CLIENT; - callBids(adUnits); + it('does not call server adapter for bidders that go to client when both configs are set to client', function () { + let TEST_CONFIG = utils.deepClone(CONFIG); + Object.assign(TEST_CONFIG, { + bidders: ['appnexus', 'adequant'], + bidderControl: { + appnexus: { + bidSource: { server: 0, client: 100 }, + includeSourceKvp: true, + }, + adequant: { + bidSource: { server: 0, client: 100 }, + includeSourceKvp: true, + }, + }, + testing: true, + }); + + let TEST_CONFIG2 = utils.deepClone(CONFIG2); + Object.assign(TEST_CONFIG2, { + bidders: ['pubmatic'], + bidderControl: { + pubmatic: { + bidSource: { server: 0, client: 100 }, + includeSourceKvp: true, + }, + }, + testing: true + }); + + config.setConfig({s2sConfig: [TEST_CONFIG, TEST_CONFIG2]}); + callBids(); - // server adapter sinon.assert.notCalled(prebidServerAdapterMock.callBids); // appnexus @@ -883,63 +1426,110 @@ describe('adapterManager tests', function () { // adequant checkClientCalled(adequantAdapterMock, 2); + + // pubmatic + checkClientCalled(pubmaticAdapterMock, 2); + + // rubicon + checkClientCalled(rubiconAdapterMock, 1); }); - it('does not call client adapters for bidders that go to server', function () { - stubGetSourceBidderMap.returns({[s2sTesting.CLIENT]: ['appnexus', 'adequant'], [s2sTesting.SERVER]: []}); - var adUnits = getTestAdUnits(); - adUnits[0].bids[0].finalSource = s2sTesting.SERVER; - adUnits[0].bids[1].finalSource = s2sTesting.SERVER; - adUnits[1].bids[0].finalSource = s2sTesting.SERVER; - adUnits[1].bids[1].finalSource = s2sTesting.SERVER; - callBids(adUnits); + it('does not call client adapters for bidders in either config when testServerOnly if true in first config', function () { + let TEST_CONFIG = utils.deepClone(CONFIG); + Object.assign(TEST_CONFIG, { + bidders: ['appnexus', 'adequant'], + testServerOnly: true, + bidderControl: { + appnexus: { + bidSource: { server: 0, client: 100 }, + includeSourceKvp: true, + }, + adequant: { + bidSource: { server: 100, client: 0 }, + includeSourceKvp: true, + }, + }, + testing: true, + }); + + let TEST_CONFIG2 = utils.deepClone(CONFIG2); + Object.assign(TEST_CONFIG2, { + bidders: ['pubmatic'], + bidderControl: { + pubmatic: { + bidSource: { server: 0, client: 100 }, + includeSourceKvp: true, + } + }, + testing: true + }); + + config.setConfig({s2sConfig: [TEST_CONFIG, TEST_CONFIG2]}); + callBids(); // server adapter - checkServerCalled(2, 2); + checkServerCalled(2, 1, 0); // appnexus sinon.assert.notCalled(appnexusAdapterMock.callBids); // adequant sinon.assert.notCalled(adequantAdapterMock.callBids); - }); - it('calls client and server adapters for bidders that go to both', function () { - stubGetSourceBidderMap.returns({[s2sTesting.CLIENT]: ['appnexus', 'adequant'], [s2sTesting.SERVER]: []}); - var adUnits = getTestAdUnits(); - // adUnits[0].bids[0].finalSource = s2sTesting.BOTH; - // adUnits[0].bids[1].finalSource = s2sTesting.BOTH; - // adUnits[1].bids[0].finalSource = s2sTesting.BOTH; - // adUnits[1].bids[1].finalSource = s2sTesting.BOTH; - callBids(adUnits); + // pubmatic + sinon.assert.notCalled(pubmaticAdapterMock.callBids); - // server adapter - checkServerCalled(2, 2); + // rubicon + sinon.assert.notCalled(rubiconAdapterMock.callBids); + }); - // appnexus - checkClientCalled(appnexusAdapterMock, 2); + it('does not call client adapters for bidders in either config when testServerOnly if true in second config', function () { + let TEST_CONFIG = utils.deepClone(CONFIG); + Object.assign(TEST_CONFIG, { + bidders: ['appnexus', 'adequant'], + bidderControl: { + appnexus: { + bidSource: { server: 0, client: 100 }, + includeSourceKvp: true, + }, + adequant: { + bidSource: { server: 100, client: 0 }, + includeSourceKvp: true, + }, + }, + testing: true, + }); - // adequant - checkClientCalled(adequantAdapterMock, 2); - }); + let TEST_CONFIG2 = utils.deepClone(CONFIG2); + Object.assign(TEST_CONFIG2, { + bidders: ['pubmatic'], + testServerOnly: true, + bidderControl: { + pubmatic: { + bidSource: { server: 100, client: 0 }, + includeSourceKvp: true, + } + }, + testing: true + }); - it('makes mixed client/server adapter calls for mixed bidder sources', function () { - stubGetSourceBidderMap.returns({[s2sTesting.CLIENT]: ['appnexus', 'adequant'], [s2sTesting.SERVER]: []}); - var adUnits = getTestAdUnits(); - adUnits[0].bids[0].finalSource = s2sTesting.CLIENT; - adUnits[0].bids[1].finalSource = s2sTesting.CLIENT; - adUnits[1].bids[0].finalSource = s2sTesting.SERVER; - adUnits[1].bids[1].finalSource = s2sTesting.SERVER; - callBids(adUnits); + config.setConfig({s2sConfig: [TEST_CONFIG, TEST_CONFIG2]}); + callBids(); // server adapter - checkServerCalled(1, 2); + checkServerCalled(2, 1, 1); // appnexus - checkClientCalled(appnexusAdapterMock, 1); + sinon.assert.notCalled(appnexusAdapterMock.callBids); // adequant - checkClientCalled(adequantAdapterMock, 1); + sinon.assert.notCalled(adequantAdapterMock.callBids); + + // pubmatic + sinon.assert.notCalled(pubmaticAdapterMock.callBids); + + // rubicon + sinon.assert.notCalled(rubiconAdapterMock.callBids); }); }); @@ -988,6 +1578,27 @@ describe('adapterManager tests', function () { expect(adapterManager.aliasRegistry).to.have.property('s2sAlias'); }); + it('should allow an alias if alias is part of s2sConfig.bidders for multiple s2sConfigs', function () { + let testS2sConfig = utils.deepClone(CONFIG); + testS2sConfig.bidders = ['s2sAlias']; + config.setConfig({s2sConfig: [ + testS2sConfig, { + enabled: true, + endpoint: 'rp-pbs-endpoint-test.com', + timeout: 500, + maxBids: 1, + adapter: 'prebidServer', + bidders: ['s2sRpAlias'], + accountId: 'def' + } + ]}); + + adapterManager.aliasBidAdapter('s2sBidder', 's2sAlias'); + expect(adapterManager.aliasRegistry).to.have.property('s2sAlias'); + adapterManager.aliasBidAdapter('s2sBidder', 's2sRpAlias'); + expect(adapterManager.aliasRegistry).to.have.property('s2sRpAlias'); + }); + it('should throw an error if alias + bidder are unknown and not part of s2sConfig.bidders', function () { let testS2sConfig = utils.deepClone(CONFIG); testS2sConfig.bidders = ['s2sAlias']; @@ -1003,6 +1614,7 @@ describe('adapterManager tests', function () { describe('makeBidRequests', function () { let adUnits; beforeEach(function () { + allS2SBidders.length = 0 adUnits = utils.deepClone(getAdUnits()).map(adUnit => { adUnit.bids = adUnit.bids.filter(bid => includes(['appnexus', 'rubicon'], bid.bidder)); return adUnit; @@ -1054,6 +1666,8 @@ describe('adapterManager tests', function () { describe('sizeMapping', function () { beforeEach(function () { + allS2SBidders.length = 0; + clientTestAdapters.length = 0; sinon.stub(window, 'matchMedia').callsFake(() => ({matches: true})); }); @@ -1187,7 +1801,7 @@ describe('adapterManager tests', function () { [] ); - // only valid sizes as specified in size config should show up in bidRequests + // only valid sizes as specified in size config should show up in bidRequests bidRequests.forEach(bidRequest => { bidRequest.bids.forEach(bid => { bid.sizes.forEach(size => { @@ -1294,9 +1908,13 @@ describe('adapterManager tests', function () { describe('s2sTesting - testServerOnly', () => { beforeEach(() => { config.setConfig({ s2sConfig: getServerTestingConfig(CONFIG) }); + allS2SBidders.length = 0 + s2sTesting.bidSource = {}; }); - afterEach(() => config.resetConfig()); + afterEach(() => { + config.resetConfig(); + }); const makeBidRequests = ads => { let bidRequests = adapterManager.makeBidRequests( @@ -1413,5 +2031,192 @@ describe('adapterManager tests', function () { } ); }); + + describe('Multiple s2sTesting - testServerOnly', () => { + beforeEach(() => { + config.setConfig({s2sConfig: [getServerTestingConfig(CONFIG), CONFIG2]}); + }); + + afterEach(() => { + config.resetConfig() + allS2SBidders.length = 0; + s2sTesting.bidSource = {}; + }); + + const makeBidRequests = ads => { + let bidRequests = adapterManager.makeBidRequests( + ads, 1111, 2222, 1000 + ); + + bidRequests.sort((a, b) => { + if (a.bidderCode < b.bidderCode) return -1; + if (a.bidderCode > b.bidderCode) return 1; + return 0; + }); + + return bidRequests; + }; + + const removeAdUnitsBidSource = adUnits => adUnits.map(adUnit => { + const newAdUnit = { ...adUnit }; + newAdUnit.bids = newAdUnit.bids.map(bid => { + if (bid.bidSource) delete bid.bidSource; + return bid; + }); + return newAdUnit; + }); + + it('suppresses all client bids if there are server bids resulting from bidSource at the adUnit Level', () => { + let ads = getServerTestingsAds(); + ads.push({ + code: 'test_div_5', + sizes: [[300, 250]], + bids: [{ bidder: 'pubmatic' }] + }) + const bidRequests = makeBidRequests(ads); + + expect(bidRequests).lengthOf(3); + + expect(bidRequests[0].bids).lengthOf(1); + expect(bidRequests[0].bids[0].bidder).equals('openx'); + expect(bidRequests[0].bids[0].finalSource).equals('server'); + + expect(bidRequests[0].bids).lengthOf(1); + expect(bidRequests[1].bids[0].bidder).equals('pubmatic'); + expect(bidRequests[1].bids[0].finalSource).equals('server'); + + expect(bidRequests[0].bids).lengthOf(1); + expect(bidRequests[2].bids[0].bidder).equals('rubicon'); + expect(bidRequests[2].bids[0].finalSource).equals('server'); + }); + + it('should not surpress client side bids if testServerOnly is true in one config, ' + + ',bidderControl resolves to server in another config' + + 'and there are no bid with bidSource at the adUnit Level', () => { + let testConfig1 = utils.deepClone(getServerTestingConfig(CONFIG)); + let testConfig2 = utils.deepClone(CONFIG2); + testConfig1.testServerOnly = false; + testConfig2.testServerOnly = true; + testConfig2.testing = true; + testConfig2.bidderControl = { + 'pubmatic': { + bidSource: { server: 0, client: 100 }, + includeSourceKvp: true, + }, + }; + config.setConfig({s2sConfig: [testConfig1, testConfig2]}); + + let ads = [ + { + code: 'test_div_1', + sizes: [[300, 250]], + bids: [{ bidder: 'adequant' }] + }, + { + code: 'test_div_2', + sizes: [[300, 250]], + bids: [{ bidder: 'openx' }] + }, + { + code: 'test_div_3', + sizes: [[300, 250]], + bids: [{ bidder: 'pubmatic' }] + }, + ]; + const bidRequests = makeBidRequests(ads); + + expect(bidRequests).lengthOf(3); + + expect(bidRequests[0].bids).lengthOf(1); + expect(bidRequests[0].bids[0].bidder).equals('adequant'); + expect(bidRequests[0].bids[0].finalSource).equals('client'); + + expect(bidRequests[1].bids).lengthOf(1); + expect(bidRequests[1].bids[0].bidder).equals('openx'); + expect(bidRequests[1].bids[0].finalSource).equals('server'); + + expect(bidRequests[2].bids).lengthOf(1); + expect(bidRequests[2].bids[0].bidder).equals('pubmatic'); + expect(bidRequests[2].bids[0].finalSource).equals('client'); + }); + + // todo: update description + it('suppresses all, and only, client bids if there are bids resulting from bidSource at the adUnit Level', () => { + const ads = getServerTestingsAds(); + + // change this adUnit to be server based + ads[1].bids[1].bidSource.client = 0; + ads[1].bids[1].bidSource.server = 100; + + const bidRequests = makeBidRequests(ads); + + expect(bidRequests).lengthOf(3); + + expect(bidRequests[0].bids).lengthOf(1); + expect(bidRequests[0].bids[0].bidder).equals('appnexus'); + expect(bidRequests[0].bids[0].finalSource).equals('server'); + + expect(bidRequests[1].bids).lengthOf(1); + expect(bidRequests[1].bids[0].bidder).equals('openx'); + expect(bidRequests[1].bids[0].finalSource).equals('server'); + + expect(bidRequests[2].bids).lengthOf(1); + expect(bidRequests[2].bids[0].bidder).equals('rubicon'); + expect(bidRequests[2].bids[0].finalSource).equals('server'); + }); + + // we have a server call now + it('does not suppress client bids if no "test case" bids result in a server bid', () => { + const ads = getServerTestingsAds(); + + // change this adUnit to be client based + ads[0].bids[0].bidSource.client = 100; + ads[0].bids[0].bidSource.server = 0; + + const bidRequests = makeBidRequests(ads); + + expect(bidRequests).lengthOf(4); + + expect(bidRequests[0].bids).lengthOf(1); + expect(bidRequests[0].bids[0].bidder).equals('adequant'); + expect(bidRequests[0].bids[0].finalSource).equals('client'); + + expect(bidRequests[1].bids).lengthOf(2); + expect(bidRequests[1].bids[0].bidder).equals('appnexus'); + expect(bidRequests[1].bids[0].finalSource).equals('client'); + expect(bidRequests[1].bids[1].bidder).equals('appnexus'); + expect(bidRequests[1].bids[1].finalSource).equals('client'); + + expect(bidRequests[2].bids).lengthOf(1); + expect(bidRequests[2].bids[0].bidder).equals('openx'); + expect(bidRequests[2].bids[0].finalSource).equals('server'); + + expect(bidRequests[3].bids).lengthOf(2); + expect(bidRequests[3].bids[0].bidder).equals('rubicon'); + expect(bidRequests[3].bids[0].finalSource).equals('client'); + expect(bidRequests[3].bids[1].bidder).equals('rubicon'); + expect(bidRequests[3].bids[1].finalSource).equals('client'); + }); + + it( + 'should surpress client side bids if no ad unit bidSources are set, ' + + 'but bidderControl resolves to server', + () => { + const ads = removeAdUnitsBidSource(getServerTestingsAds()); + + const bidRequests = makeBidRequests(ads); + + expect(bidRequests).lengthOf(2); + + expect(bidRequests[0].bids).lengthOf(1); + expect(bidRequests[0].bids[0].bidder).equals('openx'); + expect(bidRequests[0].bids[0].finalSource).equals('server'); + + expect(bidRequests[1].bids).lengthOf(2); + expect(bidRequests[1].bids[0].bidder).equals('rubicon'); + expect(bidRequests[1].bids[0].finalSource).equals('server'); + } + ); + }); }); }); diff --git a/test/spec/unit/core/bidderFactory_spec.js b/test/spec/unit/core/bidderFactory_spec.js index 692cf9a6475..a7e8a0d7871 100644 --- a/test/spec/unit/core/bidderFactory_spec.js +++ b/test/spec/unit/core/bidderFactory_spec.js @@ -673,6 +673,28 @@ describe('registerBidder', function () { expect(registerBidAdapterStub.getCall(2).args[0].getSpec().gvlid).to.equal(2); expect(registerBidAdapterStub.getCall(3).args[0].getSpec().gvlid).to.equal(undefined); }) + + it('should register alias with skipPbsAliasing', function() { + const aliases = [ + { + code: 'foo', + skipPbsAliasing: true + }, + { + code: 'bar', + skipPbsAliasing: false + }, + { + code: 'baz' + } + ] + const thisSpec = Object.assign(newEmptySpec(), { aliases: aliases }); + registerBidder(thisSpec); + + expect(registerBidAdapterStub.getCall(1).args[0].getSpec().skipPbsAliasing).to.equal(true); + expect(registerBidAdapterStub.getCall(2).args[0].getSpec().skipPbsAliasing).to.equal(false); + expect(registerBidAdapterStub.getCall(3).args[0].getSpec().skipPbsAliasing).to.equal(undefined); + }) }) describe('validate bid response: ', function () { diff --git a/webpack.conf.js b/webpack.conf.js index a5c75fa8a1a..6f0841757b0 100644 --- a/webpack.conf.js +++ b/webpack.conf.js @@ -13,7 +13,8 @@ var neverBundle = [ ]; var plugins = [ - new RequireEnsureWithoutJsonp() + new RequireEnsureWithoutJsonp(), + new webpack.EnvironmentPlugin(['LiveConnectMode']) ]; if (argv.analyze) {