diff --git a/.babelrc.js b/.babelrc.js index bece57ec4a5..2fa95d0716e 100644 --- a/.babelrc.js +++ b/.babelrc.js @@ -20,7 +20,7 @@ module.exports = { "safari >=8", "edge >= 14", "ff >= 57", - "ie >= 10", + "ie >= 11", "ios >= 8" ] } diff --git a/.circleci/config.yml b/.circleci/config.yml index 5ea5e87218c..ea5fb916a91 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -8,7 +8,7 @@ aliases: docker: # specify the version you desire here - image: circleci/node:12.16.1 - + resource_class: xlarge # Specify service dependencies here if necessary # CircleCI maintains a library of pre-built images # documented at https://circleci.com/docs/2.0/circleci-images/ @@ -94,4 +94,4 @@ workflows: - e2etest experimental: - pipelines: true \ No newline at end of file + pipelines: true 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/CONTRIBUTING.md b/CONTRIBUTING.md index 016f4055216..606d26cd25a 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -3,9 +3,11 @@ Contributions are always welcome. To contribute, [fork](https://help.github.com/ commit your changes, and [open a pull request](https://help.github.com/articles/using-pull-requests/) against the master branch. -Pull requests must have 80% code coverage before beign considered for merge. +Pull requests must have 80% code coverage before being considered for merge. Additional details about the process can be found [here](./PR_REVIEW.md). +There are more details available if you'd like to contribute a [bid adapter](https://docs.prebid.org/dev-docs/bidder-adaptor.html) or [analytics adapter](https://docs.prebid.org/dev-docs/integrate-with-the-prebid-analytics-api.html). + ## Issues [prebid.org](http://prebid.org/) contains documentation that may help answer questions you have about using Prebid.js. If you can't find the answer there, try searching for a similar issue on the [issues page](https://github.com/prebid/Prebid.js/issues). @@ -57,7 +59,7 @@ When you are adding code to Prebid.js, or modifying code that isn't covered by a Prebid.js already has many tests. Read them to see how Prebid.js is tested, and for inspiration: - Look in `test/spec` and its subdirectories -- Tests for bidder adaptors are located in `test/spec/modules` +- Tests for bidder adapters are located in `test/spec/modules` A test module might have the following general structure: diff --git a/PR_REVIEW.md b/PR_REVIEW.md index 6402fcbbbaa..84131c177a3 100644 --- a/PR_REVIEW.md +++ b/PR_REVIEW.md @@ -5,16 +5,55 @@ 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). - Verify PR is a single change type. Example, refactor OR bugfix. If more than 1 type, ask submitter to break out requests. -- Verify code under review has at least 80% unit test coverage. If legacy code has no unit test coverage, ask for unit tests to be included in the PR. +- Verify code under review has at least 80% unit test coverage. If legacy code doesn't have enough unit test coverage, require that additional unit tests to be included in the PR. - Verify tests are green in Travis-ci + local build by running `gulp serve` | `gulp test` - Verify no code quality violations are present from linting (should be reported in terminal) +- Make sure the code is not setting cookies or localstorage directly -- it must use the `StorageManager`. - Review for obvious errors or bad coding practice / use best judgement here. - If the change is a new feature / change to core prebid.js - review the change with a Tech Lead on the project and make sure they agree with the nature of change. - 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. - - Below are some examples of bidder specific updates that should require docs update (in their dev-docs/bidders/bidder.md file): +- 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 +- 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 + +Follow steps above for general review process. In addition, please verify the following: +- Verify the biddercode and aliases are valid: + - Lower case alphanumeric with the only special character allowed is underscore. + - The bidder code should be unique for the first 6 characters + - Reserved words that cannot be used as bidder names: all, context, data, general, prebid, and skadn +- Verify that bidder has submitted valid bid params and that bids are being received. +- Verify that bidder is not manipulating the prebid.js auction in any way or doing things that go against the principles of the project. If unsure check with the Tech Lead. +- Verify that code re-use is being done properly and that changes introduced by a bidder don't impact other bidders. +- If the adapter being submitted is an alias type, check with the bidder contact that is being aliased to make sure it's allowed. +- All bidder parameter conventions must be followed: + - Video params must be read from AdUnit.mediaTypes.video when available; however bidder config can override the ad unit. + - First party data must be read from [`fpd.context` and `fpd.user`](https://docs.prebid.org/dev-docs/publisher-api-reference.html#setConfig-fpd). + - Adapters that accept a floor parameter must also support the [floors module](https://docs.prebid.org/dev-docs/modules/floors.html) -- look for a call to the `getFloor()` function. + - Adapters cannot accept an schain parameter. Rather, they must look for the schain parameter at bidRequest.schain. + - The bidRequest page referrer must checked in addition to any bidder-specific parameter. + - If they're getting the COPPA flag, it must come from config.getConfig('coppa'); +- Below are some examples of bidder specific updates that should require docs update (in their dev-docs/bidders/BIDDER.md file): - If they support the GDPR consentManagement module and TCF1, add `gdpr_supported: true` - If they support the GDPR consentManagement module and TCF2, add `tcf2_supported: true` - If they support the US Privacy consentManagementUsp module, add `usp_supported: true` @@ -23,31 +62,60 @@ For modules and core platform updates, the initial reviewer should request an ad - If they support COPPA, add `coppa_supported: true` - If they support SChain, add `schain_supported: true` - If their bidder doesn't work well with safeframed creatives, add `safeframes_ok: false`. This will alert publishers to not use safeframed creatives when creating the ad server entries for their bidder. - - If they're a member of Prebid.org, add `prebid_member: true` -- If all above is good, add a `LGTM` comment and request 1 additional core member to review. -- Once there is 2 `LGTM` on the PR, merge to master -- Ask the submitter to add a PR for documentation if applicable. -- 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) -- Add the PR to the appropriate project board (I.E. 1.23.0 Release) for the week, [see](https://github.com/prebid/Prebid.js/projects) - -### New Adapter or updates to adapter process -- Follow steps above for general review process. In addition, please verify the following: -- Verify that bidder has submitted valid bid params and that bids are being received. -- Verify that bidder is not manipulating the prebid.js auction in any way or doing things that go against the principles of the project. If unsure check with the Tech Lead. -- Verify that the bidder is being as efficient as possible, ideally not loading an external library, however if they do load a library it should be cached. -- Verify that code re-use is being done properly and that changes introduced by a bidder don't impact other bidders. -- If the adapter being submitted is an alias type, check with the bidder contact that is being aliased to make sure it's allowed. -- If the adapter is triggering any user syncs make sure they are using the user sync module in the Prebid.js core. -- Requests to the bidder should support HTTPS -- Responses from the bidder should be compressed (such as gzip, compress, deflate) -- Bid responses may not use JSONP: All requests must be AJAX with JSON responses -- Video openrtb params should be read from the ad unit when available; however bidder config can override the ad unit. -- All user-sync (aka pixel) activity must be registered via the provided functions -- Adapters may not use the $$PREBID_GLOBAL$$ variable -- All adapters must support the creation of multiple concurrent instances. This means, for example, that adapters cannot rely on mutable global variables. -- Adapters may not globally override or default the standard ad server targeting values: hb_adid, hb_bidder, hb_pb, hb_deal, or hb_size, hb_source, hb_format. + - If they're setting a deal ID in some scenarios, add `bidder_supports_deals: true` + - If they have an IAB Global Vendor List ID, add `gvl_id: ID`. There's no default. - After a new adapter is approved, let the submitter know they may open a PR in the [headerbid-expert repository](https://github.com/prebid/headerbid-expert) to have their adapter recognized by the [Headerbid Expert extension](https://chrome.google.com/webstore/detail/headerbid-expert/cgfkddgbnfplidghapbbnngaogeldmop). The PR should be to the [bidder patterns file](https://github.com/prebid/headerbid-expert/blob/master/bidderPatterns.js), adding an entry with their adapter's name and the url the adapter uses to send and receive bid responses. +### Reviewing a New or Updated Analytics Adapter +Documentation they're supposed to be following is https://docs.prebid.org/dev-docs/integrate-with-the-prebid-analytics-api.html + +No additional steps above the general review process and making sure it conforms to the [Module Rules](https://docs.prebid.org/dev-docs/module-rules.html). + +Make sure there's a docs pull request + +### Reviewing a New or Updated User ID Sub-Module +Documentation they're supposed to be following is https://docs.prebid.org/dev-docs/modules/userId.html#id-providers + +Follow steps above for general review process. In addition: +- Try running the new user ID module with a basic config and confirm it hits the endpoint and stores the results. +- the filename should be camel case ending with `IdSystem` (e.g. `myCompanyIdSystem.js`) +- the `const MODULE_NAME` value should be camel case ending with `Id` (e.g. `myCompanyId` ) +- the response of the `decode` method should be an object with the key being ideally camel case similar to the module name and ending in `id` or `Id`, but in some cases this value is a shortened name and sometimes with the `id` part being all lowercase, provided there are no other uppercase letters. if there's no id or it's an invalid object, the response should be `undefined`. example "valid" values (although this is more style than a requirement) + - `mcid` + - `mcId` + - `myCompanyId` +- make sure they've added references of their new module everywhere required: + - modules/.submodules.json + - modules/userId/eids.js + - modules/userId/eids.md + - modules/userId/userId.md +- tests can go either within the userId_spec.js file or in their own _spec file if they wish +- GVLID is recommended in the *IdSystem file if they operate in EU +- make sure example configurations align to the actual code (some modules use the userId storage settings and allow pub configuration, while others handle reading/writing cookies on their own, so should not include the storage params in examples) +- the 3 available methods (getId, extendId, decode) should be used as they were intended + - decode (required method) should not be making requests to retrieve a new ID, it should just be decoding a response + - extendId (optional method) should not be making requests to retrieve a new ID, it should just be adding additional data to the id object + - getId (required method) should be the only method that gets a new ID (from ajax calls or a cookie/local storage). this ensures that decode and extend do not unnecessarily delay the auction in places where it is not expected. +- in the eids.js file, the source should be the actual domain of the provider, not a made up domain. +- in the eids.js file, the key in the array should be the same value as the key in the decode function +- make sure all supported config params align in the submodule js file and the docs / examples +- make sure there's a docs pull request + +### Reviewing a New or Updated Real-Time-Data Sub-Module +Documentation they're supposed to be following is https://docs.prebid.org/dev-docs/add-rtd-submodule.html + +Follow steps above for general review process. In addition: +- The RTD Provider must include a `providerRtdProvider.md` file. This file must have example parameters and document a sense of what to expect: what should change in the bidrequest, or what targeting data should be added? +- Try running the new sub-module and confirm the provided test parameters. +- Confirm that the module + - is not loading external code. If it is, escalate to the #prebid-js Slack channel. + - is reading `config` from the function signature rather than calling `getConfig`. + - is sending data to the bid request only as either First Party Data or in bidRequest.rtd.RTDPROVIDERCODE. + - is making HTTPS requests as early as possible, but not more often than needed. + - doesn't force bid adapters to load additional code. +- Consider whether the kind of data the module is obtaining could have privacy implications. If so, make sure they're utilizing the `consent` data passed to them. +- Make sure there's a docs pull request + ## Ticket Coordinator Each week, Prebid Org assigns one person to keep an eye on incoming issues and PRs. Every Monday morning a reminder is diff --git a/README.md b/README.md index b3f33b27aa5..a220dd69f66 100644 --- a/README.md +++ b/README.md @@ -93,12 +93,12 @@ Or for Babel 6: Then you can use Prebid.js as any other npm depedendency ```javascript -import prebid from 'prebid.js'; +import pbjs from 'prebid.js'; import 'prebid.js/modules/rubiconBidAdapter'; // imported modules will register themselves automatically with prebid import 'prebid.js/modules/appnexusBidAdapter'; -prebid.processQueue(); // required to process existing pbjs.queue blocks and setup any further pbjs.queue execution +pbjs.processQueue(); // required to process existing pbjs.queue blocks and setup any further pbjs.queue execution -prebid.requestBids({ +pbjs.requestBids({ ... }) @@ -112,11 +112,11 @@ prebid.requestBids({ $ git clone https://github.com/prebid/Prebid.js.git $ cd Prebid.js - $ npm install + $ npm ci *Note:* You need to have `NodeJS` 12.16.1 or greater installed. -*Note:* In the 1.24.0 release of Prebid.js we have transitioned to using gulp 4.0 from using gulp 3.9.1. To comply with gulp's recommended setup for 4.0, you'll need to have `gulp-cli` installed globally prior to running the general `npm install`. This shouldn't impact any other projects you may work on that use an earlier version of gulp in its setup. +*Note:* In the 1.24.0 release of Prebid.js we have transitioned to using gulp 4.0 from using gulp 3.9.1. To comply with gulp's recommended setup for 4.0, you'll need to have `gulp-cli` installed globally prior to running the general `npm ci`. This shouldn't impact any other projects you may work on that use an earlier version of gulp in its setup. If you have a previous version of `gulp` installed globally, you'll need to remove it before installing `gulp-cli`. You can check if this is installed by running `gulp -v` and seeing the version that's listed in the `CLI` field of the output. If you have the `gulp` package installed globally, it's likely the same version that you'll see in the `Local` field. If you already have `gulp-cli` installed, it should be a lower major version (it's at version `2.0.1` at the time of the transition). @@ -142,7 +142,7 @@ This runs some code quality checks, starts a web server at `http://localhost:999 ### Build Optimization -The standard build output contains all the available modules from within the `modules` folder. +The standard build output contains all the available modules from within the `modules` folder. Note, however that there are bid adapters which support multiple bidders through aliases, so if you don't see a file in modules for a bid adapter, you may need to grep the repository to find the name of the module you need to include. You might want to exclude some/most of them from the final bundle. To make sure the build only includes the modules you want, you can specify the modules to be included with the `--modules` CLI argument. @@ -202,6 +202,11 @@ To run the unit tests: gulp test ``` +To run the unit tests for a particular 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 @@ -260,7 +265,7 @@ directory you will have sourcemaps available in your browser's developer tools. To run the example file, go to: -+ `http://localhost:9999/integrationExamples/gpt/pbjs_example_gpt.html` ++ `http://localhost:9999/integrationExamples/gpt/hello_world.html` As you make code changes, the bundles will be rebuilt and the page reloaded automatically. @@ -268,7 +273,7 @@ As you make code changes, the bundles will be rebuilt and the page reloaded auto ## Contribute -Many SSPs, bidders, and publishers have contributed to this project. [60+ Bidders](https://github.com/prebid/Prebid.js/tree/master/src/adapters) are supported by Prebid.js. +Many SSPs, bidders, and publishers have contributed to this project. [Hundreds of bidders](https://github.com/prebid/Prebid.js/tree/master/src/adapters) are supported by Prebid.js. For guidelines, see [Contributing](./CONTRIBUTING.md). @@ -276,9 +281,7 @@ Our PR review process can be found [here](https://github.com/prebid/Prebid.js/tr ### Add a Bidder Adapter -To add a bidder adapter module, see the instructions in [How to add a bidder adaptor](http://prebid.org/dev-docs/bidder-adaptor.html). - -Please **do NOT load Prebid.js inside your adapter**. If you do this, we will reject or remove your adapter as appropriate. +To add a bidder adapter module, see the instructions in [How to add a bidder adapter](https://docs.prebid.org/dev-docs/bidder-adaptor.html). ### Code Quality 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/browsers.json b/browsers.json index 91e0548d78a..f73bbbb8aac 100644 --- a/browsers.json +++ b/browsers.json @@ -23,11 +23,11 @@ "device": null, "os": "Windows" }, - "bs_chrome_80_windows_10": { + "bs_chrome_89_windows_10": { "base": "BrowserStack", "os_version": "10", "browser": "chrome", - "browser_version": "80.0", + "browser_version": "89.0", "device": null, "os": "Windows" }, diff --git a/gulpfile.js b/gulpfile.js index 64152baa7ba..e2bed2a660f 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 + '*/\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([ @@ -110,6 +124,7 @@ function watch(done) { connect.server({ https: argv.https, port: port, + host: FAKE_SERVER_HOST, root: './', livereload: true }); @@ -119,6 +134,12 @@ function watch(done) { done(); }; +function makeModuleList(modules) { + return modules.map(module => { + return '"' + module + '"' + }); +} + function makeDevpackPkg() { var cloned = _.cloneDeep(webpackConfig); cloned.devtool = 'source-map'; @@ -130,6 +151,7 @@ function makeDevpackPkg() { return gulp.src([].concat(moduleSources, analyticsSources, 'src/prebid.js')) .pipe(helpers.nameModules(externalModules)) .pipe(webpackStream(cloned, webpack)) + .pipe(replace(/('|")v\$prebid\.modulesList\$('|")/g, makeModuleList(externalModules))) .pipe(gulp.dest('build/dev')) .pipe(connect.reload()); } @@ -147,10 +169,15 @@ function makeWebpackPkg() { .pipe(helpers.nameModules(externalModules)) .pipe(webpackStream(cloned, webpack)) .pipe(uglify()) + .pipe(replace(/('|")v\$prebid\.modulesList\$('|")/g, makeModuleList(externalModules))) .pipe(gulpif(file => file.basename === 'prebid-core.js', header(banner, { prebid: prebid }))) .pipe(gulp.dest('build/dist')); } +function getModulesListToAddInBanner(modules) { + return (modules.length > 0) ? modules.join(', ') : 'All available modules in current version.'; +} + function gulpBundle(dev) { return bundle(dev).pipe(gulp.dest('build/' + (dev ? 'dev' : 'dist'))); } @@ -200,6 +227,8 @@ function bundle(dev, moduleArr) { return gulp.src( entries ) + // Need to uodate the "Modules: ..." section in comment with the current modules list + .pipe(replace(/(Modules: )(.*?)(\*\/)/, ('$1' + getModulesListToAddInBanner(helpers.getArgModules()) + ' $3'))) .pipe(gulpif(dev, sourcemaps.init({ loadMaps: true }))) .pipe(concat(outputFileName)) .pipe(gulpif(!argv.manualEnable, footer('\n<%= global %>.processQueue();', { @@ -264,7 +293,7 @@ function test(done) { } else { var karmaConf = karmaConfMaker(false, argv.browserstack, argv.watch, argv.file); - var browserOverride = helpers.parseBrowserArgs(argv).map(helpers.toCapitalCase); + var browserOverride = helpers.parseBrowserArgs(argv); if (browserOverride.length > 0) { karmaConf.browsers = browserOverride; } @@ -382,4 +411,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/adUnitFloors.html b/integrationExamples/gpt/adUnitFloors.html new file mode 100644 index 00000000000..bb48a20ef78 --- /dev/null +++ b/integrationExamples/gpt/adUnitFloors.html @@ -0,0 +1,112 @@ + + + + + + + + + + + + + + +

Prebid.js Test

+
Div-1
+
+ +
+ + + + diff --git a/integrationExamples/gpt/adloox.html b/integrationExamples/gpt/adloox.html new file mode 100644 index 00000000000..33f8b9be6a2 --- /dev/null +++ b/integrationExamples/gpt/adloox.html @@ -0,0 +1,211 @@ + + + + Prebid Display/Video Merged Auction with Adloox Integration + + + + + + + + +

Prebid Display/Video Merged Auction with Adloox Integration

+ +

div-1

+
+ +
+ +

div-2

+
+ +
+ +

video-1

+
+ + + + diff --git a/integrationExamples/gpt/audigentSegments_example.html b/integrationExamples/gpt/audigentSegments_example.html deleted file mode 100644 index 7739b558327..00000000000 --- a/integrationExamples/gpt/audigentSegments_example.html +++ /dev/null @@ -1,257 +0,0 @@ - - - - - - - - - - - - - - - -

Audigent Segments Prebid

- -
- -
-TDID: -
-
- -Audigent Segments: -
-
- - diff --git a/integrationExamples/gpt/creative_rendering.html b/integrationExamples/gpt/creative_rendering.html index aef8b7f1654..04d4736c631 100644 --- a/integrationExamples/gpt/creative_rendering.html +++ b/integrationExamples/gpt/creative_rendering.html @@ -1,9 +1,4 @@ - - - - + - + + + - +

Prebid.js Test

Div-1
diff --git a/integrationExamples/gpt/haloRtdProvider_example.html b/integrationExamples/gpt/haloRtdProvider_example.html new file mode 100644 index 00000000000..14debbd2698 --- /dev/null +++ b/integrationExamples/gpt/haloRtdProvider_example.html @@ -0,0 +1,125 @@ + + + + + + + + + + + + + + + +

Halo RTD Prebid

+ +
+ +
+ +Halo Id: +
+
+ +Halo Real-Time Data: +
+
+ + diff --git a/integrationExamples/gpt/idImportLibrary_example.html b/integrationExamples/gpt/idImportLibrary_example.html new file mode 100644 index 00000000000..a3ef3f168c0 --- /dev/null +++ b/integrationExamples/gpt/idImportLibrary_example.html @@ -0,0 +1,351 @@ + + + + + + + + + + + + + +Prebid + +

ID Import Library Example

+

Steps before logging in:

+ + + + + + + + + + + diff --git a/integrationExamples/gpt/jwplayerRtdProvider_example.html b/integrationExamples/gpt/jwplayerRtdProvider_example.html new file mode 100644 index 00000000000..41c27b70ece --- /dev/null +++ b/integrationExamples/gpt/jwplayerRtdProvider_example.html @@ -0,0 +1,106 @@ + + + + + + + JW Player RTD Provider Example + + + + + + + + +
Div-1
+
+ +
+ + diff --git a/integrationExamples/gpt/permutiveRtdProvider_example.html b/integrationExamples/gpt/permutiveRtdProvider_example.html new file mode 100644 index 00000000000..a06430bcdfa --- /dev/null +++ b/integrationExamples/gpt/permutiveRtdProvider_example.html @@ -0,0 +1,232 @@ + + + + + + + + + + + +

+

Basic Prebid.js Example

+
Div-1
+
+ +
+ +
+ +
Div-2
+
+ +
+ + + + diff --git a/integrationExamples/gpt/prebidServer_example.html b/integrationExamples/gpt/prebidServer_example.html index 7761178efa8..37902edd979 100644 --- a/integrationExamples/gpt/prebidServer_example.html +++ b/integrationExamples/gpt/prebidServer_example.html @@ -56,10 +56,10 @@ s2sConfig : { accountId : '1', enabled : true, //default value set to false + defaultVendor: 'appnexus', bidders : ['appnexus'], timeout : 1000, //default value is 1000 adapter : 'prebidServer', //if we have any other s2s adapter, default value is s2s - endpoint : 'https://prebid.adnxs.com/pbs/v1/openrtb2/auction' } }); diff --git a/integrationExamples/gpt/reconciliationRtdProvider_example.html b/integrationExamples/gpt/reconciliationRtdProvider_example.html new file mode 100644 index 00000000000..4f9b663c22d --- /dev/null +++ b/integrationExamples/gpt/reconciliationRtdProvider_example.html @@ -0,0 +1,102 @@ + + + + + + + Reconciliation RTD Provider Example + + + + + + + + +
Div-1
+
+ +
+ + diff --git a/integrationExamples/gpt/sirdataRtdProvider_example.html b/integrationExamples/gpt/sirdataRtdProvider_example.html new file mode 100644 index 00000000000..444c9133905 --- /dev/null +++ b/integrationExamples/gpt/sirdataRtdProvider_example.html @@ -0,0 +1,146 @@ + + + + + + + + + + + + + + + + +
+

+Give consent or make a choice in Europe. Module will add key/value pairs in ad calls. Check out for sd_rtd key in Google Ad call (https://securepubads.g.doubleclick.net/gampad/ads...) and in the payload sent to Xandr to endpoint https://ib.adnxs.com/ut/v3/prebid : tags[0].keywords.key[sd_rtd] should have an array of string as value. This array will mix user segments and/or page categories based on user's choices. +

+
+

Basic Prebid.js Example

+
Div-1
+
+ +
+ + + \ No newline at end of file diff --git a/integrationExamples/gpt/userId_example.html b/integrationExamples/gpt/userId_example.html index 4acbe0aae31..6fbdd69aea7 100644 --- a/integrationExamples/gpt/userId_example.html +++ b/integrationExamples/gpt/userId_example.html @@ -1,276 +1,321 @@ - + + + - + } + ] + } + ]; - + - var adUnits = [ - { - code: 'test-div', - sizes: [[300,250],[300,600],[728,90]], - bids: [ - { - bidder: 'rubicon', - params: { - accountId: '1001', - siteId: '113932', - zoneId: '535510' - } - } - ] - } - ]; + - - - + 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

+

User ID Modules Example

+ +

Generated EIDs

+ +

 
-
+

Ad Slot

+
-
+
+ diff --git a/integrationExamples/mass/index.html b/integrationExamples/mass/index.html new file mode 100644 index 00000000000..3b034957d13 --- /dev/null +++ b/integrationExamples/mass/index.html @@ -0,0 +1,132 @@ + + + + + + + + + + + + + + + +
+

Note: for this example to work, you need access to a bid simulation tool from your MASS enabled Exchange partner.

+
+ +
+
+ + 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/karma.conf.maker.js b/karma.conf.maker.js index 712ef14caa1..8af216d6262 100644 --- a/karma.conf.maker.js +++ b/karma.conf.maker.js @@ -170,6 +170,16 @@ module.exports = function(codeCoverage, browserstack, watchMode, file) { plugins: plugins } + + // To ensure that, we are able to run single spec file + // here we are adding preprocessors, when file is passed + if (file) { + config.files.forEach((file) => { + config.preprocessors[file] = ['webpack', 'sourcemap']; + }); + delete config.preprocessors['test/test_index.js']; + } + setReporters(config, codeCoverage, browserstack); setBrowsers(config, browserstack); return config; diff --git a/modules/.submodules.json b/modules/.submodules.json index 58ab8798fa5..0a10044a78e 100644 --- a/modules/.submodules.json +++ b/modules/.submodules.json @@ -7,16 +7,44 @@ "britepoolIdSystem", "liveIntentIdSystem", "lotamePanoramaId", + "merkleIdSystem", "criteoIdSystem", "netIdSystem", "identityLinkIdSystem", - "sharedIdSystem" + "sharedIdSystem", + "intentIqIdSystem", + "zeotapIdPlusIdSystem", + "haloIdSystem", + "quantcastIdSystem", + "deepintentDpesIdSystem", + "nextrollIdSystem", + "idxIdSystem", + "fabrickIdSystem", + "verizonMediaIdSystem", + "pubProvidedIdSystem", + "mwOpenLinkIdSystem", + "tapadIdSystem", + "novatiqIdSystem", + "uid2IdSystem", + "admixerIdSystem", + "dmdIdSystem", + "flocIdSystem" ], "adpod": [ "freeWheelAdserverVideo", "dfpAdServerVideo" ], "rtdModule": [ - "browsiRtdProvider" + "browsiRtdProvider", + "geoedgeRtdProvider", + "haloRtdProvider", + "jwplayerRtdProvider", + "permutiveRtdProvider", + "reconciliationRtdProvider", + "sirdataRtdProvider" + ], + "fpdModule": [ + "enrichmentFpdModule", + "validationFpdModule" ] } diff --git a/modules/33acrossBidAdapter.js b/modules/33acrossBidAdapter.js index 798b6450946..4b8028d97fd 100644 --- a/modules/33acrossBidAdapter.js +++ b/modules/33acrossBidAdapter.js @@ -1,12 +1,37 @@ import { registerBidder } from '../src/adapters/bidderFactory.js'; import { config } from '../src/config.js'; import * as utils from '../src/utils.js'; +import {BANNER, VIDEO} from '../src/mediaTypes.js'; const BIDDER_CODE = '33across'; const END_POINT = 'https://ssc.33across.com/api/v1/hb'; const SYNC_ENDPOINT = 'https://ssc-cms.33across.com/ps/?m=xch&rt=html&ru=deb'; -const MEDIA_TYPE = 'banner'; + const CURRENCY = 'USD'; +const GUID_PATTERN = /^[a-zA-Z0-9_-]{22}$/; + +const PRODUCT = { + SIAB: 'siab', + INVIEW: 'inview', + INSTREAM: 'instream' +}; + +const VIDEO_ORTB_PARAMS = [ + 'mimes', + 'minduration', + 'maxduration', + 'placement', + 'protocols', + 'startdelay', + 'skip', + 'skipafter', + 'minbitrate', + 'maxbitrate', + 'delivery', + 'playbackmethod', + 'api', + 'linearity' +]; const adapterState = { uniqueSiteIds: [] @@ -14,57 +39,122 @@ const adapterState = { const NON_MEASURABLE = 'nm'; -// All this assumes that only one bid is ever returned by ttx -function _createBidResponse(response) { - return { - requestId: response.id, - bidderCode: BIDDER_CODE, - cpm: response.seatbid[0].bid[0].price, - width: response.seatbid[0].bid[0].w, - height: response.seatbid[0].bid[0].h, - ad: response.seatbid[0].bid[0].adm, - ttl: response.seatbid[0].bid[0].ttl || 60, - creativeId: response.seatbid[0].bid[0].crid, - currency: response.cur, - netRevenue: true +// **************************** VALIDATION *************************** // +function isBidRequestValid(bid) { + return ( + _validateBasic(bid) && + _validateBanner(bid) && + _validateVideo(bid) + ); +} + +function _validateBasic(bid) { + if (bid.bidder !== BIDDER_CODE || typeof bid.params === 'undefined') { + return false; + } + + if (!_validateGUID(bid)) { + return false; } + + return true; } -function _isViewabilityMeasurable(element) { - return !_isIframe() && element !== null; +function _validateGUID(bid) { + const siteID = utils.deepAccess(bid, 'params.siteId', '') || ''; + if (siteID.trim().match(GUID_PATTERN) === null) { + return false; + } + + return true; } -function _getViewability(element, topWin, { w, h } = {}) { - return topWin.document.visibilityState === 'visible' - ? _getPercentInView(element, topWin, { w, h }) - : 0; +function _validateBanner(bid) { + const banner = utils.deepAccess(bid, 'mediaTypes.banner'); + // If there's no banner no need to validate against banner rules + if (banner === undefined) { + return true; + } + + if (!Array.isArray(banner.sizes)) { + return false; + } + + return true; } -function _mapAdUnitPathToElementId(adUnitCode) { - if (utils.isGptPubadsDefined()) { - // eslint-disable-next-line no-undef - const adSlots = googletag.pubads().getSlots(); - const isMatchingAdSlot = utils.isSlotMatchingAdUnitCode(adUnitCode); +function _validateVideo(bid) { + const videoAdUnit = utils.deepAccess(bid, 'mediaTypes.video'); + const videoBidderParams = utils.deepAccess(bid, 'params.video', {}); - for (let i = 0; i < adSlots.length; i++) { - if (isMatchingAdSlot(adSlots[i])) { - const id = adSlots[i].getSlotElementId(); + // If there's no video no need to validate against video rules + if (videoAdUnit === undefined) { + return true; + } - utils.logInfo(`[33Across Adapter] Map ad unit path to HTML element id: '${adUnitCode}' -> ${id}`); + if (!Array.isArray(videoAdUnit.playerSize)) { + return false; + } - return id; - } - } + if (!videoAdUnit.context) { + return false; } - utils.logWarn(`[33Across Adapter] Unable to locate element for ad unit code: '${adUnitCode}'`); + const videoParams = { + ...videoAdUnit, + ...videoBidderParams + }; - return null; + if (!Array.isArray(videoParams.mimes) || videoParams.mimes.length === 0) { + return false; + } + + if (!Array.isArray(videoParams.protocols) || videoParams.protocols.length === 0) { + return false; + } + + // If placement if defined, it must be a number + if ( + typeof videoParams.placement !== 'undefined' && + typeof videoParams.placement !== 'number' + ) { + return false; + } + + // If startdelay is defined it must be a number + if ( + videoAdUnit.context === 'instream' && + typeof videoParams.startdelay !== 'undefined' && + typeof videoParams.startdelay !== 'number' + ) { + return false; + } + + return true; } -function _getAdSlotHTMLElement(adUnitCode) { - return document.getElementById(adUnitCode) || - document.getElementById(_mapAdUnitPathToElementId(adUnitCode)); +// **************************** BUILD REQUESTS *************************** // +// NOTE: With regards to gdrp consent data, the server will independently +// infer the gdpr applicability therefore, setting the default value to false +function buildRequests(bidRequests, bidderRequest) { + const gdprConsent = Object.assign({ + consentString: undefined, + gdprApplies: false + }, bidderRequest && bidderRequest.gdprConsent); + + const uspConsent = bidderRequest && bidderRequest.uspConsent; + const pageUrl = (bidderRequest && bidderRequest.refererInfo) ? (bidderRequest.refererInfo.referer) : (undefined); + + adapterState.uniqueSiteIds = bidRequests.map(req => req.params.siteId).filter(utils.uniques); + + return bidRequests.map(bidRequest => _createServerRequest( + { + bidRequest, + gdprConsent, + uspConsent, + pageUrl + }) + ); } // Infer the necessary data from valid bid for a minimal ttxRequest and create HTTP request @@ -72,46 +162,28 @@ function _getAdSlotHTMLElement(adUnitCode) { function _createServerRequest({bidRequest, gdprConsent = {}, uspConsent, pageUrl}) { const ttxRequest = {}; const params = bidRequest.params; - const element = _getAdSlotHTMLElement(bidRequest.adUnitCode); - const sizes = _transformSizes(bidRequest.sizes); - let format; - - // We support size based bidfloors so obtain one if there's a rule associated - if (typeof bidRequest.getFloor === 'function') { - let getFloor = bidRequest.getFloor.bind(bidRequest); - - format = sizes.map((size) => { - const formatExt = _getBidFloors(getFloor, size); + /* + * Infer data for the request payload + */ + ttxRequest.imp = [{}]; - return Object.assign({}, size, formatExt); - }); - } else { - format = sizes; + if (utils.deepAccess(bidRequest, 'mediaTypes.banner')) { + ttxRequest.imp[0].banner = { + ..._buildBannerORTB(bidRequest) + } } - const minSize = _getMinSize(sizes); - - const viewabilityAmount = _isViewabilityMeasurable(element) - ? _getViewability(element, utils.getWindowTop(), minSize) - : NON_MEASURABLE; - - const contributeViewability = ViewabilityContributor(viewabilityAmount); + if (utils.deepAccess(bidRequest, 'mediaTypes.video')) { + ttxRequest.imp[0].video = _buildVideoORTB(bidRequest); + } - /* - * Infer data for the request payload - */ - ttxRequest.imp = []; - ttxRequest.imp[0] = { - banner: { - format - }, - ext: { - ttx: { - prod: params.productId - } + ttxRequest.imp[0].ext = { + ttx: { + prod: _getProduct(bidRequest) } }; + ttxRequest.site = { id: params.siteId }; if (pageUrl) { @@ -122,18 +194,36 @@ function _createServerRequest({bidRequest, gdprConsent = {}, uspConsent, pageUrl // therefore in ad targetting process ttxRequest.id = bidRequest.bidId; - // Set GDPR related fields - ttxRequest.user = { - ext: { - consent: gdprConsent.consentString - } - }; - ttxRequest.regs = { - ext: { - gdpr: (gdprConsent.gdprApplies === true) ? 1 : 0, - us_privacy: uspConsent || null - } - }; + if (gdprConsent.consentString) { + ttxRequest.user = setExtension( + ttxRequest.user, + 'consent', + gdprConsent.consentString + ) + } + + if (Array.isArray(bidRequest.userIdAsEids) && bidRequest.userIdAsEids.length > 0) { + ttxRequest.user = setExtension( + ttxRequest.user, + 'eids', + bidRequest.userIdAsEids + ) + } + + ttxRequest.regs = setExtension( + ttxRequest.regs, + 'gdpr', + Number(gdprConsent.gdprApplies) + ); + + if (uspConsent) { + ttxRequest.regs = setExtension( + ttxRequest.regs, + 'us_privacy', + uspConsent + ) + } + ttxRequest.ext = { ttx: { prebidStartedAt: Date.now(), @@ -145,11 +235,11 @@ function _createServerRequest({bidRequest, gdprConsent = {}, uspConsent, pageUrl }; if (bidRequest.schain) { - ttxRequest.source = { - ext: { - schain: bidRequest.schain - } - } + ttxRequest.source = setExtension( + ttxRequest.source, + 'schain', + bidRequest.schain + ) } // Finally, set the openRTB 'test' param if this is to be a test bid @@ -173,53 +263,196 @@ function _createServerRequest({bidRequest, gdprConsent = {}, uspConsent, pageUrl return { 'method': 'POST', 'url': url, - 'data': JSON.stringify(contributeViewability(ttxRequest)), + 'data': JSON.stringify(ttxRequest), 'options': options } } -// Sync object will always be of type iframe for TTX -function _createSync({ siteId = 'zzz000000000003zzz', gdprConsent = {}, uspConsent }) { - const ttxSettings = config.getConfig('ttxSettings'); - const syncUrl = (ttxSettings && ttxSettings.syncUrl) || SYNC_ENDPOINT; +// BUILD REQUESTS: SET EXTENSIONS +function setExtension(obj = {}, key, value) { + return Object.assign({}, obj, { + ext: Object.assign({}, obj.ext, { + [key]: value + }) + }); +} - const { consentString, gdprApplies } = gdprConsent; +// BUILD REQUESTS: SIZE INFERENCE +function _transformSizes(sizes) { + if (utils.isArray(sizes) && sizes.length === 2 && !utils.isArray(sizes[0])) { + return [ _getSize(sizes) ]; + } - const sync = { - type: 'iframe', - url: `${syncUrl}&id=${siteId}&gdpr_consent=${encodeURIComponent(consentString)}&us_privacy=${encodeURIComponent(uspConsent)}` - }; + return sizes.map(_getSize); +} - if (typeof gdprApplies === 'boolean') { - sync.url += `&gdpr=${Number(gdprApplies)}`; +function _getSize(size) { + return { + w: parseInt(size[0], 10), + h: parseInt(size[1], 10) } +} - return sync; +// BUILD REQUESTS: PRODUCT INFERENCE +function _getProduct(bidRequest) { + const { params, mediaTypes } = bidRequest; + + const { banner, video } = mediaTypes; + + if ((video && !banner) && video.context === 'instream') { + return PRODUCT.INSTREAM; + } + + return (params.productId === PRODUCT.INVIEW) ? (params.productId) : PRODUCT.SIAB; } -function _getBidFloors(getFloor, size) { - const bidFloors = getFloor({ +// BUILD REQUESTS: BANNER +function _buildBannerORTB(bidRequest) { + const bannerAdUnit = utils.deepAccess(bidRequest, 'mediaTypes.banner', {}); + const element = _getAdSlotHTMLElement(bidRequest.adUnitCode); + + const sizes = _transformSizes(bannerAdUnit.sizes); + + let format; + + // We support size based bidfloors so obtain one if there's a rule associated + if (typeof bidRequest.getFloor === 'function') { + format = sizes.map((size) => { + const bidfloors = _getBidFloors(bidRequest, size, BANNER); + + let formatExt; + if (bidfloors) { + formatExt = { + ext: { + ttx: { + bidfloors: [ bidfloors ] + } + } + } + } + + return Object.assign({}, size, formatExt); + }); + } else { + format = sizes; + } + + const minSize = _getMinSize(sizes); + + const viewabilityAmount = _isViewabilityMeasurable(element) + ? _getViewability(element, utils.getWindowTop(), minSize) + : NON_MEASURABLE; + + const ext = contributeViewability(viewabilityAmount); + + return { + format, + ext + } +} + +// BUILD REQUESTS: VIDEO +// eslint-disable-next-line no-unused-vars +function _buildVideoORTB(bidRequest) { + const videoAdUnit = utils.deepAccess(bidRequest, 'mediaTypes.video', {}); + const videoBidderParams = utils.deepAccess(bidRequest, 'params.video', {}); + + const videoParams = { + ...videoAdUnit, + ...videoBidderParams // Bidder Specific overrides + }; + + const video = {} + + const {w, h} = _getSize(videoParams.playerSize[0]); + video.w = w; + video.h = h; + + // Obtain all ORTB params related video from Ad Unit + VIDEO_ORTB_PARAMS.forEach((param) => { + if (videoParams.hasOwnProperty(param)) { + video[param] = videoParams[param]; + } + }); + + const product = _getProduct(bidRequest); + + // Placement Inference Rules: + // - If no placement is defined then default to 2 (In Banner) + // - If product is instream (for instream context) then override placement to 1 + video.placement = video.placement || 2; + + if (product === PRODUCT.INSTREAM) { + video.startdelay = video.startdelay || 0; + video.placement = 1; + }; + + // bidfloors + if (typeof bidRequest.getFloor === 'function') { + const bidfloors = _getBidFloors(bidRequest, {w: video.w, h: video.h}, VIDEO); + + if (bidfloors) { + Object.assign(video, { + ext: { + ttx: { + bidfloors: [ bidfloors ] + } + } + }); + } + } + return video; +} + +// BUILD REQUESTS: BIDFLOORS +function _getBidFloors(bidRequest, size, mediaType) { + const bidFloors = bidRequest.getFloor({ currency: CURRENCY, - mediaType: MEDIA_TYPE, + mediaType, size: [ size.w, size.h ] }); if (!isNaN(bidFloors.floor) && (bidFloors.currency === CURRENCY)) { - return { - ext: { - ttx: { - bidfloors: [ bidFloors.floor ] - } + return bidFloors.floor; + } +} + +// BUILD REQUESTS: VIEWABILITY +function _isViewabilityMeasurable(element) { + return !_isIframe() && element !== null; +} + +function _getViewability(element, topWin, { w, h } = {}) { + return topWin.document.visibilityState === 'visible' + ? _getPercentInView(element, topWin, { w, h }) + : 0; +} + +function _mapAdUnitPathToElementId(adUnitCode) { + if (utils.isGptPubadsDefined()) { + // eslint-disable-next-line no-undef + const adSlots = googletag.pubads().getSlots(); + const isMatchingAdSlot = utils.isSlotMatchingAdUnitCode(adUnitCode); + + for (let i = 0; i < adSlots.length; i++) { + if (isMatchingAdSlot(adSlots[i])) { + const id = adSlots[i].getSlotElementId(); + + utils.logInfo(`[33Across Adapter] Map ad unit path to HTML element id: '${adUnitCode}' -> ${id}`); + + return id; } } } + + utils.logWarn(`[33Across Adapter] Unable to locate element for ad unit code: '${adUnitCode}'`); + + return null; } -function _getSize(size) { - return { - w: parseInt(size[0], 10), - h: parseInt(size[1], 10) - } +function _getAdSlotHTMLElement(adUnitCode) { + return document.getElementById(adUnitCode) || + document.getElementById(_mapAdUnitPathToElementId(adUnitCode)); } function _getMinSize(sizes) { @@ -239,14 +472,6 @@ function _getBoundingBox(element, { w, h } = {}) { return { width, height, left, top, right, bottom }; } -function _transformSizes(sizes) { - if (utils.isArray(sizes) && sizes.length === 2 && !utils.isArray(sizes[0])) { - return [ _getSize(sizes) ]; - } - - return sizes.map(_getSize); -} - function _getIntersectionOfRects(rects) { const bbox = { left: rects[0].left, @@ -307,20 +532,16 @@ function _getPercentInView(element, topWin, { w, h } = {}) { /** * Viewability contribution to request.. */ -function ViewabilityContributor(viewabilityAmount) { - function contributeViewability(ttxRequest) { - const req = Object.assign({}, ttxRequest); - const imp = req.imp = req.imp.map(impItem => Object.assign({}, impItem)); - const banner = imp[0].banner = Object.assign({}, imp[0].banner); - const ext = banner.ext = Object.assign({}, banner.ext); - const ttx = ext.ttx = Object.assign({}, ext.ttx); +function contributeViewability(viewabilityAmount) { + const amount = isNaN(viewabilityAmount) ? viewabilityAmount : Math.round(viewabilityAmount); - ttx.viewability = { amount: isNaN(viewabilityAmount) ? viewabilityAmount : Math.round(viewabilityAmount) }; - - return req; - } - - return contributeViewability; + return { + ttx: { + viewability: { + amount + } + } + }; } function _isIframe() { @@ -331,53 +552,58 @@ function _isIframe() { } } -function isBidRequestValid(bid) { - if (bid.bidder !== BIDDER_CODE || typeof bid.params === 'undefined') { - return false; - } +// **************************** INTERPRET RESPONSE ******************************** // +// NOTE: At this point, the response from 33exchange will only ever contain one bid +// i.e. the highest bid +function interpretResponse(serverResponse, bidRequest) { + const bidResponses = []; - if (typeof bid.params.siteId === 'undefined' || typeof bid.params.productId === 'undefined') { - return false; + // If there are bids, look at the first bid of the first seatbid (see NOTE above for assumption about ttx) + if (serverResponse.body.seatbid.length > 0 && serverResponse.body.seatbid[0].bid.length > 0) { + bidResponses.push(_createBidResponse(serverResponse.body)); } - return true; + return bidResponses; } -// NOTE: With regards to gdrp consent data, -// - the server independently infers gdpr applicability therefore, setting the default value to false -function buildRequests(bidRequests, bidderRequest) { - const gdprConsent = Object.assign({ - consentString: undefined, - gdprApplies: false - }, bidderRequest && bidderRequest.gdprConsent); - - const uspConsent = bidderRequest && bidderRequest.uspConsent; - const pageUrl = (bidderRequest && bidderRequest.refererInfo) ? (bidderRequest.refererInfo.referer) : (undefined); - - adapterState.uniqueSiteIds = bidRequests.map(req => req.params.siteId).filter(utils.uniques); +// All this assumes that only one bid is ever returned by ttx +function _createBidResponse(response) { + const isADomainPresent = + response.seatbid[0].bid[0].adomain && response.seatbid[0].bid[0].adomain.length; + const bid = { + requestId: response.id, + bidderCode: BIDDER_CODE, + cpm: response.seatbid[0].bid[0].price, + width: response.seatbid[0].bid[0].w, + height: response.seatbid[0].bid[0].h, + ad: response.seatbid[0].bid[0].adm, + ttl: response.seatbid[0].bid[0].ttl || 60, + creativeId: response.seatbid[0].bid[0].crid, + mediaType: utils.deepAccess(response.seatbid[0].bid[0], 'ext.ttx.mediaType', BANNER), + currency: response.cur, + netRevenue: true + } - return bidRequests.map(bidRequest => _createServerRequest( - { - bidRequest, - gdprConsent, - uspConsent, - pageUrl - }) - ); -} + if (isADomainPresent) { + bid.meta = { + advertiserDomains: response.seatbid[0].bid[0].adomain + }; + } -// NOTE: At this point, the response from 33exchange will only ever contain one bid i.e. the highest bid -function interpretResponse(serverResponse, bidRequest) { - const bidResponses = []; + if (bid.mediaType === VIDEO) { + const vastType = utils.deepAccess(response.seatbid[0].bid[0], 'ext.ttx.vastType', 'xml'); - // If there are bids, look at the first bid of the first seatbid (see NOTE above for assumption about ttx) - if (serverResponse.body.seatbid.length > 0 && serverResponse.body.seatbid[0].bid.length > 0) { - bidResponses.push(_createBidResponse(serverResponse.body)); + if (vastType === 'xml') { + bid.vastXml = bid.ad; + } else { + bid.vastUrl = bid.ad; + } } - return bidResponses; + return bid; } +// **************************** USER SYNC *************************** // // Register one sync per unique guid so long as iframe is enable // Else no syncs // For logic on how we handle gdpr data see _createSyncs and module's unit tests @@ -395,11 +621,30 @@ function getUserSyncs(syncOptions, responses, gdprConsent, uspConsent) { return syncUrls; } +// Sync object will always be of type iframe for TTX +function _createSync({ siteId = 'zzz000000000003zzz', gdprConsent = {}, uspConsent }) { + const ttxSettings = config.getConfig('ttxSettings'); + const syncUrl = (ttxSettings && ttxSettings.syncUrl) || SYNC_ENDPOINT; + + const { consentString, gdprApplies } = gdprConsent; + + const sync = { + type: 'iframe', + url: `${syncUrl}&id=${siteId}&gdpr_consent=${encodeURIComponent(consentString)}&us_privacy=${encodeURIComponent(uspConsent)}` + }; + + if (typeof gdprApplies === 'boolean') { + sync.url += `&gdpr=${Number(gdprApplies)}`; + } + + return sync; +} + export const spec = { NON_MEASURABLE, code: BIDDER_CODE, - + supportedMediaTypes: [ BANNER, VIDEO ], isBidRequestValid, buildRequests, interpretResponse, diff --git a/modules/33acrossBidAdapter.md b/modules/33acrossBidAdapter.md index c313f3b6e0b..c01c04251e5 100644 --- a/modules/33acrossBidAdapter.md +++ b/modules/33acrossBidAdapter.md @@ -10,23 +10,145 @@ Maintainer: headerbidding@33across.com Connects to 33Across's exchange for bids. -33Across bid adapter supports only Banner at present and follows MRA +33Across bid adapter supports Banner and Video at present and follows MRA # Sample Ad Unit: For Publishers +## Sample Banner only Ad Unit ``` var adUnits = [ { - code: '33across-hb-ad-123456-1', // ad slot HTML element ID - sizes: [ - [300, 250], - [728, 90] - ], - bids: [{ - bidder: '33across', - params: { - siteId: 'cxBE0qjUir6iopaKkGJozW', - productId: 'siab' + code: '33across-hb-ad-123456-1', // ad slot HTML element ID + mediaTypes: { + banner: { + sizes: [ + [300, 250], + [728, 90] + ] + } + } + bids: [{ + bidder: '33across', + params: { + siteId: 'sample33xGUID123456789', + productId: 'siab' + } + }] +} +``` + +## Sample Video only Ad Unit: Outstream +``` +var adUnits = [ +{ + code: '33across-hb-ad-123456-1', // ad slot HTML element ID + mediaTypes: { + video: { + playerSize: [300, 250], + context: 'outstream', + placement: 2 + ... // Aditional ORTB video params + } + }, + renderer: { + url: 'https://acdn.adnxs.com/video/outstream/ANOutstreamVideo.js', + render: function (bid) { + adResponse = { + ad: { + video: { + content: bid.vastXml, + player_height: bid.playerHeight, + player_width: bid.playerWidth + } + } } - }] + // push to render queue because ANOutstreamVideo may not be loaded yet. + bid.renderer.push(() => { + ANOutstreamVideo.renderAd({ + targetId: bid.adUnitCode, // target div id to render video. + adResponse: adResponse + }); + }); + } + }, + bids: [{ + bidder: '33across', + params: { + siteId: 'sample33xGUID123456789', + productId: 'siab' + } + }] +} +``` + +## Sample Multi-Format Ad Unit: Outstream +``` +var adUnits = [ +{ + code: '33across-hb-ad-123456-1', // ad slot HTML element ID + mediaTypes: { + banner: { + sizes: [ + [300, 250], + [728, 90] + ] + }, + video: { + playerSize: [300, 250], + context: 'outstream', + placement: 2 + ... // Aditional ORTB video params + } + }, + renderer: { + url: 'https://acdn.adnxs.com/video/outstream/ANOutstreamVideo.js', + render: function (bid) { + adResponse = { + ad: { + video: { + content: bid.vastXml, + player_height: bid.playerHeight, + player_width: bid.playerWidth + } + } + } + // push to render queue because ANOutstreamVideo may not be loaded yet. + bid.renderer.push(() => { + ANOutstreamVideo.renderAd({ + targetId: bid.adUnitCode, // target div id to render video. + adResponse: adResponse + }); + }); + } + }, + bids: [{ + bidder: '33across', + params: { + siteId: 'sample33xGUID123456789', + productId: 'siab' + } + }] +} +``` + +## Sample Video only Ad Unit: Instream +``` +var adUnits = [ +{ + code: '33across-hb-ad-123456-1', // ad slot HTML element ID + mediaTypes: { + video: { + playerSize: [300, 250], + context: 'intstream', + placement: 1 + ... // Aditional ORTB video params + } + } + bids: [{ + bidder: '33across', + params: { + siteId: 'sample33xGUID123456789', + productId: 'instream' + } + }] } ``` diff --git a/modules/a4gBidAdapter.js b/modules/a4gBidAdapter.js new file mode 100644 index 00000000000..1961dba1f10 --- /dev/null +++ b/modules/a4gBidAdapter.js @@ -0,0 +1,89 @@ +import {registerBidder} from '../src/adapters/bidderFactory.js'; +import * as utils from '../src/utils.js'; + +const A4G_BIDDER_CODE = 'a4g'; +const A4G_CURRENCY = 'USD'; +const A4G_DEFAULT_BID_URL = 'https://ads.ad4game.com/v1/bid'; +const A4G_TTL = 120; + +const LOCATION_PARAM_NAME = 'siteurl'; +const ID_PARAM_NAME = 'id'; +const IFRAME_PARAM_NAME = 'if'; +const ZONE_ID_PARAM_NAME = 'zoneId'; +const SIZE_PARAM_NAME = 'size'; + +const ARRAY_PARAM_SEPARATOR = ';'; +const ARRAY_SIZE_SEPARATOR = ','; +const SIZE_SEPARATOR = 'x'; + +export const spec = { + code: A4G_BIDDER_CODE, + isBidRequestValid: function(bid) { + return bid.params && !!bid.params.zoneId; + }, + + buildRequests: function(validBidRequests, bidderRequest) { + let deliveryUrl = ''; + const idParams = []; + const sizeParams = []; + const zoneIds = []; + + utils._each(validBidRequests, function(bid) { + if (!deliveryUrl && typeof bid.params.deliveryUrl === 'string') { + deliveryUrl = bid.params.deliveryUrl; + } + idParams.push(bid.bidId); + let bidSizes = (bid.mediaTypes && bid.mediaTypes.banner && bid.mediaTypes.banner.sizes) || bid.sizes; + sizeParams.push(bidSizes.map(size => size.join(SIZE_SEPARATOR)).join(ARRAY_SIZE_SEPARATOR)); + zoneIds.push(bid.params.zoneId); + }); + + if (!deliveryUrl) { + deliveryUrl = A4G_DEFAULT_BID_URL; + } + + let data = { + [IFRAME_PARAM_NAME]: 0, + [LOCATION_PARAM_NAME]: (bidderRequest.refererInfo && bidderRequest.refererInfo.referer) ? bidderRequest.refererInfo.referer : window.location.href, + [SIZE_PARAM_NAME]: sizeParams.join(ARRAY_PARAM_SEPARATOR), + [ID_PARAM_NAME]: idParams.join(ARRAY_PARAM_SEPARATOR), + [ZONE_ID_PARAM_NAME]: zoneIds.join(ARRAY_PARAM_SEPARATOR) + }; + + if (bidderRequest && bidderRequest.gdprConsent) { + data.gdpr = { + applies: bidderRequest.gdprConsent.gdprApplies, + consent: bidderRequest.gdprConsent.consentString + }; + } + + return { + method: 'GET', + url: deliveryUrl, + data: data + }; + }, + + interpretResponse: function(serverResponses, request) { + const bidResponses = []; + utils._each(serverResponses.body, function(response) { + if (response.cpm > 0) { + const bidResponse = { + requestId: response.id, + creativeId: response.crid || response.id, + cpm: response.cpm, + width: response.width, + height: response.height, + currency: A4G_CURRENCY, + netRevenue: true, + ttl: A4G_TTL, + ad: response.ad + }; + bidResponses.push(bidResponse); + } + }); + return bidResponses; + } +}; + +registerBidder(spec); diff --git a/modules/a4gBidAdapter.md b/modules/a4gBidAdapter.md index dcab312ed29..70f110724b0 100644 --- a/modules/a4gBidAdapter.md +++ b/modules/a4gBidAdapter.md @@ -6,32 +6,40 @@ Maintainer: devops@ad4game.com # Description -Ad4Game Bidder Adapter for Prebid.js. It should be tested on real domain. `localhost` should be rewritten (ex. example.com). +Ad4Game Bidder Adapter for Prebid.js. It should be tested on real domain. `localhost` should be rewritten (ex. example.com). # Test Parameters ``` var adUnits = [ { code: 'test-div', - sizes: [[300, 250]], // a display size + mediaTypes: { + banner: { + sizes: [[300, 250]], // a display size + } + }, bids: [ { bidder: 'a4g', params: { zoneId: 59304, - deliveryUrl: 'http://dev01.ad4game.com/v1/bid' + deliveryUrl: 'https://dev01.ad4game.com/v1/bid' } } ] },{ code: 'test-div', - sizes: [[300, 50]], // a mobile size + mediaTypes: { + banner: { + sizes: [[300, 50]], // a mobile size + } + }, bids: [ { bidder: 'a4g', params: { zoneId: 59354, - deliveryUrl: 'http://dev01.ad4game.com/v1/bid' + deliveryUrl: 'https://dev01.ad4game.com/v1/bid' } } ] diff --git a/modules/ablidaBidAdapter.js b/modules/ablidaBidAdapter.js index 9bd22ef1f0d..2400952367f 100644 --- a/modules/ablidaBidAdapter.js +++ b/modules/ablidaBidAdapter.js @@ -1,13 +1,14 @@ import * as utils from '../src/utils.js'; import {config} from '../src/config.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; -import {ajax} from '../src/ajax.js'; +import { BANNER, NATIVE, VIDEO } from '../src/mediaTypes.js'; const BIDDER_CODE = 'ablida'; const ENDPOINT_URL = 'https://bidder.ablida.net/prebid'; export const spec = { code: BIDDER_CODE, + supportedMediaTypes: [BANNER, NATIVE, VIDEO], /** * Determines whether or not the given bid request is valid. @@ -31,20 +32,25 @@ export const spec = { return []; } return validBidRequests.map(bidRequest => { - const sizes = utils.parseSizesInput(bidRequest.sizes)[0]; - const size = sizes.split('x'); + let sizes = [] + if (bidRequest.mediaTypes && bidRequest.mediaTypes[BANNER] && bidRequest.mediaTypes[BANNER].sizes) { + sizes = bidRequest.mediaTypes[BANNER].sizes; + } else if (bidRequest.mediaTypes[VIDEO] && bidRequest.mediaTypes[VIDEO].playerSize) { + sizes = bidRequest.mediaTypes[VIDEO].playerSize + } const jaySupported = 'atob' in window && 'currentScript' in document; const device = getDevice(); const payload = { placementId: bidRequest.params.placementId, - width: size[0], - height: size[1], + sizes: sizes, bidId: bidRequest.bidId, categories: bidRequest.params.categories, referer: bidderRequest.refererInfo.referer, jaySupported: jaySupported, device: device, - adapterVersion: 2 + adapterVersion: 5, + mediaTypes: bidRequest.mediaTypes, + gdprConsent: bidderRequest.gdprConsent }; return { method: 'POST', @@ -72,9 +78,8 @@ export const spec = { return bidResponses; }, onBidWon: function (bid) { - if (!bid['nurl']) { return false; } - ajax(bid['nurl'], null); - return true; + if (!bid['nurl']) { return; } + utils.triggerPixel(bid['nurl']); } }; diff --git a/modules/ablidaBidAdapter.md b/modules/ablidaBidAdapter.md index 70e6576cd30..e0a9f3f9405 100644 --- a/modules/ablidaBidAdapter.md +++ b/modules/ablidaBidAdapter.md @@ -27,6 +27,46 @@ Module that connects to Ablida's bidder for bids. } } ] + }, { + code: 'native-ad-div', + mediaTypes: { + native: { + image: { + sendId: true, + required: true + }, + title: { + required: true + }, + body: { + required: true + } + } + }, + bids: [ + { + bidder: 'ablida', + params: { + placementId: 'native-demo' + } + } + ] + }, { + code: 'video-ad', + mediaTypes: { + video: { + playerSize: [[640, 360]], + context: 'instream' + } + }, + bids: [ + { + bidder: 'ablida', + params: { + placementId: 'instream-demo' + } + } + ] } - ]; + ]; ``` diff --git a/modules/adWMGBidAdapter.js b/modules/adWMGBidAdapter.js new file mode 100644 index 00000000000..689e7d02124 --- /dev/null +++ b/modules/adWMGBidAdapter.js @@ -0,0 +1,315 @@ +'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://hb.adwmg.com/hb'; +let SYNC_ENDPOINT = 'https://hb.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 checkFloorValue = (value) => { + if (isNaN(parseFloat(value))) { + return 0; + } else return parseFloat(value); + } + + const adUnit = { + code: bidRequest.adUnitCode, + bids: { + bidder: bidRequest.bidder, + params: { + publisherId: bidRequest.params.publisherId, + IABCategories: bidRequest.params.IABCategories || [], + floorCPM: bidRequest.params.floorCPM ? checkFloorValue(bidRequest.params.floorCPM) : 0 + } + }, + 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 b4c2a6ac0d6..66653567dab 100644 --- a/modules/adagioBidAdapter.js +++ b/modules/adagioBidAdapter.js @@ -1,23 +1,29 @@ import find from 'core-js-pure/features/array/find.js'; import * as utils from '../src/utils.js'; +import { config } from '../src/config.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; -import { loadExternalScript } from '../src/adloader.js' +import { loadExternalScript } from '../src/adloader.js'; import JSEncrypt from 'jsencrypt/bin/jsencrypt.js'; import sha256 from 'crypto-js/sha256.js'; import { getStorageManager } from '../src/storageManager.js'; import { getRefererInfo } from '../src/refererDetection.js'; +import { createEidsArray } from './userId/eids.js'; +import { BANNER, NATIVE, VIDEO } from '../src/mediaTypes.js'; +import { Renderer } from '../src/Renderer.js'; +import { OUTSTREAM } from '../src/video.js'; export const BIDDER_CODE = 'adagio'; export const LOG_PREFIX = 'Adagio:'; -export const VERSION = '2.3.0'; +export const VERSION = '2.10.0'; export const FEATURES_VERSION = '1'; export const ENDPOINT = 'https://mp.4dex.io/prebid'; -export const SUPPORTED_MEDIA_TYPES = ['banner']; +export const SUPPORTED_MEDIA_TYPES = [BANNER, NATIVE, VIDEO]; export const ADAGIO_TAG_URL = 'https://script.4dex.io/localstore.js'; export const ADAGIO_LOCALSTORAGE_KEY = 'adagioScript'; export const GVLID = 617; export const storage = getStorageManager(GVLID, 'adagio'); - +export const RENDERER_URL = 'https://script.4dex.io/outstream-player.js'; +export const MAX_SESS_DURATION = 30 * 60 * 1000; export const ADAGIO_PUBKEY = `-----BEGIN PUBLIC KEY----- MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC9el0+OEn6fvEh1RdVHQu4cnT0 jFSzIbGJJyg3cKqvtE6A0iaz9PkIdJIvSSSNrmJv+lRGKPEyRA/VnzJIieL39Ngl @@ -25,8 +31,39 @@ t0b0lsHN+W4n9kitS/DZ/xnxWK/9vxhv0ZtL1LL/rwR5Mup7rmJbNtDoNBw4TIGj pV6EP3MTLosuUEpLaQIDAQAB -----END PUBLIC KEY-----`; +// This provide a whitelist and a basic validation +// of OpenRTB 2.5 options used by the Adagio SSP. +// https://www.iab.com/wp-content/uploads/2016/03/OpenRTB-API-Specification-Version-2-5-FINAL.pdf +export const ORTB_VIDEO_PARAMS = { + 'mimes': (value) => Array.isArray(value) && value.length > 0 && value.every(v => typeof v === 'string'), + 'minduration': (value) => utils.isInteger(value), + 'maxduration': (value) => utils.isInteger(value), + 'protocols': (value) => Array.isArray(value) && value.every(v => [1, 2, 3, 4, 5, 6, 7, 8, 9, 10].indexOf(v) !== -1), + 'w': (value) => utils.isInteger(value), + 'h': (value) => utils.isInteger(value), + 'startdelay': (value) => utils.isInteger(value), + 'placement': (value) => Array.isArray(value) && value.every(v => [1, 2, 3, 4, 5].indexOf(v) !== -1), + 'linearity': (value) => [1, 2].indexOf(value) !== -1, + 'skip': (value) => [0, 1].indexOf(value) !== -1, + 'skipmin': (value) => utils.isInteger(value), + 'skipafter': (value) => utils.isInteger(value), + 'sequence': (value) => utils.isInteger(value), + 'battr': (value) => Array.isArray(value) && value.every(v => Array.from({length: 17}, (_, i) => i + 1).indexOf(v) !== -1), + 'maxextended': (value) => utils.isInteger(value), + 'minbitrate': (value) => utils.isInteger(value), + 'maxbitrate': (value) => utils.isInteger(value), + 'boxingallowed': (value) => [0, 1].indexOf(value) !== -1, + 'playbackmethod': (value) => Array.isArray(value) && value.every(v => [1, 2, 3, 4, 5, 6].indexOf(v) !== -1), + 'playbackend': (value) => [1, 2, 3].indexOf(value) !== -1, + 'delivery': (value) => [1, 2, 3].indexOf(value) !== -1, + 'pos': (value) => [0, 1, 2, 3, 4, 5, 6, 7].indexOf(value) !== -1, + 'api': (value) => Array.isArray(value) && value.every(v => [1, 2, 3, 4, 5, 6].indexOf(v) !== -1) +}; + let currentWindow; +const EXT_DATA = {} + export function adagioScriptFromLocalStorageCb(ls) { try { if (!ls) { @@ -62,7 +99,7 @@ export function adagioScriptFromLocalStorageCb(ls) { export function getAdagioScript() { storage.getDataFromLocalStorage(ADAGIO_LOCALSTORAGE_KEY, (ls) => { - internal.adagioScriptFromLocalStorageCb(ls) + internal.adagioScriptFromLocalStorageCb(ls); }); storage.localStorageIsEnabled(isValid => { @@ -73,6 +110,9 @@ export function getAdagioScript() { // It's an antipattern regarding the TCF2 enforcement logic // but it's the only way to respect the user choice update. window.localStorage.removeItem(ADAGIO_LOCALSTORAGE_KEY); + // Extra data from external script. + // This key is removed only if localStorage is not accessible. + window.localStorage.removeItem('adagio'); } }); } @@ -96,6 +136,37 @@ function isSafeFrameWindow() { return !!(ws.$sf && ws.$sf.ext); } +// Get localStorage "adagio" data to be passed to the request +export function prepareExchange(storageValue) { + const adagioStorage = JSON.parse(storageValue, function(name, value) { + if (!name.startsWith('_') || name === '') { + return value; + } + }); + let random = utils.deepAccess(adagioStorage, 'session.rnd'); + let newSession = false; + + if (internal.isNewSession(adagioStorage)) { + newSession = true; + random = Math.random(); + } + + const data = { + session: { + new: newSession, + rnd: random + } + } + + utils.mergeDeep(EXT_DATA, adagioStorage, data); + + internal.enqueue({ + action: 'session', + ts: Date.now(), + data: EXT_DATA + }); +} + function initAdagio() { if (canAccessTopWindow()) { currentWindow = (canAccessTopWindow()) ? utils.getWindowTop() : utils.getWindowSelf(); @@ -111,6 +182,14 @@ function initAdagio() { w.ADAGIO.versions.adagioBidderAdapter = VERSION; w.ADAGIO.isSafeFrameWindow = isSafeFrameWindow(); + storage.getDataFromLocalStorage('adagio', (storageData) => { + try { + internal.prepareExchange(storageData); + } catch (e) { + utils.logError(LOG_PREFIX, e); + } + }); + getAdagioScript(); } @@ -333,7 +412,7 @@ function getOrAddAdagioAdUnit(adUnitCode) { w.ADAGIO = w.ADAGIO || {}; if (w.ADAGIO.adUnits[adUnitCode]) { - return w.ADAGIO.adUnits[adUnitCode] + return w.ADAGIO.adUnits[adUnitCode]; } return w.ADAGIO.adUnits[adUnitCode] = {}; @@ -433,7 +512,7 @@ function getElementFromTopWindow(element, currentWindow) { }; function autoDetectAdUnitElementId(adUnitCode) { - const autoDetectedAdUnit = utils.getGptSlotInfoForAdUnitCode(adUnitCode) + const autoDetectedAdUnit = utils.getGptSlotInfoForAdUnitCode(adUnitCode); let adUnitElementId = null; if (autoDetectedAdUnit && autoDetectedAdUnit.divId) { @@ -448,18 +527,24 @@ function autoDetectEnvironment() { let environment; switch (device) { case 2: - environment = 'desktop' + environment = 'desktop'; break; case 4: - environment = 'mobile' + environment = 'mobile'; break; case 5: - environment = 'tablet' + environment = 'tablet'; break; }; - return environment + 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; @@ -505,6 +590,36 @@ function getFeatures(bidRequest, bidderRequest) { return features; }; +function isRendererPreferredFromPublisher(bidRequest) { + // renderer defined at adUnit level + const adUnitRenderer = utils.deepAccess(bidRequest, 'renderer'); + const hasValidAdUnitRenderer = !!(adUnitRenderer && adUnitRenderer.url && adUnitRenderer.render); + + // renderer defined at adUnit.mediaTypes level + const mediaTypeRenderer = utils.deepAccess(bidRequest, 'mediaTypes.video.renderer'); + const hasValidMediaTypeRenderer = !!(mediaTypeRenderer && mediaTypeRenderer.url && mediaTypeRenderer.render); + + return !!( + (hasValidAdUnitRenderer && !(adUnitRenderer.backupOnly === true)) || + (hasValidMediaTypeRenderer && !(mediaTypeRenderer.backupOnly === true)) + ); +} + +/** + * + * @param {object} adagioStorage + * @returns {boolean} + */ +function isNewSession(adagioStorage) { + const now = Date.now(); + const { lastActivityTime, vwSmplg } = utils.deepAccess(adagioStorage, 'session', {}); + return ( + !utils.isNumber(lastActivityTime) || + !utils.isNumber(vwSmplg) || + (now - lastActivityTime) > MAX_SESS_DURATION + ) +} + export const internal = { enqueue, getOrAddAdagioAdUnit, @@ -519,7 +634,11 @@ export const internal = { getRefererInfo, adagioScriptFromLocalStorageCb, getCurrentWindow, - canAccessTopWindow + supportIObs, + canAccessTopWindow, + isRendererPreferredFromPublisher, + isNewSession, + prepareExchange }; function _getGdprConsent(bidderRequest) { @@ -537,7 +656,7 @@ function _getGdprConsent(bidderRequest) { const consent = {}; if (apiVersion !== undefined) { - consent.apiVersion = apiVersion + consent.apiVersion = apiVersion; } if (consentString !== undefined) { @@ -555,12 +674,186 @@ function _getGdprConsent(bidderRequest) { return consent; } +function _getCoppa() { + return { + required: config.getConfig('coppa') === true ? 1 : 0 + }; +} + +function _getUspConsent(bidderRequest) { + return (utils.deepAccess(bidderRequest, 'uspConsent')) ? { uspConsent: bidderRequest.uspConsent } : false; +} + function _getSchain(bidRequest) { if (utils.deepAccess(bidRequest, 'schain')) { return bidRequest.schain; } } +function _getEids(bidRequest) { + if (utils.deepAccess(bidRequest, 'userId')) { + return createEidsArray(bidRequest.userId); + } +} + +function _buildVideoBidRequest(bidRequest) { + const videoAdUnitParams = utils.deepAccess(bidRequest, 'mediaTypes.video', {}); + const videoBidderParams = utils.deepAccess(bidRequest, 'params.video', {}); + const computedParams = {}; + + // Special case for playerSize. + // Eeach props will be overrided if they are defined in config. + if (Array.isArray(videoAdUnitParams.playerSize)) { + const tempSize = (Array.isArray(videoAdUnitParams.playerSize[0])) ? videoAdUnitParams.playerSize[0] : videoAdUnitParams.playerSize; + computedParams.w = tempSize[0]; + computedParams.h = tempSize[1]; + } + + const videoParams = { + ...computedParams, + ...videoAdUnitParams, + ...videoBidderParams + }; + + if (videoParams.context && videoParams.context === OUTSTREAM) { + bidRequest.mediaTypes.video.playerName = (internal.isRendererPreferredFromPublisher(bidRequest)) ? 'other' : 'adagio'; + + if (bidRequest.mediaTypes.video.playerName === 'other') { + utils.logWarn(`${LOG_PREFIX} renderer.backupOnly has not been set. Adagio recommends to use its own player to get expected behavior.`); + } + } + + // Only whitelisted OpenRTB options need to be validated. + // Other options will still remain in the `mediaTypes.video` object + // sent in the ad-request, but will be ignored by the SSP. + Object.keys(ORTB_VIDEO_PARAMS).forEach(paramName => { + if (videoParams.hasOwnProperty(paramName)) { + if (ORTB_VIDEO_PARAMS[paramName](videoParams[paramName])) { + bidRequest.mediaTypes.video[paramName] = videoParams[paramName]; + } else { + delete bidRequest.mediaTypes.video[paramName]; + utils.logWarn(`${LOG_PREFIX} The OpenRTB video param ${paramName} has been skipped due to misformating. Please refer to OpenRTB 2.5 spec.`); + } + } + }); +} + +function _renderer(bid) { + bid.renderer.push(() => { + if (typeof window.ADAGIO.outstreamPlayer === 'function') { + window.ADAGIO.outstreamPlayer(bid); + } else { + utils.logError(`${LOG_PREFIX} Adagio outstream player is not defined`); + } + }); +} + +function _parseNativeBidResponse(bid) { + if (!bid.admNative || !Array.isArray(bid.admNative.assets)) { + utils.logError(`${LOG_PREFIX} Invalid native response`); + return; + } + + const native = {} + + function addAssetDataValue(data) { + const map = { + 1: 'sponsoredBy', // sponsored + 2: 'body', // desc + 3: 'rating', + 4: 'likes', + 5: 'downloads', + 6: 'price', + 7: 'salePrice', + 8: 'phone', + 9: 'address', + 10: 'body2', // desc2 + 11: 'displayUrl', + 12: 'cta' + } + if (map.hasOwnProperty(data.type) && typeof data.value === 'string') { + native[map[data.type]] = data.value; + } + } + + // assets + bid.admNative.assets.forEach(asset => { + if (asset.title) { + native.title = asset.title.text + } else if (asset.data) { + addAssetDataValue(asset.data) + } else if (asset.img) { + switch (asset.img.type) { + case 1: + native.icon = { + url: asset.img.url, + width: asset.img.w, + height: asset.img.h + }; + break; + default: + native.image = { + url: asset.img.url, + width: asset.img.w, + height: asset.img.h + }; + break; + } + } + }); + + if (bid.admNative.link) { + if (bid.admNative.link.url) { + native.clickUrl = bid.admNative.link.url; + } + if (Array.isArray(bid.admNative.link.clickTrackers)) { + native.clickTrackers = bid.admNative.link.clickTrackers + } + } + + if (Array.isArray(bid.admNative.eventtrackers)) { + native.impressionTrackers = []; + bid.admNative.eventtrackers.forEach(tracker => { + // Only Impression events are supported. Prebid does not support Viewability events yet. + if (tracker.event !== 1) { + return; + } + + // methods: + // 1: image + // 2: js + // note: javascriptTrackers is a string. If there's more than one JS tracker in bid response, the last script will be used. + switch (tracker.method) { + case 1: + native.impressionTrackers.push(tracker.url); + break; + case 2: + native.javascriptTrackers = ``; + break; + } + }); + } else { + native.impressionTrackers = Array.isArray(bid.admNative.imptrackers) ? bid.admNative.imptrackers : []; + if (bid.admNative.jstracker) { + native.javascriptTrackers = bid.admNative.jstracker; + } + } + + if (bid.admNative.privacy) { + native.privacyLink = bid.admNative.privacy; + } + + if (bid.admNative.ext) { + native.ext = {} + + if (bid.admNative.ext.bvw) { + native.ext.adagio_bvw = bid.admNative.ext.bvw; + } + } + + bid.native = native +} + export const spec = { code: BIDDER_CODE, gvlid: GVLID, @@ -573,16 +866,22 @@ 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 = () => ({ action: 'pb-dbg', @@ -613,7 +912,7 @@ export const spec = { // Store adUnits config. // If an adUnitCode has already been stored, it will be replaced. w.ADAGIO = w.ADAGIO || {}; - w.ADAGIO.pbjsAdUnits = w.ADAGIO.pbjsAdUnits.filter((adUnit) => adUnit.code !== adUnitCode) + w.ADAGIO.pbjsAdUnits = w.ADAGIO.pbjsAdUnits.filter((adUnit) => adUnit.code !== adUnitCode); w.ADAGIO.pbjsAdUnits.push({ code: adUnitCode, mediaTypes: mediaTypes || {}, @@ -643,9 +942,17 @@ export const spec = { const site = internal.getSite(bidderRequest); const pageviewId = internal.getPageviewId(); const gdprConsent = _getGdprConsent(bidderRequest) || {}; + const uspConsent = _getUspConsent(bidderRequest) || {}; + const coppa = _getCoppa(); const schain = _getSchain(validBidRequests[0]); + const eids = _getEids(validBidRequests[0]) || []; const adUnits = utils._map(validBidRequests, (bidRequest) => { bidRequest.features = internal.getFeatures(bidRequest, bidderRequest); + + if (utils.deepAccess(bidRequest, 'mediaTypes.video')) { + _buildVideoBidRequest(bidRequest); + } + return bidRequest; }); @@ -653,7 +960,7 @@ export const spec = { const groupedAdUnits = adUnits.reduce((groupedAdUnits, adUnit) => { adUnit.params.organizationId = adUnit.params.organizationId.toString(); - groupedAdUnits[adUnit.params.organizationId] = groupedAdUnits[adUnit.params.organizationId] || [] + groupedAdUnits[adUnit.params.organizationId] = groupedAdUnits[adUnit.params.organizationId] || []; groupedAdUnits[adUnit.params.organizationId].push(adUnit); return groupedAdUnits; @@ -672,8 +979,16 @@ export const spec = { site: site, pageviewId: pageviewId, adUnits: groupedAdUnits[organizationId], - gdpr: gdprConsent, + data: EXT_DATA, + regs: { + gdpr: gdprConsent, + coppa: coppa, + ccpa: uspConsent + }, schain: schain, + user: { + eids: eids + }, prebidVersion: '$prebid.version$', adapterVersion: VERSION, featuresVersion: FEATURES_VERSION @@ -681,7 +996,7 @@ export const spec = { options: { contentType: 'text/plain' } - } + }; }); return requests; @@ -702,7 +1017,38 @@ export const spec = { if (response.bids) { response.bids.forEach(bidObj => { const bidReq = (find(bidRequest.data.adUnits, bid => bid.bidId === bidObj.requestId)); + if (bidReq) { + bidObj.meta = utils.deepAccess(bidObj, 'meta', {}); + bidObj.meta.mediaType = bidObj.mediaType; + bidObj.meta.advertiserDomains = (Array.isArray(bidObj.aDomain) && bidObj.aDomain.length) ? bidObj.aDomain : []; + + if (bidObj.mediaType === VIDEO) { + const mediaTypeContext = utils.deepAccess(bidReq, 'mediaTypes.video.context'); + // Adagio SSP returns a `vastXml` only. No `vastUrl` nor `videoCacheKey`. + if (!bidObj.vastUrl && bidObj.vastXml) { + bidObj.vastUrl = 'data:text/xml;charset=utf-8;base64,' + btoa(bidObj.vastXml.replace(/\\"/g, '"')); + } + + if (mediaTypeContext === OUTSTREAM) { + bidObj.renderer = Renderer.install({ + id: bidObj.requestId, + adUnitCode: bidObj.adUnitCode, + url: bidObj.urlRenderer || RENDERER_URL, + config: { + ...utils.deepAccess(bidReq, 'mediaTypes.video'), + ...utils.deepAccess(bidObj, 'outstream', {}) + } + }); + + bidObj.renderer.setRender(_renderer); + } + } + + if (bidObj.mediaType === NATIVE) { + _parseNativeBidResponse(bidObj); + } + bidObj.site = bidReq.params.site; bidObj.placement = bidReq.params.placement; bidObj.pagetype = bidReq.params.pagetype; diff --git a/modules/adagioBidAdapter.md b/modules/adagioBidAdapter.md index b34cc3fe37a..46656d88d37 100644 --- a/modules/adagioBidAdapter.md +++ b/modules/adagioBidAdapter.md @@ -22,10 +22,10 @@ Connects to Adagio demand source to fetch bids. bids: [{ bidder: 'adagio', // Required params: { - organizationId: '0', // Required - Organization ID provided by Adagio. - site: 'news-of-the-day', // Required - Site Name provided by Adagio. - placement: 'ban_atf', // Required. Refers to the placement of an adunit in a page. Must not contain any information about the type of device. Other example: `mpu_btf'. - adUnitElementId: 'dfp_banniere_atf', // Required - AdUnit element id. Refers to the adunit id in a page. Usually equals to the adunit code above. + organizationId: '1002', // Required - Organization ID provided by Adagio. + site: 'adagio-io', // Required - Site Name provided by Adagio. + placement: 'in_article', // Required. Refers to the placement of an adunit in a page. Must not contain any information about the type of device. Other example: `mpu_btf'. + adUnitElementId: 'article_outstream', // Required - AdUnit element id. Refers to the adunit id in a page. Usually equals to the adunit code above. // The following params are limited to 30 characters, // and can only contain the following characters: @@ -37,7 +37,110 @@ Connects to Adagio demand source to fetch bids. environment: 'mobile', // Recommended. Environment where the page is displayed. 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. + 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, + cpm: 3.00 // default to 1.00 + } + } + }] + }, + { + code: 'article_outstream', + mediaTypes: { + video: { + context: 'outstream', + playerSize: [640, 480], + mimes: ['video/mp4'], + skip: 1 + // Other OpenRTB 2.5 video options… + } + }, + bids: [{ + bidder: 'adagio', // Required + params: { + organizationId: '1002', // Required - Organization ID provided by Adagio. + site: 'adagio-io', // Required - Site Name provided by Adagio. + placement: 'in_article', // Required. Refers to the placement of an adunit in a page. Must not contain any information about the type of device. Other example: `mpu_btf'. + adUnitElementId: 'article_outstream', // Required - AdUnit element id. Refers to the adunit id in a page. Usually equals to the adunit code above. + + // The following params are limited to 30 characters, + // and can only contain the following characters: + // - alphanumeric (A-Z+a-z+0-9, case-insensitive) + // - dashes `-` + // - underscores `_` + // Also, each param can have at most 50 unique active values (case-insensitive). + pagetype: 'article', // Highly recommended. The pagetype describes what kind of content will be present in the page. + environment: 'mobile', // Recommended. Environment where the page is displayed. + 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. + }, + // Optional debug mode, used to get a bid response with expected cpm. + debug: { + enabled: true, + cpm: 3.00 // default to 1.00 + } + } + }] + }, + { + code: 'article_native', + mediaTypes: { + native: { + // generic Prebid options + title: { + required: true, + len: 80 + }, + // … + // Custom Adagio data assets + ext: { + adagio_bvw: { + required: false + } + } + } + }, + bids: [{ + bidder: 'adagio', // Required + params: { + organizationId: '1002', // Required - Organization ID provided by Adagio. + site: 'adagio-io', // Required - Site Name provided by Adagio. + placement: 'in_article', // Required. Refers to the placement of an adunit in a page. Must not contain any information about the type of device. Other example: `mpu_btf'. + adUnitElementId: 'article_native', // Required - AdUnit element id. Refers to the adunit id in a page. Usually equals to the adunit code above. + + // The following params are limited to 30 characters, + // and can only contain the following characters: + // - alphanumeric (A-Z+a-z+0-9, case-insensitive) + // - dashes `-` + // - underscores `_` + // Also, each param can have at most 50 unique active values (case-insensitive). + pagetype: 'article', // Highly recommended. The pagetype describes what kind of content will be present in the page. + environment: 'mobile', // Recommended. Environment where the page is displayed. + 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 OpenRTB Native 1.2 request object. Only `context`, `plcmttype` fields are supported. + native: { + context: 1, + plcmttype: 2 + }, + // Optional debug mode, used to get a bid response with expected cpm. + debug: { + enabled: true, + cpm: 3.00 // default to 1.00 + } } }] } @@ -88,5 +191,4 @@ Connects to Adagio demand source to fetch bids. ] } } - ``` 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/addefendBidAdapter.js b/modules/addefendBidAdapter.js new file mode 100644 index 00000000000..18cafe829b5 --- /dev/null +++ b/modules/addefendBidAdapter.js @@ -0,0 +1,79 @@ +import {registerBidder} from '../src/adapters/bidderFactory.js'; + +const BIDDER_CODE = 'addefend'; + +export const spec = { + code: BIDDER_CODE, + hostname: 'https://addefend-platform.com', + + getHostname() { + return this.hostname; + }, + isBidRequestValid: function(bid) { + return (bid.sizes !== undefined && bid.bidId !== undefined && bid.params !== undefined && + (bid.params.pageId !== undefined && (typeof bid.params.pageId === 'string')) && + (bid.params.placementId !== undefined && (typeof bid.params.placementId === 'string'))); + }, + buildRequests: function(validBidRequests, bidderRequest) { + let bid = { + v: $$PREBID_GLOBAL$$.version, + auctionId: false, + pageId: false, + gdpr_consent: bidderRequest.gdprConsent && bidderRequest.gdprConsent.consentString ? bidderRequest.gdprConsent.consentString : '', + referer: bidderRequest.refererInfo.referer, + bids: [], + }; + + for (var i = 0; i < validBidRequests.length; i++) { + let vb = validBidRequests[i]; + let o = vb.params; + bid.auctionId = vb.auctionId; + o.bidId = vb.bidId; + o.transactionId = vb.transactionId; + o.sizes = []; + if (o.trafficTypes) { + bid.trafficTypes = o.trafficTypes; + } + delete o.trafficTypes; + + bid.pageId = o.pageId; + delete o.pageId; + + if (vb.sizes && Array.isArray(vb.sizes)) { + for (var j = 0; j < vb.sizes.length; j++) { + let s = vb.sizes[j]; + if (Array.isArray(s) && s.length == 2) { + o.sizes.push(s[0] + 'x' + s[1]); + } + } + } + bid.bids.push(o); + } + return [{ + method: 'POST', + url: this.getHostname() + '/bid', + options: { withCredentials: true }, + data: bid + }]; + }, + interpretResponse: function(serverResponse, request) { + const requiredKeys = ['requestId', 'cpm', 'width', 'height', 'ad', 'ttl', 'creativeId', 'netRevenue', 'currency', 'advertiserDomains']; + const validBidResponses = []; + serverResponse = serverResponse.body; + if (serverResponse && (serverResponse.length > 0)) { + serverResponse.forEach((bid) => { + const bidResponse = {}; + for (const requiredKey of requiredKeys) { + if (!bid.hasOwnProperty(requiredKey)) { + return []; + } + bidResponse[requiredKey] = bid[requiredKey]; + } + validBidResponses.push(bidResponse); + }); + } + return validBidResponses; + } +} + +registerBidder(spec); diff --git a/modules/addefendBidAdapter.md b/modules/addefendBidAdapter.md new file mode 100644 index 00000000000..f1ac3ff2c46 --- /dev/null +++ b/modules/addefendBidAdapter.md @@ -0,0 +1,43 @@ +# Overview + +``` +Module Name: AdDefend Bid Adapter +Module Type: Bidder Adapter +Maintainer: prebid@addefend.com +``` + +# Description + +Module that connects to AdDefend as a demand source. + +## Parameters +| Param | Description | Optional | Default | +| ------------- | ------------- | ----- | ----- | +| pageId | id assigned to the website in the AdDefend system. (ask AdDefend support) | no | - | +| placementId | id of the placement in the AdDefend system. (ask AdDefend support) | no | - | +| trafficTypes | comma seperated list of the following traffic types:
ADBLOCK - user has a activated adblocker
PM - user has firefox private mode activated
NC - user has not given consent
NONE - user traffic is none of the above, this usually means this is a "normal" user.
| yes | ADBLOCK | + + +# Test Parameters +``` + var adUnits = [ + { + code: 'test-div', + mediaTypes: { + banner: { + sizes: [[970, 250]], // a display size + } + }, + bids: [ + { + bidder: "addefend", + params: { + pageId: "887", + placementId: "9398", + trafficTypes: "ADBLOCK" + } + } + ] + } + ]; +``` diff --git a/modules/adformOpenRTBBidAdapter.js b/modules/adfBidAdapter.js similarity index 97% rename from modules/adformOpenRTBBidAdapter.js rename to modules/adfBidAdapter.js index 3270fb5865a..8b3550e6108 100644 --- a/modules/adformOpenRTBBidAdapter.js +++ b/modules/adfBidAdapter.js @@ -10,8 +10,9 @@ import { import * as utils from '../src/utils.js'; import { config } from '../src/config.js'; -const BIDDER_CODE = 'adformOpenRTB'; +const BIDDER_CODE = 'adf'; const GVLID = 50; +const BIDDER_ALIAS = [ { code: 'adformOpenRTB', gvlid: GVLID } ]; const NATIVE_ASSET_IDS = { 0: 'title', 2: 'icon', 3: 'image', 5: 'sponsoredBy', 4: 'body', 1: 'cta' }; const NATIVE_PARAMS = { title: { @@ -47,6 +48,7 @@ const NATIVE_PARAMS = { export const spec = { code: BIDDER_CODE, + aliases: BIDDER_ALIAS, gvlid: GVLID, supportedMediaTypes: [ NATIVE ], isBidRequestValid: bid => !!bid.params.mid, @@ -170,7 +172,6 @@ export const spec = { netRevenue: bid.netRevenue === 'net', currency: cur, mediaType: NATIVE, - bidderCode: BIDDER_CODE, native: parseNative(bidResponse) }; } diff --git a/modules/adformOpenRTBBidAdapter.md b/modules/adfBidAdapter.md similarity index 90% rename from modules/adformOpenRTBBidAdapter.md rename to modules/adfBidAdapter.md index 0dd98ad07b8..190aa91ea57 100644 --- a/modules/adformOpenRTBBidAdapter.md +++ b/modules/adfBidAdapter.md @@ -1,13 +1,13 @@ # Overview -Module Name: Adform OpenRTB Adapter +Module Name: Adf Adapter Module Type: Bidder Adapter Maintainer: Scope.FL.Scripts@adform.com # Description Module that connects to Adform demand sources to fetch bids. -Only native format is supported. Using OpenRTB standard. +Only native format is supported. Using OpenRTB standard. Previous adapter name - adformOpenRTB. # Test Parameters ``` @@ -42,7 +42,7 @@ Only native format is supported. Using OpenRTB standard. } }, bids: [{ - bidder: 'adformOpenRTB', + bidder: 'adf', params: { mid: 606169, // required adxDomain: 'adx.adform.net', // optional diff --git a/modules/adformBidAdapter.js b/modules/adformBidAdapter.js index 48000e082b2..3d0ed0fba79 100644 --- a/modules/adformBidAdapter.js +++ b/modules/adformBidAdapter.js @@ -5,6 +5,7 @@ import { config } from '../src/config.js'; import { BANNER, VIDEO } from '../src/mediaTypes.js'; import { Renderer } from '../src/Renderer.js'; import * as utils from '../src/utils.js'; +import includes from 'core-js-pure/features/array/includes.js'; const OUTSTREAM_RENDERER_URL = 'https://s2.adform.net/banners/scripts/video/outstream/render.js'; @@ -23,14 +24,33 @@ export const spec = { const eids = getEncodedEIDs(utils.deepAccess(validBidRequests, '0.userIdAsEids')); var request = []; - var globalParams = [ [ 'adxDomain', 'adx.adform.net' ], [ 'fd', 1 ], [ 'url', null ], [ 'tid', null ] ]; + var globalParams = [ [ 'adxDomain', 'adx.adform.net' ], [ 'fd', 1 ], [ 'url', null ], [ 'tid', null ], [ 'eids', eids ] ]; + const targetingParams = { mkv: [], mkw: [], msw: [] }; + var bids = JSON.parse(JSON.stringify(validBidRequests)); var bidder = (bids[0] && bids[0].bidder) || BIDDER_CODE; + + // set common targeting options as query params + if (bids.length > 1) { + for (let key in targetingParams) { + if (targetingParams.hasOwnProperty(key)) { + const collection = bids.map(bid => ((bid.params[key] && bid.params[key].split(',')) || [])); + targetingParams[key] = collection.reduce(intersects); + if (targetingParams[key].length) { + bids.forEach((bid, index) => { + bid.params[key] = collection[index].filter(item => !includes(targetingParams[key], item)); + }); + } + } + } + } + for (i = 0, l = bids.length; i < l; i++) { bid = bids[i]; if ((bid.params.priceType === 'net') || (bid.params.pt === 'net')) { netRevenue = 'net'; } + for (j = 0, k = globalParams.length; j < k; j++) { _key = globalParams[j][0]; _value = bid[_key] || bid.params[_key]; @@ -65,8 +85,10 @@ export const spec = { request.push('us_privacy=' + bidderRequest.uspConsent); } - if (eids) { - request.push('eids=' + eids); + for (let key in targetingParams) { + if (targetingParams.hasOwnProperty(key)) { + globalParams.push([ key, targetingParams[key].join(',') ]); + } } for (i = 1, l = globalParams.length; i < l; i++) { @@ -100,7 +122,7 @@ export const spec = { function getEncodedEIDs(eids) { if (utils.isArray(eids) && eids.length > 0) { const parsed = parseEIDs(eids); - return encodeURIComponent(btoa(JSON.stringify(parsed))); + return btoa(JSON.stringify(parsed)); } } @@ -118,6 +140,10 @@ export const spec = { return result; }, {}); } + + function intersects(col1, col2) { + return col1.filter(item => includes(col2, item)); + } }, interpretResponse: function (serverResponse, bidRequest) { const VALID_RESPONSES = { @@ -144,6 +170,7 @@ export const spec = { currency: response.win_cur, netRevenue: bidRequest.netRevenue !== 'gross', ttl: 360, + meta: { advertiserDomains: response && response.adomain ? response.adomain : [] }, ad: response.banner, bidderCode: bidRequest.bidder, transactionId: bid.transactionId, 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 cd5a44f34d8..b9dbae529ba 100644 --- a/modules/adheseBidAdapter.js +++ b/modules/adheseBidAdapter.js @@ -4,10 +4,12 @@ import { registerBidder } from '../src/adapters/bidderFactory.js'; import { BANNER, VIDEO } from '../src/mediaTypes.js'; const BIDDER_CODE = 'adhese'; +const GVLID = 553; const USER_SYNC_BASE_URL = 'https://user-sync.adhese.com/iframe/user_sync.html'; export const spec = { code: BIDDER_CODE, + gvlid: GVLID, supportedMediaTypes: [BANNER, VIDEO], isBidRequestValid: function(bid) { @@ -20,16 +22,25 @@ 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, + vastContentAsUrl: true, + user: { + ext: { + eids: getEids(validBidRequests), + } + } + }; const account = getAccount(validBidRequests); const uri = 'https://ads-' + account + '.adhese.com/json'; @@ -85,7 +96,7 @@ function adResponse(bid, ad) { const bidResponse = getbaseAdResponse({ requestId: bid.bidId, - mediaType: getMediaType(markup), + mediaType: ad.extension.mediaType, cpm: Number(price.amount), currency: price.currency, width: Number(ad.width), @@ -100,7 +111,11 @@ function adResponse(bid, ad) { }); if (bidResponse.mediaType === VIDEO) { - bidResponse.vastXml = markup; + if (ad.cachedBodyUrl) { + bidResponse.vastUrl = ad.cachedBodyUrl + } else { + bidResponse.vastXml = markup; + } } else { const counter = ad.impressionCounter ? "" : ''; bidResponse.ad = markup + counter; @@ -108,16 +123,20 @@ 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]; - const values = Array.isArray(val) ? val : [val]; - if (targets[key]) { - const distinctValues = values.filter(v => targets[key].indexOf(v) < 0); - targets[key].push.apply(targets[key], distinctValues); - } else { - targets[key] = values; + const dirtyValues = Array.isArray(val) ? val : [val]; + const values = dirtyValues.filter(v => v === 0 || v); + if (values.length > 0) { + if (targets[key]) { + const distinctValues = values.filter(v => targets[key].indexOf(v) < 0); + targets[key].push.apply(targets[key], distinctValues); + } else { + targets[key] = values; + } } }); } @@ -144,9 +163,9 @@ function getAccount(validBidRequests) { return validBidRequests[0].params.account; } -function getId5Id(validBidRequests) { - if (validBidRequests[0] && validBidRequests[0].userId && validBidRequests[0].userId.id5id) { - return validBidRequests[0].userId.id5id; +function getEids(validBidRequests) { + if (validBidRequests[0] && validBidRequests[0].userIdAsEids) { + return validBidRequests[0].userIdAsEids; } } @@ -158,11 +177,6 @@ function isAdheseAd(ad) { return !ad.origin || ad.origin === 'JERLICIA'; } -function getMediaType(markup) { - const isVideo = markup.trim().toLowerCase().match(/<\?xml| { adapterManager.registerAnalyticsAdapter({ adapter: analyticsAdapter, - code: 'adkernelAdn' + code: 'adkernelAdn', + gvlid: GVLID }); export default analyticsAdapter; @@ -390,3 +395,19 @@ function getLocationAndReferrer(win) { loc: win.location }; } + +function initPrivacy(template, requests) { + let consent = requests[0].gdprConsent; + if (consent && consent.gdprApplies) { + template.user.gdpr = ~~consent.gdprApplies; + } + if (consent && consent.consentString) { + template.user.gdpr_consent = consent.consentString; + } + if (requests[0].uspConsent) { + template.user.us_privacy = requests[0].uspConsent; + } + if (config.getConfig('coppa')) { + template.user.coppa = 1; + } +} diff --git a/modules/adkernelAdnBidAdapter.js b/modules/adkernelAdnBidAdapter.js index 0a0317e1f59..0de750c773f 100644 --- a/modules/adkernelAdnBidAdapter.js +++ b/modules/adkernelAdnBidAdapter.js @@ -1,11 +1,13 @@ import * as utils from '../src/utils.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; import {BANNER, VIDEO} from '../src/mediaTypes.js'; +import {config} from '../src/config.js'; const DEFAULT_ADKERNEL_DSP_DOMAIN = 'tag.adkernel.com'; const DEFAULT_MIMES = ['video/mp4', 'video/webm', 'application/x-shockwave-flash', 'application/javascript']; const DEFAULT_PROTOCOLS = [2, 3, 5, 6]; const DEFAULT_APIS = [1, 2]; +const GVLID = 14; function isRtbDebugEnabled(refInfo) { return refInfo.referer.indexOf('adk_debug=true') !== -1; @@ -16,13 +18,15 @@ function buildImp(bidRequest) { id: bidRequest.bidId, tagid: bidRequest.adUnitCode }; + let mediaType; let bannerReq = utils.deepAccess(bidRequest, `mediaTypes.banner`); let videoReq = utils.deepAccess(bidRequest, `mediaTypes.video`); if (bannerReq) { let sizes = canonicalizeSizesArray(bannerReq.sizes); imp.banner = { format: utils.parseSizesInput(sizes) - } + }; + mediaType = BANNER; } else if (videoReq) { let size = canonicalizeSizesArray(videoReq.playerSize)[0]; imp.video = { @@ -32,6 +36,11 @@ function buildImp(bidRequest) { protocols: videoReq.protocols || DEFAULT_PROTOCOLS, api: videoReq.api || DEFAULT_APIS }; + mediaType = VIDEO; + } + let bidFloor = getBidFloor(bidRequest, mediaType, '*'); + if (bidFloor) { + imp.bidfloor = bidFloor; } return imp; } @@ -67,6 +76,9 @@ function buildRequestParams(tags, bidderRequest) { if (uspConsent) { utils.deepSetValue(req, 'user.us_privacy', uspConsent); } + if (config.getConfig('coppa')) { + utils.deepSetValue(req, 'user.coppa', 1); + } return req; } @@ -108,8 +120,21 @@ function buildBid(tag) { return bid; } +function getBidFloor(bid, mediaType, sizes) { + var floor; + var size = sizes.length === 1 ? sizes[0] : '*'; + if (typeof bid.getFloor === 'function') { + const floorInfo = bid.getFloor({currency: 'USD', mediaType, size}); + if (typeof floorInfo === 'object' && floorInfo.currency === 'USD' && !isNaN(parseFloat(floorInfo.floor))) { + floor = parseFloat(floorInfo.floor); + } + } + return floor; +} + export const spec = { code: 'adkernelAdn', + gvlid: GVLID, supportedMediaTypes: [BANNER, VIDEO], aliases: ['engagesimply'], diff --git a/modules/adkernelBidAdapter.js b/modules/adkernelBidAdapter.js index 972dd696bf6..e4b04a27d42 100644 --- a/modules/adkernelBidAdapter.js +++ b/modules/adkernelBidAdapter.js @@ -15,13 +15,14 @@ import {config} from '../src/config.js'; const VIDEO_TARGETING = Object.freeze(['mimes', 'minduration', 'maxduration', 'protocols', 'startdelay', 'linearity', 'boxingallowed', 'playbackmethod', 'delivery', 'pos', 'api', 'ext']); -const VERSION = '1.5'; +const VERSION = '1.6'; const SYNC_IFRAME = 1; const SYNC_IMAGE = 2; const SYNC_TYPES = Object.freeze({ 1: 'iframe', 2: 'image' }); +const GVLID = 14; const NATIVE_MODEL = [ {name: 'title', assetType: 'title'}, @@ -50,9 +51,25 @@ const NATIVE_INDEX = NATIVE_MODEL.reduce((acc, val, idx) => { * Adapter for requesting bids from AdKernel white-label display platform */ export const spec = { - code: 'adkernel', - aliases: ['headbidding', 'adsolut', 'oftmediahb', 'audiencemedia', 'waardex_ak', 'roqoon'], + gvlid: GVLID, + aliases: [ + {code: 'headbidding'}, + {code: 'adsolut'}, + {code: 'oftmediahb'}, + {code: 'audiencemedia'}, + {code: 'waardex_ak'}, + {code: 'roqoon'}, + {code: 'andbeyond'}, + {code: 'adbite'}, + {code: 'houseofpubs'}, + {code: 'torchad'}, + {code: 'stringads'}, + {code: 'bcm'}, + {code: 'engageadx'}, + {code: 'converge_digital', gvlid: 248}, + {code: 'adomega'} + ], supportedMediaTypes: [BANNER, VIDEO, NATIVE], /** @@ -116,13 +133,10 @@ export const spec = { requestId: rtbBid.impid, cpm: rtbBid.price, creativeId: rtbBid.crid, - currency: 'USD', + currency: response.cur || 'USD', ttl: 360, netRevenue: true }; - if (rtbBid.dealid !== undefined) { - prBid.dealId = rtbBid.dealid; - } if ('banner' in imp) { prBid.mediaType = BANNER; prBid.width = rtbBid.w; @@ -137,6 +151,27 @@ export const spec = { prBid.mediaType = NATIVE; prBid.native = buildNativeAd(JSON.parse(rtbBid.adm)); } + if (utils.isStr(rtbBid.dealid)) { + prBid.dealId = rtbBid.dealid; + } + if (utils.isArray(rtbBid.adomain)) { + utils.deepSetValue(prBid, 'meta.advertiserDomains', rtbBid.adomain); + } + if (utils.isArray(rtbBid.cat)) { + utils.deepSetValue(prBid, 'meta.secondaryCatIds', rtbBid.cat); + } + if (utils.isPlainObject(rtbBid.ext)) { + if (utils.isNumber(rtbBid.ext.advertiser_id)) { + utils.deepSetValue(prBid, 'meta.advertiserId', rtbBid.ext.advertiser_id); + } + if (utils.isStr(rtbBid.ext.advertiser_name)) { + utils.deepSetValue(prBid, 'meta.advertiserName', rtbBid.ext.advertiser_name); + } + if (utils.isStr(rtbBid.ext.agency_name)) { + utils.deepSetValue(prBid, 'meta.agencyName', rtbBid.ext.agency_name); + } + } + return prBid; }); }, @@ -178,6 +213,18 @@ function dispatchImps(bidRequests, refererInfo) { }, {}); } +function getBidFloor(bid, mediaType, sizes) { + var floor; + var size = sizes.length === 1 ? sizes[0] : '*'; + if (typeof bid.getFloor === 'function') { + const floorInfo = bid.getFloor({currency: 'USD', mediaType, size}); + if (typeof floorInfo === 'object' && floorInfo.currency === 'USD' && !isNaN(parseFloat(floorInfo.floor))) { + floor = parseFloat(floorInfo.floor); + } + } + return floor; +} + /** * Builds rtb imp object for single adunit * @param bidRequest {BidRequest} @@ -188,27 +235,42 @@ function buildImp(bidRequest, secure) { 'id': bidRequest.bidId, 'tagid': bidRequest.adUnitCode }; + var mediaType; + var sizes = []; - if (utils.deepAccess(bidRequest, `mediaTypes.banner`)) { - let sizes = utils.getAdUnitSizes(bidRequest); + if (utils.deepAccess(bidRequest, 'mediaTypes.banner')) { + sizes = utils.getAdUnitSizes(bidRequest); imp.banner = { format: sizes.map(wh => utils.parseGPTSingleSizeArrayToRtbSize(wh)), topframe: 0 }; + mediaType = BANNER; } else if (utils.deepAccess(bidRequest, 'mediaTypes.video')) { - let sizes = bidRequest.mediaTypes.video.playerSize || []; - imp.video = utils.parseGPTSingleSizeArrayToRtbSize(sizes[0]) || {}; + let video = utils.deepAccess(bidRequest, 'mediaTypes.video'); + imp.video = {}; + if (video.playerSize) { + sizes = video.playerSize[0]; + imp.video = Object.assign(imp.video, utils.parseGPTSingleSizeArrayToRtbSize(sizes) || {}); + } if (bidRequest.params.video) { Object.keys(bidRequest.params.video) .filter(key => includes(VIDEO_TARGETING, key)) .forEach(key => imp.video[key] = bidRequest.params.video[key]); } + mediaType = VIDEO; } else if (utils.deepAccess(bidRequest, 'mediaTypes.native')) { let nativeRequest = buildNativeRequest(bidRequest.mediaTypes.native); imp.native = { ver: '1.1', request: JSON.stringify(nativeRequest) - } + }; + mediaType = NATIVE; + } else { + throw new Error('Unsupported bid received'); + } + let floor = getBidFloor(bidRequest, mediaType, sizes); + if (floor) { + imp.bidfloor = floor; } if (secure) { imp.secure = 1; @@ -302,7 +364,7 @@ function getAllowedSyncMethod(bidderCode) { */ function buildRtbRequest(imps, bidderRequest, schain) { let {bidderCode, gdprConsent, auctionId, refererInfo, timeout, uspConsent} = bidderRequest; - + let coppa = config.getConfig('coppa'); let req = { 'id': auctionId, 'imp': imps, @@ -331,6 +393,9 @@ function buildRtbRequest(imps, bidderRequest, schain) { if (uspConsent) { utils.deepSetValue(req, 'regs.ext.us_privacy', uspConsent); } + if (coppa) { + utils.deepSetValue(req, 'regs.coppa', 1); + } let syncMethod = getAllowedSyncMethod(bidderCode); if (syncMethod) { utils.deepSetValue(req, 'ext.adk_usersync', syncMethod); diff --git a/modules/adlooxAnalyticsAdapter.js b/modules/adlooxAnalyticsAdapter.js new file mode 100644 index 00000000000..3e92ae34004 --- /dev/null +++ b/modules/adlooxAnalyticsAdapter.js @@ -0,0 +1,288 @@ +/** + * This module provides [Adloox]{@link https://www.adloox.com/} Analytics + * The module will inject Adloox's verification JS tag alongside slot at bidWin + * @module modules/adlooxAnalyticsAdapter + */ + +import adapterManager from '../src/adapterManager.js'; +import adapter from '../src/AnalyticsAdapter.js'; +import { loadExternalScript } from '../src/adloader.js'; +import { auctionManager } from '../src/auctionManager.js'; +import { AUCTION_COMPLETED } from '../src/auction.js'; +import { EVENTS } from '../src/constants.json'; +import find from 'core-js-pure/features/array/find.js'; +import * as utils from '../src/utils.js'; + +const MODULE = 'adlooxAnalyticsAdapter'; + +const URL_JS = 'https://j.adlooxtracking.com/ads/js/tfav_adl_%%clientid%%.js'; + +const ADLOOX_VENDOR_ID = 93; + +const ADLOOX_MEDIATYPE = { + DISPLAY: 2, + VIDEO: 6 +}; + +const MACRO = {}; +MACRO['client'] = function(b, c) { + return c.client; +}; +MACRO['clientid'] = function(b, c) { + return c.clientid; +}; +MACRO['tagid'] = function(b, c) { + return c.tagid; +}; +MACRO['platformid'] = function(b, c) { + return c.platformid; +}; +MACRO['targetelt'] = function(b, c) { + return c.toselector(b); +}; +MACRO['creatype'] = function(b, c) { + return b.mediaType == 'video' ? ADLOOX_MEDIATYPE.VIDEO : ADLOOX_MEDIATYPE.DISPLAY; +}; +MACRO['pbAdSlot'] = function(b, c) { + const adUnit = find(auctionManager.getAdUnits(), a => b.adUnitCode === a.code); + return utils.deepAccess(adUnit, 'fpd.context.pbAdSlot') || utils.getGptSlotInfoForAdUnitCode(b.adUnitCode).gptSlot || b.adUnitCode; +}; + +const PARAMS_DEFAULT = { + 'id1': function(b) { return b.adUnitCode }, + 'id2': '%%pbAdSlot%%', + 'id3': function(b) { return b.bidder }, + 'id4': function(b) { return b.adId }, + 'id5': function(b) { return b.dealId }, + 'id6': function(b) { return b.creativeId }, + 'id7': function(b) { return b.size }, + 'id11': '$ADLOOX_WEBSITE' +}; + +const NOOP = function() {}; + +let analyticsAdapter = Object.assign(adapter({ analyticsType: 'endpoint' }), { + track({ eventType, args }) { + if (!analyticsAdapter[`handle_${eventType}`]) return; + + utils.logInfo(MODULE, 'track', eventType, args); + + analyticsAdapter[`handle_${eventType}`](args); + } +}); + +analyticsAdapter.context = null; + +analyticsAdapter.originEnableAnalytics = analyticsAdapter.enableAnalytics; +analyticsAdapter.enableAnalytics = function(config) { + analyticsAdapter.context = null; + + utils.logInfo(MODULE, 'config', config); + + if (!utils.isPlainObject(config.options)) { + utils.logError(MODULE, 'missing options'); + return; + } + if (!(config.options.js === undefined || utils.isStr(config.options.js))) { + utils.logError(MODULE, 'invalid js options value'); + return; + } + if (!(config.options.toselector === undefined || utils.isFn(config.options.toselector))) { + utils.logError(MODULE, 'invalid toselector options value'); + return; + } + if (!utils.isStr(config.options.client)) { + utils.logError(MODULE, 'invalid client options value'); + return; + } + if (!utils.isNumber(config.options.clientid)) { + utils.logError(MODULE, 'invalid clientid options value'); + return; + } + if (!utils.isNumber(config.options.tagid)) { + utils.logError(MODULE, 'invalid tagid options value'); + return; + } + if (!utils.isNumber(config.options.platformid)) { + utils.logError(MODULE, 'invalid platformid options value'); + return; + } + if (!(config.options.params === undefined || utils.isPlainObject(config.options.params))) { + utils.logError(MODULE, 'invalid params options value'); + return; + } + + analyticsAdapter.context = { + js: config.options.js || URL_JS, + toselector: config.options.toselector || function(bid) { + let code = utils.getGptSlotInfoForAdUnitCode(bid.adUnitCode).divId || bid.adUnitCode; + // https://mathiasbynens.be/notes/css-escapes + code = code.replace(/^\d/, '\\3$& '); + return `#${code}` + }, + client: config.options.client, + clientid: config.options.clientid, + tagid: config.options.tagid, + platformid: config.options.platformid, + params: [] + }; + + config.options.params = utils.mergeDeep({}, PARAMS_DEFAULT, config.options.params || {}); + Object + .keys(config.options.params) + .forEach(k => { + if (!Array.isArray(config.options.params[k])) { + config.options.params[k] = [ config.options.params[k] ]; + } + config.options.params[k].forEach(v => analyticsAdapter.context.params.push([ k, v ])); + }); + + Object.keys(COMMAND_QUEUE).forEach(commandProcess); + + analyticsAdapter.originEnableAnalytics(config); +} + +analyticsAdapter.originDisableAnalytics = analyticsAdapter.disableAnalytics; +analyticsAdapter.disableAnalytics = function() { + analyticsAdapter.context = null; + + analyticsAdapter.originDisableAnalytics(); +} + +analyticsAdapter.url = function(url, args, bid) { + // utils.formatQS outputs PHP encoded querystrings... (╯°□°)╯ ┻━┻ + function a2qs(a) { + // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/encodeURIComponent + function fixedEncodeURIComponent(str) { + return encodeURIComponent(str).replace(/[!'()*]/g, function(c) { + return '%' + c.charCodeAt(0).toString(16); + }); + } + + const args = []; + let n = a.length; + while (n-- > 0) { + if (!(a[n][1] === undefined || a[n][1] === null || a[n][1] === false)) { + args.unshift(fixedEncodeURIComponent(a[n][0]) + (a[n][1] !== true ? ('=' + fixedEncodeURIComponent(a[n][1])) : '')); + } + } + + return args.join('&'); + } + + const macros = (str) => { + return str.replace(/%%([a-z]+)%%/gi, (match, p1) => MACRO[p1] ? MACRO[p1](bid, analyticsAdapter.context) : match); + }; + + url = macros(url); + args = args || []; + + let n = args.length; + while (n-- > 0) { + if (utils.isFn(args[n][1])) { + try { + args[n][1] = args[n][1](bid); + } catch (_) { + utils.logError(MODULE, 'macro', args[n][0], _.message); + args[n][1] = `ERROR: ${_.message}`; + } + } + if (utils.isStr(args[n][1])) { + args[n][1] = macros(args[n][1]); + } + } + + return url + a2qs(args); +} + +analyticsAdapter[`handle_${EVENTS.AUCTION_END}`] = function(auctionDetails) { + if (!(auctionDetails.auctionStatus == AUCTION_COMPLETED && auctionDetails.bidsReceived.length > 0)) return; + analyticsAdapter[`handle_${EVENTS.AUCTION_END}`] = NOOP; + + utils.logMessage(MODULE, 'preloading verification JS'); + + const uri = utils.parseUrl(analyticsAdapter.url(`${analyticsAdapter.context.js}#`)); + + const link = document.createElement('link'); + link.setAttribute('href', `${uri.protocol}://${uri.host}${uri.pathname}`); + link.setAttribute('rel', 'preload'); + link.setAttribute('as', 'script'); + utils.insertElement(link); +} + +analyticsAdapter[`handle_${EVENTS.BID_WON}`] = function(bid) { + const sl = analyticsAdapter.context.toselector(bid); + let el; + try { + el = document.querySelector(sl); + } catch (_) { } + if (!el) { + utils.logWarn(MODULE, `unable to find ad unit code '${bid.adUnitCode}' slot using selector '${sl}' (use options.toselector to change), ignoring`); + return; + } + + utils.logMessage(MODULE, `measuring '${bid.mediaType}' unit at '${bid.adUnitCode}'`); + + const params = analyticsAdapter.context.params.concat([ + [ 'tagid', '%%tagid%%' ], + [ 'platform', '%%platformid%%' ], + [ 'fwtype', 4 ], + [ 'targetelt', '%%targetelt%%' ], + [ 'creatype', '%%creatype%%' ] + ]); + + loadExternalScript(analyticsAdapter.url(`${analyticsAdapter.context.js}#`, params, bid), 'adloox'); +} + +adapterManager.registerAnalyticsAdapter({ + adapter: analyticsAdapter, + code: 'adloox', + gvlid: ADLOOX_VENDOR_ID +}); + +export default analyticsAdapter; + +// src/events.js does not support custom events or handle races... (╯°□°)╯ ┻━┻ +const COMMAND_QUEUE = {}; +export const COMMAND = { + CONFIG: 'config', + URL: 'url', + TRACK: 'track' +}; +export function command(cmd, data, callback0) { + const cid = utils.getUniqueIdentifierStr(); + const callback = function() { + delete COMMAND_QUEUE[cid]; + if (callback0) callback0.apply(null, arguments); + }; + COMMAND_QUEUE[cid] = { cmd, data, callback }; + if (analyticsAdapter.context) commandProcess(cid); +} +function commandProcess(cid) { + const { cmd, data, callback } = COMMAND_QUEUE[cid]; + + utils.logInfo(MODULE, 'command', cmd, data); + + switch (cmd) { + case COMMAND.CONFIG: + const response = { + client: analyticsAdapter.context.client, + clientid: analyticsAdapter.context.clientid, + tagid: analyticsAdapter.context.tagid, + platformid: analyticsAdapter.context.platformid + }; + callback(response); + break; + case COMMAND.URL: + if (data.ids) data.args = data.args.concat(analyticsAdapter.context.params.filter(p => /^id([1-9]|10)$/.test(p[0]))); // not >10 + callback(analyticsAdapter.url(data.url, data.args, data.bid)); + break; + case COMMAND.TRACK: + analyticsAdapter.track(data); + callback(); // drain queue + break; + default: + utils.logWarn(MODULE, 'command unknown', cmd); + // do not callback as arguments are unknown and to aid debugging + } +} diff --git a/modules/adlooxAnalyticsAdapter.md b/modules/adlooxAnalyticsAdapter.md new file mode 100644 index 00000000000..0ca67f937f6 --- /dev/null +++ b/modules/adlooxAnalyticsAdapter.md @@ -0,0 +1,146 @@ +# Overview + + Module Name: Adloox Analytics Adapter + Module Type: Analytics Adapter + Maintainer: technique@adloox.com + +# Description + +Analytics adapter for adloox.com. Contact adops@adloox.com for information. + +This module can be used to track: + + * Display + * Native + * Video (see below for further instructions) + +The adapter adds an 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 ad488aa50d9..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,13 +320,19 @@ let atsAnalyticsAdapter = Object.assign(adapter( callHandler(eventType, args); } if (eventType === CONSTANTS.EVENTS.AUCTION_END) { - // send data to ats analytic endpoint + let envelopeSourceCookieValue = storage.getCookie('_lr_env_src_ats'); try { - let dataToSend = {'Data': atsAnalyticsAdapter.context.events}; - let strJSON = JSON.stringify(dataToSend); - ajax(atsAnalyticsAdapter.context.host, function () { - }, strJSON, {method: 'POST', contentType: 'application/json'}); + 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); } } } @@ -141,22 +341,24 @@ let atsAnalyticsAdapter = Object.assign(adapter( // save the base class function atsAnalyticsAdapter.originEnableAnalytics = atsAnalyticsAdapter.enableAnalytics; +// add check to not fire request every time, but instead to send 1/10 events +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; @@ -165,7 +367,8 @@ atsAnalyticsAdapter.enableAnalytics = function (config) { adaptermanager.registerAnalyticsAdapter({ adapter: atsAnalyticsAdapter, - code: 'atsAnalytics' + code: 'atsAnalytics', + gvlid: 97 }); export default atsAnalyticsAdapter; 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/audigentRtdProvider.js b/modules/audigentRtdProvider.js deleted file mode 100644 index 0f32c84962f..00000000000 --- a/modules/audigentRtdProvider.js +++ /dev/null @@ -1,141 +0,0 @@ -/** - * This module adds audigent provider to the real time data module - * The {@link module:modules/realTimeData} module is required - * The module will fetch segments from audigent server - * @module modules/audigentRtdProvider - * @requires module:modules/realTimeData - */ - -/** - * @typedef {Object} ModuleParams - * @property {string} siteKey - * @property {string} pubKey - * @property {string} url - * @property {?string} keyName - * @property {number} auctionDelay - */ - -import {config} from '../src/config.js'; -import {getGlobal} from '../src/prebidGlobal.js'; -import * as utils from '../src/utils.js'; -import {submodule} from '../src/hook.js'; -import {ajax} from '../src/ajax.js'; -import { getStorageManager } from '../src/storageManager.js'; - -const storage = getStorageManager(); - -/** @type {string} */ -const MODULE_NAME = 'realTimeData'; - -/** @type {ModuleParams} */ -let _moduleParams = {}; - -/** - * XMLHttpRequest to get data form audigent server - * @param {string} url server url with query params - */ - -export function setData(data) { - storage.setDataInLocalStorage('__adgntseg', JSON.stringify(data)); -} - -function getSegments(adUnits, onDone) { - try { - let jsonData = storage.getDataFromLocalStorage('__adgntseg'); - if (jsonData) { - let data = JSON.parse(jsonData); - if (data.audigent_segments) { - let dataToReturn = adUnits.reduce((rp, cau) => { - const adUnitCode = cau && cau.code; - if (!adUnitCode) { return rp } - rp[adUnitCode] = data; - return rp; - }, {}); - - onDone(dataToReturn); - return; - } - } - getSegmentsAsync(adUnits, onDone); - } catch (e) { - getSegmentsAsync(adUnits, onDone); - } -} - -function getSegmentsAsync(adUnits, onDone) { - const userIds = (getGlobal()).getUserIds(); - let tdid = null; - - if (userIds && userIds['tdid']) { - tdid = userIds['tdid']; - } else { - onDone({}); - } - - const url = `https://seg.ad.gt/api/v1/rtb_segments?tdid=${tdid}`; - - ajax(url, { - success: function (response, req) { - if (req.status === 200) { - try { - const data = JSON.parse(response); - if (data && data.audigent_segments) { - setData(data); - let dataToReturn = adUnits.reduce((rp, cau) => { - const adUnitCode = cau && cau.code; - if (!adUnitCode) { return rp } - rp[adUnitCode] = data; - return rp; - }, {}); - - onDone(dataToReturn); - } else { - onDone({}); - } - } catch (err) { - utils.logError('unable to parse audigent segment data'); - onDone({}) - } - } else if (req.status === 204) { - // unrecognized site key - onDone({}); - } - }, - error: function () { - onDone({}); - utils.logError('unable to get audigent segment data'); - } - } - ); -} - -/** @type {RtdSubmodule} */ -export const audigentSubmodule = { - /** - * used to link submodule with realTimeData - * @type {string} - */ - name: 'audigent', - /** - * get data and send back to realTimeData module - * @function - * @param {adUnit[]} adUnits - * @param {function} onDone - */ - getData: getSegments -}; - -export function init(config) { - const confListener = config.getConfig(MODULE_NAME, ({realTimeData}) => { - try { - _moduleParams = realTimeData.dataProviders && realTimeData.dataProviders.filter(pr => pr.name && pr.name.toLowerCase() === 'audigent')[0].params; - _moduleParams.auctionDelay = realTimeData.auctionDelay; - } catch (e) { - _moduleParams = {}; - } - confListener(); - }); -} - -submodule('realTimeData', audigentSubmodule); -init(config); diff --git a/modules/audigentRtdProvider.md b/modules/audigentRtdProvider.md deleted file mode 100644 index 47bcbbbf951..00000000000 --- a/modules/audigentRtdProvider.md +++ /dev/null @@ -1,52 +0,0 @@ -Audigent is a next-generation data management platform and a first-of-a-kind -"data agency" containing some of the most exclusive content-consuming audiences -across desktop, mobile and social platforms. - -This real-time data module provides first-party Audigent segments that can be -attached to bid request objects destined for different SSPs in order to optimize -targeting. Audigent maintains a large database of first-party Tradedesk Unified -ID to third party segment mappings that can now be queried at bid-time. - -Usage: - -Compile the audigent RTD module into your Prebid build: - -`gulp build --modules=userId,unifiedIdSystem,rtdModule,audigentRtdProvider,rubiconBidAdapter` - -Audigent segments will then be attached to each bid request objects in -`bid.realTimeData.audigent_segments` - -The format of the segments is a per-SSP mapping: - -``` -{ - 'appnexus': ['anseg1', 'anseg2'], - 'google': ['gseg1', 'gseg2'] -} -``` - -If a given SSP's API backend supports segment fields, they can then be -attached prior to the bid request being sent: - -``` -pbjs.requestBids({bidsBackHandler: addAudigentSegments}); - -function addAudigentSegments() { - for (i = 0; i < adUnits.length; i++) { - let adUnit = adUnits[i]; - for (j = 0; j < adUnit.bids.length; j++) { - adUnit.bids[j].userId.lipb.segments = adUnit.bids[j].realTimeData.audigent_segments['rubicon']; - } - } -} -``` - -To view an example of the segments returned by Audigent's backends: - -`gulp serve --modules=userId,unifiedIdSystem,rtdModule,audigentRtdProvider,rubiconBidAdapter` - -and then point your browser at: - -`http://localhost:9999/integrationExamples/gpt/audigentSegments_example.html` - - diff --git a/modules/automatadBidAdapter.js b/modules/automatadBidAdapter.js index 414cadcd405..415c52ba6d3 100644 --- a/modules/automatadBidAdapter.js +++ b/modules/automatadBidAdapter.js @@ -27,12 +27,12 @@ export const spec = { } const siteId = validBidRequests[0].params.siteId - const placementId = validBidRequests[0].params.placementId const impressions = validBidRequests.map(bidRequest => { return { id: bidRequest.bidId, adUnitCode: bidRequest.adUnitCode, + placement: bidRequest.params.placementId, banner: { format: bidRequest.sizes.map(sizeArr => ({ w: sizeArr[0], @@ -48,7 +48,6 @@ export const spec = { imp: impressions, site: { id: siteId, - placement: placementId, domain: window.location.hostname, page: window.location.href, ref: bidderRequest.refererInfo ? bidderRequest.refererInfo.referer || null : null, @@ -72,20 +71,24 @@ export const spec = { const bidResponses = [] const response = (serverResponse || {}).body - if (response && response.seatbid && response.seatbid.length === 1 && response.seatbid[0].bid && response.seatbid[0].bid.length) { - response.seatbid[0].bid.forEach(bid => { - bidResponses.push({ - requestId: bid.impid, - cpm: bid.price, - ad: bid.adm, - adDomain: bid.adomain[0], - currency: DEFAULT_CURRENCY, - ttl: DEFAULT_BID_TTL, - creativeId: bid.crid, - width: bid.w, - height: bid.h, - netRevenue: DEFAULT_NET_REVENUE, - nurl: bid.nurl, + if (response && response.seatbid && response.seatbid[0].bid && response.seatbid[0].bid.length) { + response.seatbid.forEach(bidObj => { + bidObj.bid.forEach(bid => { + bidResponses.push({ + requestId: bid.impid, + cpm: bid.price, + ad: bid.adm, + meta: { + advertiserDomains: bid.adomain + }, + currency: DEFAULT_CURRENCY, + ttl: DEFAULT_BID_TTL, + creativeId: bid.crid, + width: bid.w, + height: bid.h, + netRevenue: DEFAULT_NET_REVENUE, + nurl: bid.nurl, + }) }) }) } else { diff --git a/modules/avocetBidAdapter.js b/modules/avocetBidAdapter.js index 1163ac830ba..1283bb865d4 100644 --- a/modules/avocetBidAdapter.js +++ b/modules/avocetBidAdapter.js @@ -53,7 +53,7 @@ export const spec = { const publisherDomain = config.getConfig('publisherDomain'); // First-party data from config - const fpd = config.getConfig('fpd'); + const fpd = config.getLegacyFpd(config.getConfig('ortb2')); // GDPR status and TCF consent string let tcfConsentString; @@ -77,8 +77,8 @@ export const spec = { // ID5 identifier let id5id; - if (bidRequests[0].userId && bidRequests[0].userId.id5id) { - id5id = bidRequests[0].userId.id5id; + if (bidRequests[0].userId && bidRequests[0].userId.id5id && bidRequests[0].userId.id5id.uid) { + id5id = bidRequests[0].userId.id5id.uid; } // Build the avocet ext object diff --git a/modules/axonixBidAdapter.js b/modules/axonixBidAdapter.js new file mode 100644 index 00000000000..daaac27e6a4 --- /dev/null +++ b/modules/axonixBidAdapter.js @@ -0,0 +1,185 @@ +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.2'; + +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 = window.navigator && (window.navigator.connection || window.navigator.mozConnection || window.navigator.webkitConnection) + let connectionType = 'unknown'; + let effectiveType = ''; + + if (connection) { + if (connection.type) { + connectionType = connection.type; + } + + if (connection.effectiveType) { + effectiveType = 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, + effectiveType, + 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) { + const response = serverResponse ? serverResponse.body : []; + + if (!utils.isArray(response)) { + return []; + } + + const responses = []; + + for (const resp of response) { + if (resp.requestId) { + responses.push(Object.assign(resp, { + 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(bid) { + const { nurl } = bid || {}; + + if (bid.nurl) { + utils.triggerPixel(utils.replaceAuctionPrice(nurl, bid.cpm)); + }; + } +} + +registerBidder(spec); diff --git a/modules/axonixBidAdapter.md b/modules/axonixBidAdapter.md new file mode 100644 index 00000000000..7a4606d5502 --- /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 12e78c684ad..da5f385b0da 100644 --- a/modules/beachfrontBidAdapter.js +++ b/modules/beachfrontBidAdapter.js @@ -6,17 +6,24 @@ 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.11'; +const ADAPTER_VERSION = '1.16'; const ADAPTER_NAME = 'BFIO_PREBID'; const OUTSTREAM = 'outstream'; +const CURRENCY = 'USD'; export const VIDEO_ENDPOINT = 'https://reachms.bfmio.com/bid.json?exchange_id='; export const BANNER_ENDPOINT = 'https://display.bfmio.com/prebid_display'; export const OUTSTREAM_SRC = 'https://player-cdn.beachfrontmedia.com/playerapi/loader/outstream.js'; -export const VIDEO_TARGETING = ['mimes', 'playbackmethod', 'maxduration', 'placement']; +export const VIDEO_TARGETING = ['mimes', 'playbackmethod', 'maxduration', 'placement', 'skip', 'skipmin', 'skipafter']; export const DEFAULT_MIMES = ['video/mp4', 'application/javascript']; +export const SUPPORTED_USER_IDS = [ + { key: 'tdid', source: 'adserver.org', rtiPartner: 'TDID', queryParam: 'tdid' }, + { key: 'idl_env', source: 'liveramp.com', rtiPartner: 'idl', queryParam: 'idl' }, + { key: 'uid2.id', source: 'uidapi.com', rtiPartner: 'UID2', queryParam: 'uid2' } +]; + let appId = ''; export const spec = { @@ -56,28 +63,37 @@ export const spec = { response = response.body; if (isVideoBid(bidRequest)) { - if (!response || !response.url || !response.bidPrice) { + if (!response || !response.bidPrice) { utils.logWarn(`No valid video bids from ${spec.code} bidder`); return []; } let sizes = getVideoSizes(bidRequest); let firstSize = getFirstSize(sizes); let context = utils.deepAccess(bidRequest, 'mediaTypes.video.context'); - return { + let responseType = getVideoBidParam(bidRequest, 'responseType') || 'both'; + let bidResponse = { requestId: bidRequest.bidId, bidderCode: spec.code, - vastUrl: response.url, - vastXml: response.vast, cpm: response.bidPrice, width: firstSize.w, height: firstSize.h, creativeId: response.crid || response.cmpId, renderer: context === OUTSTREAM ? createRenderer(bidRequest) : null, mediaType: VIDEO, - currency: 'USD', + currency: CURRENCY, netRevenue: true, ttl: 300 }; + + if (responseType === 'nurl' || responseType === 'both') { + bidResponse.vastUrl = response.url; + } + + if (responseType === 'adm' || responseType === 'both') { + bidResponse.vastXml = response.vast; + } + + return bidResponse; } else { if (!response || !response.length) { utils.logWarn(`No valid banner bids from ${spec.code} bidder`); @@ -96,7 +112,7 @@ export const spec = { width: bid.w, height: bid.h, mediaType: BANNER, - currency: 'USD', + currency: CURRENCY, netRevenue: true, ttl: 300 }; @@ -236,6 +252,16 @@ function getPlayerBidParam(bid, key, defaultValue) { return param === undefined ? defaultValue : param; } +function getBannerBidFloor(bid) { + let floorInfo = utils.isFn(bid.getFloor) ? bid.getFloor({ currency: CURRENCY, mediaType: 'banner', size: '*' }) : {}; + return floorInfo.floor || getBannerBidParam(bid, 'bidfloor'); +} + +function getVideoBidFloor(bid) { + let floorInfo = utils.isFn(bid.getFloor) ? bid.getFloor({ currency: CURRENCY, mediaType: 'video', size: '*' }) : {}; + return floorInfo.floor || getVideoBidParam(bid, 'bidfloor'); +} + function isVideoBidValid(bid) { return isVideoBid(bid) && getVideoBidParam(bid, 'appId') && getVideoBidParam(bid, 'bidfloor'); } @@ -257,13 +283,43 @@ function getTopWindowReferrer() { } } +function getEids(bid) { + return SUPPORTED_USER_IDS + .map(getUserId(bid)) + .filter(x => x); +} + +function getUserId(bid) { + return ({ key, source, rtiPartner }) => { + let id = utils.deepAccess(bid, `userId.${key}`); + return id ? formatEid(id, source, rtiPartner) : null; + }; +} + +function formatEid(id, source, rtiPartner) { + return { + source, + uids: [{ + id, + ext: { rtiPartner } + }] + }; +} + function getVideoTargetingParams(bid) { - return Object.keys(Object(bid.params.video)) - .filter(param => includes(VIDEO_TARGETING, param)) - .reduce((obj, param) => { - obj[ param ] = bid.params.video[ param ]; - return obj; - }, {}); + const result = {}; + const excludeProps = ['playerSize', 'context', 'w', 'h']; + Object.keys(Object(bid.mediaTypes.video)) + .filter(key => !includes(excludeProps, key)) + .forEach(key => { + result[ key ] = bid.mediaTypes.video[ key ]; + }); + Object.keys(Object(bid.params.video)) + .filter(key => includes(VIDEO_TARGETING, key)) + .forEach(key => { + result[ key ] = bid.params.video[ key ]; + }); + return result; } function createVideoRequestData(bid, bidderRequest) { @@ -271,9 +327,10 @@ function createVideoRequestData(bid, bidderRequest) { let firstSize = getFirstSize(sizes); let video = getVideoTargetingParams(bid); let appId = getVideoBidParam(bid, 'appId'); - let bidfloor = getVideoBidParam(bid, 'bidfloor'); + let bidfloor = getVideoBidFloor(bid); let tagid = getVideoBidParam(bid, 'tagid'); let topLocation = getTopWindowLocation(bidderRequest); + let eids = getEids(bid); let payload = { isPrebid: true, appId: appId, @@ -306,10 +363,13 @@ function createVideoRequestData(bid, bidderRequest) { regs: { ext: {} }, + source: { + ext: {} + }, user: { ext: {} }, - cur: ['USD'] + cur: [CURRENCY] }; if (bidderRequest && bidderRequest.uspConsent) { @@ -322,16 +382,12 @@ function createVideoRequestData(bid, bidderRequest) { payload.user.ext.consent = consentString; } - if (bid.userId && bid.userId.tdid) { - payload.user.ext.eids = [{ - source: 'adserver.org', - uids: [{ - id: bid.userId.tdid, - ext: { - rtiPartner: 'TDID' - } - }] - }]; + if (bid.schain) { + payload.source.ext.schain = bid.schain; + } + + if (eids.length > 0) { + payload.user.ext.eids = eids; } let connection = navigator.connection || navigator.webkitConnection; @@ -349,7 +405,8 @@ function createBannerRequestData(bids, bidderRequest) { return { slot: bid.adUnitCode, id: getBannerBidParam(bid, 'appId'), - bidfloor: getBannerBidParam(bid, 'bidfloor'), + bidfloor: getBannerBidFloor(bid), + tagid: getBannerBidParam(bid, 'tagid'), sizes: getBannerSizes(bid) }; }); @@ -378,10 +435,17 @@ function createBannerRequestData(bids, bidderRequest) { payload.gdprConsent = consentString; } - if (bids[0] && bids[0].userId && bids[0].userId.tdid) { - payload.tdid = bids[0].userId.tdid; + if (bids[0] && bids[0].schain) { + payload.schain = bids[0].schain; } + SUPPORTED_USER_IDS.forEach(({ key, queryParam }) => { + let id = utils.deepAccess(bids, `0.userId.${key}`) + if (id) { + payload[queryParam] = id; + } + }); + return payload; } diff --git a/modules/beachfrontBidAdapter.md b/modules/beachfrontBidAdapter.md index 0a6b8b73da4..9de415f8fc5 100644 --- a/modules/beachfrontBidAdapter.md +++ b/modules/beachfrontBidAdapter.md @@ -4,7 +4,7 @@ Module Name: Beachfront Bid Adapter Module Type: Bidder Adapter -Maintainer: john@beachfront.com +Maintainer: prebid@beachfront.com # Description @@ -18,7 +18,8 @@ Module that connects to Beachfront's demand sources mediaTypes: { video: { context: 'instream', - playerSize: [ 640, 360 ] + playerSize: [640, 360], + mimes: ['video/mp4', 'application/javascript'] } }, bids: [ @@ -26,10 +27,7 @@ Module that connects to Beachfront's demand sources bidder: 'beachfront', params: { bidfloor: 0.01, - appId: '11bc5dd5-7421-4dd8-c926-40fa653bec76', - video: { - mimes: [ 'video/mp4', 'application/javascript' ] - } + appId: '11bc5dd5-7421-4dd8-c926-40fa653bec76' } } ] @@ -37,7 +35,7 @@ Module that connects to Beachfront's demand sources code: 'test-banner', mediaTypes: { banner: { - sizes: [ 300, 250 ] + sizes: [300, 250] } }, bids: [ @@ -61,10 +59,11 @@ Module that connects to Beachfront's demand sources mediaTypes: { video: { context: 'outstream', - playerSize: [ 640, 360 ] + playerSize: [640, 360], + mimes: ['video/mp4', 'application/javascript'] }, banner: { - sizes: [ 300, 250 ] + sizes: [300, 250] } }, bids: [ @@ -74,7 +73,6 @@ Module that connects to Beachfront's demand sources video: { bidfloor: 0.01, appId: '11bc5dd5-7421-4dd8-c926-40fa653bec76', - mimes: [ 'video/mp4', 'application/javascript' ] }, banner: { bidfloor: 0.01, @@ -95,7 +93,8 @@ Module that connects to Beachfront's demand sources mediaTypes: { video: { context: 'outstream', - playerSize: [ 640, 360 ] + playerSize: [ 640, 360 ], + mimes: ['video/mp4', 'application/javascript'] } }, bids: [ @@ -104,8 +103,7 @@ Module that connects to Beachfront's demand sources params: { video: { bidfloor: 0.01, - appId: '11bc5dd5-7421-4dd8-c926-40fa653bec76', - mimes: [ 'video/mp4', 'application/javascript' ] + appId: '11bc5dd5-7421-4dd8-c926-40fa653bec76' }, player: { progressColor: '#50A8FA', diff --git a/modules/betweenBidAdapter.js b/modules/betweenBidAdapter.js index c435a5a993e..5a351def958 100644 --- a/modules/betweenBidAdapter.js +++ b/modules/betweenBidAdapter.js @@ -1,6 +1,9 @@ import {registerBidder} from '../src/adapters/bidderFactory.js'; -import { getAdUnitSizes, parseSizesInput } from '../src/utils.js'; +import { getAdUnitSizes, parseSizesInput, deepAccess } 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, @@ -24,15 +27,18 @@ export const spec = { buildRequests: function(validBidRequests, bidderRequest) { let requests = []; const gdprConsent = bidderRequest && bidderRequest.gdprConsent; + const refInfo = getRefererInfo(); validBidRequests.forEach(i => { let params = { - sizes: parseSizesInput(getAdUnitSizes(i)).join('%2C'), + sizes: parseSizesInput(getAdUnitSizes(i)), jst: 'hb', ord: Math.random() * 10000000000000000, tz: getTz(), fl: getFl(), rr: getRr(), + shid: getSharedId(i)('id'), + shid3: getSharedId(i)('third'), s: i.params.s, bidid: i.bidId, transactionid: i.transactionId, @@ -56,6 +62,12 @@ export const spec = { } } + if (i.schain) { + params.schain = encodeToBase64WebSafe(JSON.stringify(i.schain)); + } + + if (refInfo && refInfo.referer) params.ref = refInfo.referer; + if (gdprConsent) { if (typeof gdprConsent.gdprApplies !== 'undefined') { params.gdprApplies = !!gdprConsent.gdprApplies; @@ -65,9 +77,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. @@ -87,7 +104,10 @@ export const spec = { creativeId: serverResponse.body[i].creativeid, currency: serverResponse.body[i].currency || 'RUB', netRevenue: serverResponse.body[i].netRevenue || true, - ad: serverResponse.body[i].ad + ad: serverResponse.body[i].ad, + meta: { + advertiserDomains: serverResponse.body[i].adomain ? serverResponse.body[i].adomain : [] + } }; bidResponses.push(bidResponse); } @@ -129,6 +149,15 @@ export const spec = { } } +function getSharedId(bid) { + const id = deepAccess(bid, 'userId.sharedid.id'); + const third = deepAccess(bid, 'userId.sharedid.third'); + return function(kind) { + if (kind === 'id') return id || ''; + return third || ''; + } +} + function getRr() { try { var td = top.document; @@ -161,6 +190,10 @@ function getTz() { return new Date().getTimezoneOffset(); } +function encodeToBase64WebSafe(string) { + return btoa(string).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, ''); +} + /* function get_pubdata(adds) { if (adds !== undefined && adds.pubdata !== undefined) { 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/bidglassBidAdapter.js b/modules/bidglassBidAdapter.js index 6db35f184ca..44f5cdf4384 100644 --- a/modules/bidglassBidAdapter.js +++ b/modules/bidglassBidAdapter.js @@ -46,7 +46,7 @@ export const spec = { return window === window.top ? window.location.href : window.parent === window.top ? document.referrer : null; }; let getOrigins = function() { - var ori = ['https://' + window.location.hostname]; + var ori = [window.location.protocol + '//' + window.location.hostname]; if (window.location.ancestorOrigins) { for (var i = 0; i < window.location.ancestorOrigins.length; i++) { @@ -56,7 +56,7 @@ export const spec = { // Derive the parent origin var parts = document.referrer.split('/'); - ori.push('https://' + parts[2]); + ori.push(parts[0] + '//' + parts[2]); if (window.parent !== window.top) { // Additional unknown origins exist @@ -67,15 +67,30 @@ export const spec = { return ori; }; + let bidglass = window['bidglass']; + utils._each(validBidRequests, function(bid) { bid.sizes = ((utils.isArray(bid.sizes) && utils.isArray(bid.sizes[0])) ? bid.sizes : [bid.sizes]); bid.sizes = bid.sizes.filter(size => utils.isArray(size)); - // Stuff to send: [bid id, sizes, adUnitId] + var adUnitId = utils.getBidIdParameter('adUnitId', bid.params); + var options = utils.deepClone(bid.params); + + delete options.adUnitId; + + // Merge externally set targeting params + if (typeof bidglass === 'object' && bidglass.getTargeting) { + let targeting = bidglass.getTargeting(adUnitId, options.targeting); + + if (targeting && Object.keys(targeting).length > 0) options.targeting = targeting; + } + + // Stuff to send: [bid id, sizes, adUnitId, options] imps.push({ bidId: bid.bidId, sizes: bid.sizes, - adUnitId: utils.getBidIdParameter('adUnitId', bid.params) + adUnitId: adUnitId, + options: options }); }); diff --git a/modules/bizzclickBidAdapter.js b/modules/bizzclickBidAdapter.js new file mode 100644 index 00000000000..2af9a7afed2 --- /dev/null +++ b/modules/bizzclickBidAdapter.js @@ -0,0 +1,326 @@ +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 = 'bizzclick'; +const ACCOUNTID_MACROS = '[account_id]'; +const URL_ENDPOINT = `https://us-e-node1.bizzclick.com/bid?rtb_seat_id=prebidjs&secret_key=${ACCOUNTID_MACROS}`; +const NATIVE_ASSET_IDS = { 0: 'title', 2: 'icon', 3: 'image', 5: 'sponsoredBy', 4: 'body', 1: 'cta' }; +const NATIVE_PARAMS = { + title: { + id: 0, + name: 'title' + }, + icon: { + id: 2, + type: 1, + name: 'img' + }, + image: { + id: 3, + type: 3, + name: 'img' + }, + 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, + at: 1, + cur: ['USD'], + device: { + w: winTop.screen.width, + h: winTop.screen.height, + dnt: utils.getDNT() ? 1 : 0, + language: (navigator && navigator.language) ? navigator.language.indexOf('-') != -1 ? navigator.language.split('-')[0] : navigator.language : '', + }, + site: { + page: location.pathname, + host: location.host + }, + source: { + tid: bidRequest.transactionId + }, + regs: { + coppa: config.getConfig('coppa') === true ? 1 : 0, + ext: {} + }, + user: { + ext: {} + }, + tmax: bidRequest.timeout, + imp: [impObject], + }; + if (bidRequest) { + if (bidRequest.gdprConsent && bidRequest.gdprConsent.gdprApplies) { + utils.deepSetValue(data, 'regs.ext.gdpr', bidRequest.gdprConsent.gdprApplies ? 1 : 0); + utils.deepSetValue(data, 'user.ext.consent', bidRequest.gdprConsent.consentString); + } + + if (bidRequest.uspConsent !== undefined) { + utils.deepSetValue(data, 'regs.ext.us_privacy', bidRequest.uspConsent); + } + } + 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 bizzclickResponse = serverResponse.body; + + let bids = []; + for (let response of bizzclickResponse) { + let mediaType = response.seatbid[0].bid[0].ext && response.seatbid[0].bid[0].ext.mediaType ? response.seatbid[0].bid[0].ext.mediaType : BANNER; + + let bid = { + requestId: response.id, + cpm: response.seatbid[0].bid[0].price, + width: response.seatbid[0].bid[0].w, + height: response.seatbid[0].bid[0].h, + ttl: response.ttl || 1200, + currency: response.cur || 'USD', + netRevenue: true, + creativeId: response.seatbid[0].bid[0].crid, + dealId: response.seatbid[0].bid[0].dealid, + mediaType: mediaType + }; + + 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/bizzclickBidAdapter.md b/modules/bizzclickBidAdapter.md index 7dfa458b34c..6fc1bebf546 100644 --- a/modules/bizzclickBidAdapter.md +++ b/modules/bizzclickBidAdapter.md @@ -14,14 +14,91 @@ Module that connects to BizzClick SSP demand sources ``` var adUnits = [{ code: 'placementId', - sizes: [[300, 250]], + mediaTypes: { + banner: { + sizes: [[300, 250], [300,600]] + } + }, bids: [{ bidder: 'bizzclick', params: { - placementId: 0, - type: 'banner' + placementId: 'hash', + accountId: 'accountId' } }] + }, + { + code: 'native_example', + // sizes: [[1, 1]], + mediaTypes: { + native: { + title: { + required: true, + len: 800 + }, + image: { + required: true, + len: 80 + }, + sponsoredBy: { + required: true + }, + clickUrl: { + required: true + }, + privacyLink: { + required: false + }, + body: { + required: true + }, + icon: { + required: true, + sizes: [50, 50] + } + } + + }, + bids: [ { + bidder: 'bizzclick', + params: { + placementId: 'hash', + accountId: 'accountId' + } + }] + }, + { + code: 'video1', + sizes: [640,480], + mediaTypes: { video: { + minduration:0, + maxduration:999, + boxingallowed:1, + skip:0, + mimes:[ + 'application/javascript', + 'video/mp4' + ], + w:1920, + h:1080, + protocols:[ + 2 + ], + linearity:1, + api:[ + 1, + 2 + ] + } }, + bids: [ + { + bidder: 'bizzclick', + params: { + placementId: 'hash', + accountId: 'accountId' } + } + ] + } ]; ``` \ No newline at end of file diff --git a/modules/bluebillywigBidAdapter.js b/modules/bluebillywigBidAdapter.js index 4d40d931e1d..cac993b0f8c 100644 --- a/modules/bluebillywigBidAdapter.js +++ b/modules/bluebillywigBidAdapter.js @@ -1,4 +1,5 @@ import * as utils from '../src/utils.js'; +import find from 'core-js-pure/features/array/find.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; import { VIDEO } from '../src/mediaTypes.js'; import { config } from '../src/config.js'; @@ -17,17 +18,18 @@ const BB_CONSTANTS = { DEFAULT_TTL: 300, DEFAULT_WIDTH: 768, DEFAULT_HEIGHT: 432, - DEFAULT_NET_REVENUE: true + DEFAULT_NET_REVENUE: true, + VIDEO_PARAMS: ['mimes', 'minduration', 'maxduration', 'protocols', 'w', 'h', 'startdelay', 'placement', 'linearity', 'skip', 'skipmin', + 'skipafter', 'sequence', 'battr', 'maxextended', 'minbitrate', 'maxbitrate', 'boxingallowed', 'playbackmethod', 'playbackend', 'delivery', 'pos', 'companionad', + 'api', 'companiontype', 'ext'] }; // Aliasing const getConfig = config.getConfig; // Helper Functions -export const BB_HELPERS = { +const BB_HELPERS = { addSiteAppDevice: function(request, pageUrl) { - if (!request) return; - if (typeof getConfig('app') === 'object') request.app = getConfig('app'); else { request.site = {}; @@ -41,21 +43,15 @@ export const BB_HELPERS = { if (!request.device.h) request.device.h = window.innerHeight; }, addSchain: function(request, validBidRequests) { - if (!request) return; - const schain = utils.deepAccess(validBidRequests, '0.schain'); if (schain) request.source.ext = { schain: schain }; }, addCurrency: function(request) { - if (!request) return; - const adServerCur = getConfig('currency.adServerCurrency'); if (adServerCur && typeof adServerCur === 'string') request.cur = [adServerCur]; else if (Array.isArray(adServerCur) && adServerCur.length) request.cur = [adServerCur[0]]; }, addUserIds: function(request, validBidRequests) { - if (!request) return; - const bidUserId = utils.deepAccess(validBidRequests, '0.userId'); const eids = createEidsArray(bidUserId); @@ -63,10 +59,6 @@ export const BB_HELPERS = { utils.deepSetValue(request, 'user.ext.eids', eids); } }, - addDigiTrust: function(request, bidRequests) { - const digiTrust = BB_HELPERS.getDigiTrustParams(bidRequests && bidRequests[0]); - if (digiTrust) utils.deepSetValue(request, 'user.ext.digitrust', digiTrust); - }, substituteUrl: function (url, publication, renderer) { return url.replace('$$URL_START', (DEV_MODE) ? 'https://dev.' : 'https://').replace('$$PUBLICATION', publication).replace('$$RENDERER', renderer); }, @@ -79,41 +71,59 @@ export const BB_HELPERS = { getRendererUrl: function(publication, renderer) { return BB_HELPERS.substituteUrl(BB_CONSTANTS.RENDERER_URL, publication, renderer); }, - getDigiTrustParams: function(bidRequest) { - const digiTrustId = BB_HELPERS.getDigiTrustId(bidRequest); + transformVideoParams: function(videoParams, videoParamsExt) { + videoParams = utils.deepClone(videoParams); - if (!digiTrustId || (digiTrustId.privacy && digiTrustId.privacy.optout)) return null; - return { - id: digiTrustId.id, - keyv: digiTrustId.keyv - } - }, - getDigiTrustId: function(bidRequest) { - const bidRequestDigiTrust = utils.deepAccess(bidRequest, 'userId.digitrustid.data'); - if (bidRequestDigiTrust) return bidRequestDigiTrust; + let playerSize = videoParams.playerSize || [BB_CONSTANTS.DEFAULT_WIDTH, BB_CONSTANTS.DEFAULT_HEIGHT]; + if (Array.isArray(playerSize[0])) playerSize = playerSize[0]; + + videoParams.w = playerSize[0]; + videoParams.h = playerSize[1]; + videoParams.placement = 3; + + if (videoParamsExt) videoParams = Object.assign(videoParams, videoParamsExt); + + const videoParamsProperties = Object.keys(videoParams); + + videoParamsProperties.forEach(property => { + if (BB_CONSTANTS.VIDEO_PARAMS.indexOf(property) === -1) delete videoParams[property]; + }); - const digiTrustUser = getConfig('digiTrustId'); - return (digiTrustUser && digiTrustUser.success && digiTrustUser.identity) || null; + return videoParams; }, transformRTBToPrebidProps: function(bid, serverResponse) { - bid.cpm = bid.price; delete bid.price; - bid.bidId = bid.impid; - bid.requestId = bid.impid; delete bid.impid; - bid.width = bid.w || BB_CONSTANTS.DEFAULT_WIDTH; - bid.height = bid.h || BB_CONSTANTS.DEFAULT_HEIGHT; + const bidObject = { + cpm: bid.price, + currency: serverResponse.cur, + netRevenue: BB_CONSTANTS.DEFAULT_NET_REVENUE, + bidId: bid.impid, + requestId: bid.impid, + creativeId: bid.crid, + mediaType: VIDEO, + width: bid.w || BB_CONSTANTS.DEFAULT_WIDTH, + height: bid.h || BB_CONSTANTS.DEFAULT_HEIGHT, + ttl: BB_CONSTANTS.DEFAULT_TTL + }; + + const extPrebidTargeting = utils.deepAccess(bid, 'ext.prebid.targeting'); + const extPrebidCache = utils.deepAccess(bid, 'ext.prebid.cache'); + + if (extPrebidCache && typeof extPrebidCache.vastXml === 'object' && extPrebidCache.vastXml.cacheId && extPrebidCache.vastXml.url) { + bidObject.videoCacheKey = extPrebidCache.vastXml.cacheId; + bidObject.vastUrl = extPrebidCache.vastXml.url; + } else if (extPrebidTargeting && extPrebidTargeting.hb_uuid && extPrebidTargeting.hb_cache_host && extPrebidTargeting.hb_cache_path) { + bidObject.videoCacheKey = extPrebidTargeting.hb_uuid; + bidObject.vastUrl = `https://${extPrebidTargeting.hb_cache_host}${extPrebidTargeting.hb_cache_path}?uuid=${extPrebidTargeting.hb_uuid}`; + } if (bid.adm) { - bid.ad = bid.adm; - bid.vastXml = bid.adm; - delete bid.adm; + bidObject.ad = bid.adm; + bidObject.vastXml = bid.adm; } - if (bid.nurl && !bid.adm) { // ad markup is on win notice url, and adm is ommited according to OpenRTB 2.5 - bid.vastUrl = bid.nurl; - delete bid.nurl; + if (!bidObject.vastUrl && bid.nurl && !bid.adm) { // ad markup is on win notice url, and adm is ommited according to OpenRTB 2.5 + bidObject.vastUrl = bid.nurl; } - bid.netRevenue = BB_CONSTANTS.DEFAULT_NET_REVENUE; - bid.creativeId = bid.crid; delete bid.crid; - bid.currency = serverResponse.cur; - bid.ttl = BB_CONSTANTS.DEFAULT_TTL; + + return bidObject; }, }; @@ -132,20 +142,16 @@ const BB_RENDERER = { return; } - const rendererId = BB_RENDERER.getRendererId(bid.publicationName, bid.rendererCode); + if (!(window.bluebillywig && window.bluebillywig.renderers)) { + utils.logWarn(`${BB_CONSTANTS.BIDDER_CODE}: renderer code failed to initialize...`); + return; + } + const rendererId = BB_RENDERER.getRendererId(bid.publicationName, bid.rendererCode); const ele = document.getElementById(bid.adUnitCode); // NB convention + const renderer = find(window.bluebillywig.renderers, r => r._id === rendererId); - let renderer; - - for (let rendererIndex = 0; rendererIndex < window.bluebillywig.renderers.length; rendererIndex++) { - if (window.bluebillywig.renderers[rendererIndex]._id === rendererId) { - renderer = window.bluebillywig.renderers[rendererIndex]; - break; - } - } - - if (renderer) renderer.bootstrap(config, ele); + if (renderer) renderer.bootstrap(config, ele, bid.rendererSettings || {}); else utils.logWarn(`${BB_CONSTANTS.BIDDER_CODE}: Couldn't find a renderer with ${rendererId}`); }, newRenderer: function(rendererUrl, adUnitCode) { @@ -212,9 +218,8 @@ export const spec = { utils.logError(`${BB_CONSTANTS.BIDDER_CODE}: connections is not of type array. Rejecting bid: `, bid); return false; } else { - for (let connectionIndex = 0; connectionIndex < bid.params.connections.length; connectionIndex++) { - const connection = bid.params.connections[connectionIndex]; - if (!bid.params.hasOwnProperty(connection)) { + for (let i = 0; i < bid.params.connections.length; i++) { + if (!bid.params.hasOwnProperty(bid.params.connections[i])) { utils.logError(`${BB_CONSTANTS.BIDDER_CODE}: connection specified in params.connections, but not configured in params. Rejecting bid: `, bid); return false; } @@ -225,6 +230,16 @@ export const spec = { return false; } + if (bid.params.hasOwnProperty('video') && (bid.params.video === null || typeof bid.params.video !== 'object')) { + utils.logError(`${BB_CONSTANTS.BIDDER_CODE}: params.video must be of type object. Rejecting bid: `, bid); + return false; + } + + if (bid.params.hasOwnProperty('rendererSettings') && (bid.params.rendererSettings === null || typeof bid.params.rendererSettings !== 'object')) { + utils.logError(`${BB_CONSTANTS.BIDDER_CODE}: params.rendererSettings must be of type object. Rejecting bid: `, bid); + return false; + } + if (bid.hasOwnProperty('mediaTypes') && bid.mediaTypes.hasOwnProperty(VIDEO)) { if (!bid.mediaTypes[VIDEO].hasOwnProperty('context')) { utils.logError(`${BB_CONSTANTS.BIDDER_CODE}: no context specified in bid. Rejecting bid: `, bid); @@ -245,20 +260,21 @@ export const spec = { buildRequests(validBidRequests, bidderRequest) { const imps = []; - for (let validBidRequestIndex = 0; validBidRequestIndex < validBidRequests.length; validBidRequestIndex++) { - const validBidRequest = validBidRequests[validBidRequestIndex]; - const _this = this; + validBidRequests.forEach(validBidRequest => { + if (!this.syncStore.publicationName) this.syncStore.publicationName = validBidRequest.params.publicationName; + if (!this.syncStore.accountId) this.syncStore.accountId = validBidRequest.params.accountId; - const ext = validBidRequest.params.connections.reduce(function(extBuilder, connection) { + const ext = validBidRequest.params.connections.reduce((extBuilder, connection) => { extBuilder[connection] = validBidRequest.params[connection]; - if (_this.syncStore.bidders.indexOf(connection) === -1) _this.syncStore.bidders.push(connection); + if (this.syncStore.bidders.indexOf(connection) === -1) this.syncStore.bidders.push(connection); return extBuilder; }, {}); - imps.push({ id: validBidRequest.bidId, ext, secure: window.location.protocol === 'https' ? 1 : 0, video: utils.deepAccess(validBidRequest, 'mediaTypes.video') }); - } + const videoParams = BB_HELPERS.transformVideoParams(utils.deepAccess(validBidRequest, 'mediaTypes.video'), utils.deepAccess(validBidRequest, 'params.video')); + imps.push({ id: validBidRequest.bidId, ext, secure: window.location.protocol === 'https' ? 1 : 0, video: videoParams }); + }); const request = { id: bidderRequest.auctionId, @@ -293,7 +309,6 @@ export const spec = { BB_HELPERS.addSchain(request, validBidRequests); BB_HELPERS.addCurrency(request); BB_HELPERS.addUserIds(request, validBidRequests); - BB_HELPERS.addDigiTrust(request, validBidRequests); return { method: 'POST', @@ -311,74 +326,44 @@ export const spec = { const bids = []; - for (let seatbidIndex = 0; seatbidIndex < serverResponse.seatbid.length; seatbidIndex++) { - const seatbid = serverResponse.seatbid[seatbidIndex]; - if (!seatbid.bid || !Array.isArray(seatbid.bid)) continue; - for (let bidIndex = 0; bidIndex < seatbid.bid.length; bidIndex++) { - const bid = seatbid.bid[bidIndex]; - BB_HELPERS.transformRTBToPrebidProps(bid, serverResponse); - - let bidParams; - for (let bidderRequestBidsIndex = 0; bidderRequestBidsIndex < request.bidderRequest.bids.length; bidderRequestBidsIndex++) { - if (request.bidderRequest.bids[bidderRequestBidsIndex].bidId === bid.bidId) { - bidParams = request.bidderRequest.bids[bidderRequestBidsIndex].params; - } - } + serverResponse.seatbid.forEach(seatbid => { + if (!seatbid.bid || !Array.isArray(seatbid.bid)) return; + seatbid.bid.forEach(bid => { + bid = BB_HELPERS.transformRTBToPrebidProps(bid, serverResponse); - if (bidParams) { - bid.publicationName = bidParams.publicationName; - bid.rendererCode = bidParams.rendererCode; - bid.accountId = bidParams.accountId; - } + const bidParams = find(request.bidderRequest.bids, bidderRequestBid => bidderRequestBid.bidId === bid.bidId).params; + bid.publicationName = bidParams.publicationName; + bid.rendererCode = bidParams.rendererCode; + bid.accountId = bidParams.accountId; + bid.rendererSettings = bidParams.rendererSettings; const rendererUrl = BB_HELPERS.getRendererUrl(bid.publicationName, bid.rendererCode); - bid.renderer = BB_RENDERER.newRenderer(rendererUrl, bid.adUnitCode); bids.push(bid); - } - } + }); + }); return bids; }, getUserSyncs(syncOptions, serverResponses, gdpr) { - if (!serverResponses || !serverResponses.length) return []; if (!syncOptions.iframeEnabled) return []; const queryString = []; - let accountId; - let publication; - - const serverResponse = serverResponses[0]; - if (!serverResponse.body || !serverResponse.body.seatbid) return []; - - for (let seatbidIndex = 0; seatbidIndex < serverResponse.body.seatbid.length; seatbidIndex++) { - const seatbid = serverResponse.body.seatbid[seatbidIndex]; - for (let bidIndex = 0; bidIndex < seatbid.bid.length; bidIndex++) { - const bid = seatbid.bid[bidIndex]; - accountId = bid.accountId || null; - publication = bid.publicationName || null; - - if (publication && accountId) break; - } - if (publication && accountId) break; - } - - if (!publication || !accountId) return []; if (gdpr.gdprApplies) queryString.push(`gdpr=${gdpr.gdprApplies ? 1 : 0}`); if (gdpr.gdprApplies && gdpr.consentString) queryString.push(`gdpr_consent=${gdpr.consentString}`); if (this.syncStore.uspConsent) queryString.push(`usp_consent=${this.syncStore.uspConsent}`); - queryString.push(`accountId=${accountId}`); + queryString.push(`accountId=${this.syncStore.accountId}`); queryString.push(`bidders=${btoa(JSON.stringify(this.syncStore.bidders))}`); queryString.push(`cb=${Date.now()}-${Math.random().toString().replace('.', '')}`); if (DEV_MODE) queryString.push('bbpbs_debug=true'); // NB syncUrl by default starts with ?pub=$$PUBLICATION - const syncUrl = `${BB_HELPERS.getSyncUrl(publication)}&${queryString.join('&')}`; + const syncUrl = `${BB_HELPERS.getSyncUrl(this.syncStore.publicationName)}&${queryString.join('&')}`; return [{ type: 'iframe', diff --git a/modules/bridgewellBidAdapter.js b/modules/bridgewellBidAdapter.js index 0303e4f74bd..58c4e907328 100644 --- a/modules/bridgewellBidAdapter.js +++ b/modules/bridgewellBidAdapter.js @@ -5,7 +5,7 @@ import find from 'core-js-pure/features/array/find.js'; const BIDDER_CODE = 'bridgewell'; const REQUEST_ENDPOINT = 'https://prebid.scupio.com/recweb/prebid.aspx?cb=' + Math.random(); -const BIDDER_VERSION = '0.0.2'; +const BIDDER_VERSION = '0.0.3'; export const spec = { code: BIDDER_CODE, @@ -19,11 +19,13 @@ export const spec = { */ isBidRequestValid: function (bid) { let valid = false; - - if (bid && bid.params && bid.params.ChannelID) { - valid = true; + if (bid && bid.params) { + if ((bid.params.cid) && (typeof bid.params.cid === 'number')) { + valid = true; + } else if (bid.params.ChannelID) { + valid = true; + } } - return valid; }, @@ -36,15 +38,29 @@ export const spec = { buildRequests: function (validBidRequests, bidderRequest) { const adUnits = []; utils._each(validBidRequests, function (bid) { - adUnits.push({ - ChannelID: bid.params.ChannelID, - adUnitCode: bid.adUnitCode, - mediaTypes: bid.mediaTypes || { - banner: { - sizes: bid.sizes + if (bid.params.cid) { + adUnits.push({ + cid: bid.params.cid, + adUnitCode: bid.adUnitCode, + requestId: bid.bidId, + mediaTypes: bid.mediaTypes || { + banner: { + sizes: bid.sizes + } } - } - }); + }); + } else { + adUnits.push({ + ChannelID: bid.params.ChannelID, + adUnitCode: bid.adUnitCode, + requestId: bid.bidId, + mediaTypes: bid.mediaTypes || { + banner: { + sizes: bid.sizes + } + } + }); + } }); let topUrl = ''; @@ -63,7 +79,8 @@ export const spec = { inIframe: utils.inIframe(), url: topUrl, referrer: getTopWindowReferrer(), - adUnits: adUnits + adUnits: adUnits, + refererInfo: bidderRequest.refererInfo, }, validBidRequests: validBidRequests }; @@ -143,6 +160,10 @@ export const spec = { bidResponse.currency = matchedResponse.currency; bidResponse.mediaType = matchedResponse.mediaType; + if (matchedResponse.adomain) { + utils.deepSetValue(bidResponse, 'meta.advertiserDomains', Array.isArray(matchedResponse.adomain) ? matchedResponse.adomain : [matchedResponse.adomain]); + } + // check required parameters by matchedResponse.mediaType switch (matchedResponse.mediaType) { case BANNER: diff --git a/modules/bridgewellBidAdapter.md b/modules/bridgewellBidAdapter.md index 6bcab4b8820..97e11f6eaf9 100644 --- a/modules/bridgewellBidAdapter.md +++ b/modules/bridgewellBidAdapter.md @@ -20,7 +20,7 @@ Module that connects to Bridgewell demand source to fetch bids. bids: [{ bidder: 'bridgewell', params: { - ChannelID: 'CgUxMjMzOBIBNiIFcGVubnkqCQisAhD6ARoBOQ' + cid: 12345 } }] }, { @@ -33,7 +33,7 @@ Module that connects to Bridgewell demand source to fetch bids. bids: [{ bidder: 'bridgewell', params: { - ChannelID: 'CgUxMjMzOBIBNiIGcGVubnkzKggI2AUQWhoBOQ' + cid: 56789 } }] }, { @@ -70,7 +70,7 @@ Module that connects to Bridgewell demand source to fetch bids. bids: [{ bidder: 'bridgewell', params: { - ChannelID: 'CgUxMjMzOBIBNiIGcGVubnkzKggI2AUQWhoBOQ' + cid: 2394 } }] }]; diff --git a/modules/brightMountainMediaBidAdapter.js b/modules/brightMountainMediaBidAdapter.js index 5a285be71c0..5539004bdcd 100644 --- a/modules/brightMountainMediaBidAdapter.js +++ b/modules/brightMountainMediaBidAdapter.js @@ -1,13 +1,27 @@ import { registerBidder } from '../src/adapters/bidderFactory.js'; -import { BANNER, NATIVE, VIDEO } from '../src/mediaTypes.js'; +import { BANNER, VIDEO } from '../src/mediaTypes.js'; import * as utils from '../src/utils.js'; -const BIDDER_CODE = 'brightmountainmedia'; -const AD_URL = 'https://console.brightmountainmedia.com/hb/bid'; +const BIDDER_CODE = 'bmtm'; +const AD_URL = 'https://one.elitebidder.com/api/hb'; +const SYNC_URL = 'https://console.brightmountainmedia.com:8443/cookieSync'; + +const videoExt = [ + 'video/x-ms-wmv', + 'video/x-flv', + 'video/mp4', + 'video/3gpp', + 'application/x-mpegURL', + 'video/quicktime', + 'video/x-msvideo', + 'application/x-shockwave-flash', + 'application/javascript' +]; export const spec = { code: BIDDER_CODE, - supportedMediaTypes: [BANNER, VIDEO, NATIVE], + aliases: ['brightmountainmedia'], + supportedMediaTypes: [BANNER, VIDEO], isBidRequestValid: (bid) => { return Boolean(bid.bidId && bid.params && bid.params.placement_id); @@ -41,13 +55,38 @@ export const spec = { } for (let i = 0; i < validBidRequests.length; i++) { let bid = validBidRequests[i]; - let traff = bid.params.traffic || BANNER let placement = { placementId: bid.params.placement_id, bidId: bid.bidId, - sizes: bid.mediaTypes[traff].sizes, - traffic: traff }; + + if (bid.mediaTypes.hasOwnProperty(BANNER)) { + placement['traffic'] = BANNER; + if (bid.mediaTypes.banner.sizes) { + placement['sizes'] = bid.mediaTypes.banner.sizes; + } + } + + if (bid.mediaTypes.hasOwnProperty(VIDEO)) { + placement['traffic'] = VIDEO; + if (bid.mediaTypes.video.context) { + placement['context'] = bid.mediaTypes.video.context; + } + if (bid.mediaTypes.video.playerSize) { + placement['sizes'] = bid.mediaTypes.video.playerSize; + } + if (bid.mediaTypes.video.mimes && Array.isArray(bid.mediaTypes.video.mimes)) { + placement['mimes'] = bid.mediaTypes.video.mimes; + } else { + placement['mimes'] = videoExt; + } + if (bid.mediaTypes.video.skip != undefined) { + placement['skip'] = bid.mediaTypes.video.skip; + } + if (bid.mediaTypes.video.playbackmethod && Array.isArray(bid.mediaTypes.video.playbackmethod)) { + placement['playbackmethod'] = bid.mediaTypes.video.playbackmethod; + } + } if (bid.schain) { placement.schain = bid.schain; } @@ -61,25 +100,26 @@ export const spec = { }, interpretResponse: (serverResponse) => { - let response = []; - try { - serverResponse = serverResponse.body; - for (let i = 0; i < serverResponse.length; i++) { - let resItem = serverResponse[i]; - - response.push(resItem); + let bidResponse = []; + const response = serverResponse.body; + if (response && Array.isArray(response) && response.length > 0) { + for (let i = 0; i < response.length; i++) { + if (response[i].cpm > 0) { + if (response[i].mediaType && response[i].mediaType === 'video') { + response[i].vastXml = response[i].ad; + } + bidResponse.push(response[i]); + } } - } catch (e) { - utils.logMessage(e); - }; - return response; + } + return bidResponse; }, getUserSyncs: (syncOptions) => { if (syncOptions.iframeEnabled) { return [{ type: 'iframe', - url: 'https://console.brightmountainmedia.com:4444/cookieSync' + url: SYNC_URL }]; } }, diff --git a/modules/brightMountainMediaBidAdapter.md b/modules/brightMountainMediaBidAdapter.md index a9900b5264d..97cebe5b633 100644 --- a/modules/brightMountainMediaBidAdapter.md +++ b/modules/brightMountainMediaBidAdapter.md @@ -10,25 +10,102 @@ Maintainer: dev@brightmountainmedia.com Connects to Bright Mountain Media exchange for bids. -Bright Mountain Media bid adapter currently supports Banner. +Bright Mountain Media bid adapter currently supports Banner and Video. + +# Sample Ad Unit: For Publishers + +## Sample Banner only Ad Unit -# Test Parameters ``` - var adUnits = [ - code: 'placementid_0', - mediaTypes: { +var adUnits = [ + { + code: 'postbid_iframe', + mediaTypes: { banner: { - sizes: [[300, 250]] + sizes: [[300, 250]] } - }, - bids: [ + }, + bids: [ { - bidder: 'brightmountainmedia', - params: { - placement_id: '5f21784949be82079d08c', - traffic: 'banner' - } + "bidder": "bmtm", + "params": { + "placement_id": 1 + } } - ] + ] + } ]; -``` \ No newline at end of file +``` + +## Sample Video only Ad Unit: Outstream + +``` +var adUnits = [ + { + code: 'postbid_iframe_video', + mediaTypes: { + video: { + playerSize: [[640, 480]], + context: 'outstream' + ... // Additional ORTB video params + } + }, + bids: [ + { + bidder: "bmtm", + params: { + placement_id: 1 + } + } + ], + renderer: { + url: 'https://acdn.adnxs.com/video/outstream/ANOutstreamVideo.js', + render: function (bid) { + adResponse = { + ad: { + video: { + content: bid.vastXml, + player_height: bid.height, + player_width: bid.width + } + } + } + // push to render queue because ANOutstreamVideo may not be loaded yet. + bid.renderer.push(() => { + ANOutstreamVideo.renderAd({ + targetId: bid.adUnitCode, + adResponse: adResponse + }); + }); + } + } + } +]; + +``` + +## Sample Video only Ad Unit: Instream + +``` +var adUnits = [ + { + code: 'postbid_iframe_video', + mediaTypes: { + video: { + playerSize: [[640, 480]], + context: 'instream' + ... // Additional ORTB video params + }, + }, + bids: [ + { + bidder: "bmtm", + params: { + placement_id: 1 + } + } + ] + } +]; + +``` diff --git a/modules/britepoolIdSystem.js b/modules/britepoolIdSystem.js index 17a39e96aad..3bf416957d2 100644 --- a/modules/britepoolIdSystem.js +++ b/modules/britepoolIdSystem.js @@ -8,6 +8,7 @@ import * as utils from '../src/utils.js' import {ajax} from '../src/ajax.js'; import {submodule} from '../src/hook.js'; +const PIXEL = 'https://px.britepool.com/new?partner_id=t'; /** @type {Submodule} */ export const britepoolIdSubmodule = { @@ -28,10 +29,12 @@ export const britepoolIdSubmodule = { /** * Performs action to obtain id and return a value in the callback's response argument * @function - * @param {SubmoduleParams} [configParams] + * @param {SubmoduleConfig} [submoduleConfig] + * @param {ConsentData|undefined} consentData * @returns {function(callback:function)} */ - getId(submoduleConfigParams, consentData) { + getId(submoduleConfig, consentData) { + const submoduleConfigParams = (submoduleConfig && submoduleConfig.params) || {}; const { params, headers, url, getter, errors } = britepoolIdSubmodule.createParams(submoduleConfigParams, consentData); let getterResponse = null; if (typeof getter === 'function') { @@ -44,6 +47,9 @@ export const britepoolIdSubmodule = { }; } } + if (utils.isEmpty(params)) { + utils.triggerPixel(PIXEL); + } // Return for async operation return { callback: function(callback) { @@ -79,13 +85,17 @@ export const britepoolIdSubmodule = { }, /** * Helper method to create params for our API call - * @param {SubmoduleParams} [configParams] + * @param {SubmoduleParams} [submoduleConfigParams] + * @param {ConsentData|undefined} consentData * @returns {object} Object with parsed out params */ createParams(submoduleConfigParams, consentData) { + const hasGdprData = consentData && typeof consentData.gdprApplies === 'boolean' && consentData.gdprApplies; + const gdprConsentString = hasGdprData ? consentData.consentString : undefined; let errors = []; const headers = {}; - let params = Object.assign({}, submoduleConfigParams); + const dynamicVars = typeof britepool_pubparams !== 'undefined' ? britepool_pubparams : {}; // eslint-disable-line camelcase, no-undef + let params = Object.assign({}, submoduleConfigParams, dynamicVars); if (params.getter) { // Custom getter will not require other params if (typeof params.getter !== 'function') { @@ -98,7 +108,7 @@ export const britepoolIdSubmodule = { headers['x-api-key'] = params.api_key; } } - const url = params.url || 'https://api.britepool.com/v1/britepool/id'; + const url = params.url || `https://api.britepool.com/v1/britepool/id${gdprConsentString ? '?gdprString=' + encodeURIComponent(gdprConsentString) : ''}`; const getter = params.getter; delete params.api_key; delete params.url; 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.js b/modules/browsiRtdProvider.js index 9317786fb8d..4ee338e94cc 100644 --- a/modules/browsiRtdProvider.js +++ b/modules/browsiRtdProvider.js @@ -13,30 +13,23 @@ * @property {string} pubKey * @property {string} url * @property {?string} keyName - * @property {?number} auctionDelay - * @property {?number} timeout */ -import {config} from '../src/config.js'; import * as utils from '../src/utils.js'; import {submodule} from '../src/hook.js'; import {ajaxBuilder} from '../src/ajax.js'; import {loadExternalScript} from '../src/adloader.js'; -import { getStorageManager } from '../src/storageManager.js'; +import {getStorageManager} from '../src/storageManager.js'; import find from 'core-js-pure/features/array/find.js'; const storage = getStorageManager(); -/** @type {string} */ -const MODULE_NAME = 'realTimeData'; -/** @type {number} */ -const DEF_TIMEOUT = 1000; /** @type {ModuleParams} */ let _moduleParams = {}; /** @type {null|Object} */ -let _data = null; -/** @type {null | function} */ -let _dataReadyCallback = null; +let _predictionsData = null; +/** @type {string} */ +const DEF_KEYNAME = 'browsiViewability'; /** * add browsi script to page @@ -61,7 +54,7 @@ export function addBrowsiTag(data) { * collect required data from page * send data to browsi server to get predictions */ -function collectData() { +export function collectData() { const win = window.top; const doc = win.document; let browsiData = null; @@ -86,61 +79,33 @@ function collectData() { } export function setData(data) { - _data = data; - - if (typeof _dataReadyCallback === 'function') { - _dataReadyCallback(_data); - _dataReadyCallback = null; - } -} - -/** - * wait for data from server - * call callback when data is ready - * @param {function} callback - */ -function waitForData(callback) { - if (_data) { - _dataReadyCallback = null; - callback(_data); - } else { - _dataReadyCallback = callback; - } + _predictionsData = data; } -/** - * filter server data according to adUnits received - * call callback (onDone) when data is ready - * @param {adUnit[]} adUnits - * @param {function} onDone callback function - */ -function sendDataToModule(adUnits, onDone) { +function sendDataToModule(adUnitsCodes) { try { - waitForData(_predictionsData => { - const _predictions = _predictionsData.p; - if (!_predictions || !Object.keys(_predictions).length) { - return onDone({}); + const _predictions = (_predictionsData && _predictionsData.p) || {}; + return adUnitsCodes.reduce((rp, adUnitCode) => { + if (!adUnitCode) { + return rp } - let dataToReturn = adUnits.reduce((rp, cau) => { - const adUnitCode = cau && cau.code; - if (!adUnitCode) { return rp } - const adSlot = getSlotByCode(adUnitCode); - const identifier = adSlot ? getMacroId(_predictionsData.pmd, adSlot) : adUnitCode; - const predictionData = _predictions[identifier]; - if (!predictionData) { return rp } - - if (predictionData.p) { - if (!isIdMatchingAdUnit(adSlot, predictionData.w)) { - return rp; - } - rp[adUnitCode] = getKVObject(predictionData.p, _predictionsData.kn); + const adSlot = getSlotByCode(adUnitCode); + const identifier = adSlot ? getMacroId(_predictionsData['pmd'], adSlot) : adUnitCode; + const predictionData = _predictions[identifier]; + rp[adUnitCode] = getKVObject(-1, _predictionsData['kn']); + if (!predictionData) { + return rp + } + if (predictionData.p) { + if (!isIdMatchingAdUnit(adSlot, predictionData.w)) { + return rp; } - return rp; - }, {}); - return onDone(dataToReturn); - }); + rp[adUnitCode] = getKVObject(predictionData.p, _predictionsData.kn); + } + return rp; + }, {}); } catch (e) { - onDone({}); + return {}; } } @@ -160,7 +125,7 @@ function getAllSlots() { function getKVObject(p, keyName) { const prValue = p < 0 ? 'NA' : (Math.floor(p * 10) / 10).toFixed(2); let prObject = {}; - prObject[((_moduleParams['keyName'] || keyName).toString())] = prValue.toString(); + prObject[((_moduleParams['keyName'] || keyName || DEF_KEYNAME).toString())] = prValue.toString(); return prObject; } /** @@ -231,7 +196,7 @@ function evaluate(macro, divId, adUnit, replacer) { * @param {string} url server url with query params */ function getPredictionsFromServer(url) { - let ajax = ajaxBuilder(_moduleParams.auctionDelay || _moduleParams.timeout || DEF_TIMEOUT); + let ajax = ajaxBuilder(); ajax(url, { @@ -283,30 +248,23 @@ export const browsiSubmodule = { /** * get data and send back to realTimeData module * @function - * @param {adUnit[]} adUnits - * @param {function} onDone + * @param {string[]} adUnitsCodes */ - getData: sendDataToModule + getTargetingData: sendDataToModule, + init: init, }; -export function init(config) { - const confListener = config.getConfig(MODULE_NAME, ({realTimeData}) => { - try { - _moduleParams = realTimeData.dataProviders && realTimeData.dataProviders.filter( - pr => pr.name && pr.name.toLowerCase() === 'browsi')[0].params; - _moduleParams.auctionDelay = realTimeData.auctionDelay; - _moduleParams.timeout = realTimeData.timeout; - } catch (e) { - _moduleParams = {}; - } - if (_moduleParams.siteKey && _moduleParams.pubKey && _moduleParams.url) { - confListener(); - collectData(); - } else { - utils.logError('missing params for Browsi provider'); - } - }); +function init(moduleConfig) { + _moduleParams = moduleConfig.params; + if (_moduleParams && _moduleParams.siteKey && _moduleParams.pubKey && _moduleParams.url) { + collectData(); + } else { + utils.logError('missing params for Browsi provider'); + } + return true; } -submodule('realTimeData', browsiSubmodule); -init(config); +function registerSubModule() { + submodule('realTimeData', browsiSubmodule); +} +registerSubModule(); diff --git a/modules/browsiRtdProvider.md b/modules/browsiRtdProvider.md new file mode 100644 index 00000000000..9eed4b2b2d4 --- /dev/null +++ b/modules/browsiRtdProvider.md @@ -0,0 +1,55 @@ +# Overview + +The Browsi RTD module provides viewability predictions for ad slots on the page. +To use this module, you’ll need to work with [Browsi](https://gobrowsi.com/) to get an account and receive instructions on how to set up your pages and ad server. + +# Configurations + +Compile the Browsi RTD Provider into your Prebid build: + +`gulp build --modules=browsiRtdProvider` + + +Configuration example for using RTD module with `browsi` provider +```javascript + pbjs.setConfig({ + "realTimeData": { + "auctionDelay": 1000, + dataProviders:[{ + "name": "browsi", + "waitForIt": "true" + "params": { + "url": "yield-manager.browsiprod.com", + "siteKey": "browsidemo", + "pubKey": "browsidemo" + "keyName":"bv" + } + }] + } + }); +``` + +#Params + +Contact Browsi to get required params + +| param name | type |Scope | Description | +| :------------ | :------------ | :------- | :------- | +| url | string | required | Browsi server URL | +| siteKey | string | required | Site key | +| pubKey | string | required | Publisher key | +| keyName | string | optional | Ad unit targeting key | + + +#Output +`getTargetingData` function will return expected viewability prediction in the following structure: +```json +{ + "adUnitCode":{ + "browsiViewability":"0.6" + }, + "adUnitCode2":{ + "browsiViewability":"0.9" + } +} +``` diff --git a/modules/bucksenseBidAdapter.js b/modules/bucksenseBidAdapter.js index 3f327e62121..46d0dfe3590 100644 --- a/modules/bucksenseBidAdapter.js +++ b/modules/bucksenseBidAdapter.js @@ -90,6 +90,7 @@ export const spec = { var sCurrency = oResponse.currency || 'USD'; var bNetRevenue = oResponse.netRevenue || true; var sAd = oResponse.ad || ''; + var sAdomains = oResponse.adomains || []; if (request && sRequestID.length == 0) { utils.logInfo(WHO + ' interpretResponse() - use RequestID from Placments'); @@ -110,7 +111,10 @@ export const spec = { creativeId: sCreativeID, currency: sCurrency, netRevenue: bNetRevenue, - ad: sAd + ad: sAd, + meta: { + advertiserDomains: sAdomains + } }; bidResponses.push(bidResponse); } else { diff --git a/modules/ccxBidAdapter.js b/modules/ccxBidAdapter.js index ee15d6bb3ec..2160e539040 100644 --- a/modules/ccxBidAdapter.js +++ b/modules/ccxBidAdapter.js @@ -94,12 +94,12 @@ function _buildBid (bid) { } } - placement.video.protocols = utils.deepAccess(bid, 'params.video.protocols') || SUPPORTED_VIDEO_PROTOCOLS - placement.video.mimes = utils.deepAccess(bid, 'params.video.mimes') || SUPPORTED_VIDEO_MIMES - placement.video.playbackmethod = utils.deepAccess(bid, 'params.video.playbackmethod') || SUPPORTED_VIDEO_PLAYBACK_METHODS - placement.video.skip = utils.deepAccess(bid, 'params.video.skip') || 0 - if (placement.video.skip === 1 && utils.deepAccess(bid, 'params.video.skipafter')) { - placement.video.skipafter = utils.deepAccess(bid, 'params.video.skipafter') + placement.video.protocols = utils.deepAccess(bid, 'mediaTypes.video.protocols') || utils.deepAccess(bid, 'params.video.protocols') || SUPPORTED_VIDEO_PROTOCOLS + placement.video.mimes = utils.deepAccess(bid, 'mediaTypes.video.mimes') || utils.deepAccess(bid, 'params.video.mimes') || SUPPORTED_VIDEO_MIMES + placement.video.playbackmethod = utils.deepAccess(bid, 'mediaTypes.video.playbackmethod') || utils.deepAccess(bid, 'params.video.playbackmethod') || SUPPORTED_VIDEO_PLAYBACK_METHODS + placement.video.skip = utils.deepAccess(bid, 'mediaTypes.video.skip') || utils.deepAccess(bid, 'params.video.skip') || 0 + if (placement.video.skip === 1 && (utils.deepAccess(bid, 'mediaTypes.video.skipafter') || utils.deepAccess(bid, 'params.video.skipafter'))) { + placement.video.skipafter = utils.deepAccess(bid, 'mediaTypes.video.skipafter') || utils.deepAccess(bid, 'params.video.skipafter') } } @@ -120,6 +120,11 @@ function _buildResponse (bid, currency, ttl) { currency: currency } + resp.meta = {}; + if (bid.adomain && bid.adomain.length > 0) { + resp.meta.advertiserDomains = bid.adomain; + } + if (bid.ext.type === 'video') { resp.vastXml = bid.adm } else { diff --git a/modules/ccxBidAdapter.md b/modules/ccxBidAdapter.md index 740457dd099..7d86507bccb 100644 --- a/modules/ccxBidAdapter.md +++ b/modules/ccxBidAdapter.md @@ -32,67 +32,21 @@ Module that connects to Clickonometrics's demand sources mediaTypes: { video: { playerSize: [1920, 1080] - + protocols: [2, 3, 5, 6], //default + mimes: ["video/mp4", "video/x-flv"], //default + playbackmethod: [1, 2, 3, 4], //default + skip: 1, //default 0 + skipafter: 5 //delete this key if skip = 0 } }, bids: [ { bidder: "ccx", params: { - placementId: 3287742, - //following options are not required, default values will be used. Uncomment if you want to use custom values - /*video: { - //check OpenRTB documentation for following options description - protocols: [2, 3, 5, 6], //default - mimes: ["video/mp4", "video/x-flv"], //default - playbackmethod: [1, 2, 3, 4], //default - skip: 1, //default 0 - skipafter: 5 //delete this key if skip = 0 - }*/ + placementId: 3287742 } } ] } ]; - -# Pre 1.0 Support - - var adUnits = [ - { - code: 'test-banner', - mediaType: 'banner', - sizes: [300, 250], - bids: [ - { - bidder: "ccx", - params: { - placementId: 3286844 - } - } - ] - }, - { - code: 'test-video', - mediaType: 'video', - sizes: [1920, 1080] - bids: [ - { - bidder: "ccx", - params: { - placementId: 3287742, - //following options are not required, default values will be used. Uncomment if you want to use custom values - /*video: { - //check OpenRTB documentation for following options description - protocols: [2, 3, 5, 6], //default - mimes: ["video/mp4", "video/x-flv"], //default - playbackmethod: [1, 2, 3, 4], //default - skip: 1, //default 0 - skipafter: 5 //delete this key if skip = 0 - }*/ - } - } - ] - } - - ]; \ No newline at end of file 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/cointrafficBidAdapter.js b/modules/cointrafficBidAdapter.js new file mode 100644 index 00000000000..43f5cc01ccf --- /dev/null +++ b/modules/cointrafficBidAdapter.js @@ -0,0 +1,97 @@ +import * as utils from '../src/utils.js'; +import { registerBidder } from '../src/adapters/bidderFactory.js'; +import { BANNER } from '../src/mediaTypes.js' +import { config } from '../src/config.js' + +const BIDDER_CODE = 'cointraffic'; +const ENDPOINT_URL = 'https://appspb.cointraffic.io/pb/tmp'; +const DEFAULT_CURRENCY = 'EUR'; +const ALLOWED_CURRENCIES = [ + 'EUR', 'USD', 'JPY', 'BGN', 'CZK', 'DKK', 'GBP', 'HUF', 'PLN', 'RON', 'SEK', 'CHF', 'ISK', 'NOK', 'HRK', 'RUB', 'TRY', + 'AUD', 'BRL', 'CAD', 'CNY', 'HKD', 'IDR', 'ILS', 'INR', 'KRW', 'MXN', 'MYR', 'NZD', 'PHP', 'SGD', 'THB', 'ZAR', +]; + +export const spec = { + code: BIDDER_CODE, + supportedMediaTypes: [BANNER], + + /** + * Determines whether or not the given bid request is valid. + * + * @param {BidRequest} bid The bid params to validate. + * @return boolean True if this is a valid bid, and false otherwise. + */ + isBidRequestValid: function (bid) { + return !!(bid.params.placementId); + }, + + /** + * Make a server request from the list of BidRequests. + * + * @param validBidRequests + * @param bidderRequest + * @return Array Info describing the request to the server. + */ + buildRequests: function (validBidRequests, bidderRequest) { + return validBidRequests.map(bidRequest => { + const sizes = utils.parseSizesInput(bidRequest.params.size || bidRequest.sizes); + const currency = + config.getConfig(`currency.bidderCurrencyDefault.${BIDDER_CODE}`) || + config.getConfig('currency.adServerCurrency') || + DEFAULT_CURRENCY; + + if (ALLOWED_CURRENCIES.indexOf(currency) === -1) { + utils.logError('Currency is not supported - ' + currency); + return; + } + + const payload = { + placementId: bidRequest.params.placementId, + currency: currency, + sizes: sizes, + bidId: bidRequest.bidId, + referer: bidderRequest.refererInfo.referer, + }; + + return { + method: 'POST', + url: ENDPOINT_URL, + data: payload + }; + }); + }, + + /** + * Unpack the response from the server into a list of bids. + * + * @param {ServerResponse} serverResponse A successful response from the server. + * @param bidRequest + * @return {Bid[]} An array of bids which were nested inside the server. + */ + interpretResponse: function (serverResponse, bidRequest) { + const bidResponses = []; + const response = serverResponse.body; + + if (utils.isEmpty(response)) { + return bidResponses; + } + + const bidResponse = { + requestId: response.requestId, + cpm: response.cpm, + currency: response.currency, + netRevenue: response.netRevenue, + width: response.width, + height: response.height, + creativeId: response.creativeId, + ttl: response.ttl, + ad: response.ad + }; + + bidResponses.push(bidResponse); + + return bidResponses; + } +}; + +registerBidder(spec); diff --git a/modules/cointrafficBidAdapter.md b/modules/cointrafficBidAdapter.md new file mode 100644 index 00000000000..fcbcad1cad7 --- /dev/null +++ b/modules/cointrafficBidAdapter.md @@ -0,0 +1,31 @@ +# Overview + +``` +Module Name: Cointraffic Bidder Adapter +Module Type: Cointraffic Adapter +Maintainer: tech@cointraffic.io +``` + +# Description +The Cointraffic client module makes it easy to implement Cointraffic directly into your website. To get started, simply replace the ``placementId`` with your assigned tracker key. This is dependent on the size required by your account dashboard. +We support response in different currencies. Supported currencies listed [here](https://www.ecb.europa.eu/stats/policy_and_exchange_rates/euro_reference_exchange_rates/html/index.en.html). + +For additional information on this module, please contact us at ``support@cointraffic.io``. + +# Test Parameters +``` + var adUnits = [{ + code: 'test-ad-div', + mediaTypes: { + banner: { + sizes: [[300, 250]], + } + }, + bids: [{ + bidder: 'cointraffic', + params: { + placementId: 'testPlacementId' + } + }] + }]; +``` diff --git a/modules/colossussspBidAdapter.js b/modules/colossussspBidAdapter.js index baa60a76a0d..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,8 +87,22 @@ 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; } diff --git a/modules/concertAnalyticsAdapter.js b/modules/concertAnalyticsAdapter.js new file mode 100644 index 00000000000..a81d07e63b5 --- /dev/null +++ b/modules/concertAnalyticsAdapter.js @@ -0,0 +1,120 @@ +import {ajax} from '../src/ajax.js'; +import adapter from '../src/AnalyticsAdapter.js'; +import CONSTANTS from '../src/constants.json'; +import adapterManager from '../src/adapterManager.js'; +import * as utils from '../src/utils.js'; + +const analyticsType = 'endpoint'; + +// We only want to send about 1% of events for sampling purposes +const SAMPLE_RATE_PERCENTAGE = 1 / 100; +const pageIncludedInSample = sampleAnalytics(); + +const url = 'https://bids.concert.io/analytics'; + +const { + EVENTS: { + BID_RESPONSE, + BID_WON, + AUCTION_END + } +} = CONSTANTS; + +let queue = []; + +let concertAnalytics = Object.assign(adapter({url, analyticsType}), { + track({ eventType, args }) { + switch (eventType) { + case BID_RESPONSE: + if (args.bidder !== 'concert') break; + queue.push(mapBidEvent(eventType, args)); + break; + + case BID_WON: + if (args.bidder !== 'concert') break; + queue.push(mapBidEvent(eventType, args)); + break; + + case AUCTION_END: + // Set a delay, as BID_WON events will come after AUCTION_END events + setTimeout(() => sendEvents(), 3000); + break; + + default: + break; + } + } +}); + +function mapBidEvent(eventType, args) { + const { adId, auctionId, cpm, creativeId, width, height, timeToRespond } = args; + const [gamCreativeId, concertRequestId] = getConcertRequestId(creativeId); + + const payload = { + event: eventType, + concert_rid: concertRequestId, + adId, + auctionId, + creativeId: gamCreativeId, + position: args.adUnitCode, + url: window.location.href, + cpm, + width, + height, + timeToRespond + } + + return payload; +} + +/** + * In order to pass back the concert_rid from CBS, it is tucked into the `creativeId` + * slot in the bid response and combined with a pipe `|`. This method splits the creative ID + * and the concert_rid. + * + * @param {string} creativeId + */ +function getConcertRequestId(creativeId) { + if (!creativeId || creativeId.indexOf('|') < 0) return [null, null]; + + return creativeId.split('|'); +} + +function sampleAnalytics() { + return Math.random() <= SAMPLE_RATE_PERCENTAGE; +} + +function sendEvents() { + concertAnalytics.eventsStorage = queue; + + if (!queue.length) return; + + if (!pageIncludedInSample) { + utils.logMessage('Page not included in sample for Concert Analytics'); + return; + } + + try { + const body = JSON.stringify(queue); + ajax(url, () => queue = [], body, { + contentType: 'application/json', + method: 'POST' + }); + } catch (err) { utils.logMessage('Concert Analytics error') } +} + +// save the base class function +concertAnalytics.originEnableAnalytics = concertAnalytics.enableAnalytics; +concertAnalytics.eventsStorage = []; + +// override enableAnalytics so we can get access to the config passed in from the page +concertAnalytics.enableAnalytics = function (config) { + concertAnalytics.originEnableAnalytics(config); +}; + +adapterManager.registerAnalyticsAdapter({ + adapter: concertAnalytics, + code: 'concert' +}); + +export default concertAnalytics; diff --git a/modules/concertAnalyticsAdapter.md b/modules/concertAnalyticsAdapter.md new file mode 100644 index 00000000000..7c9b6d22703 --- /dev/null +++ b/modules/concertAnalyticsAdapter.md @@ -0,0 +1,11 @@ +# Overview + +``` +Module Name: Concert Analytics Adapter +Module Type: Analytics Adapter +Maintainer: support@concert.io +``` + +# Description + +Analytics adapter for concert. \ No newline at end of file diff --git a/modules/concertBidAdapter.js b/modules/concertBidAdapter.js new file mode 100644 index 00000000000..60634151aba --- /dev/null +++ b/modules/concertBidAdapter.js @@ -0,0 +1,212 @@ + +import * as utils from '../src/utils.js'; +import { registerBidder } from '../src/adapters/bidderFactory.js'; +import { getStorageManager } from '../src/storageManager.js' + +const BIDDER_CODE = 'concert'; +const CONCERT_ENDPOINT = 'https://bids.concert.io'; +const USER_SYNC_URL = 'https://cdn.concert.io/lib/bids/sync.html'; + +export const spec = { + code: BIDDER_CODE, + /** + * Determines whether or not the given bid request is valid. + * + * @param {BidRequest} bid The bid params to validate. + * @return boolean True if this is a valid bid, and false otherwise. + */ + isBidRequestValid: function(bid) { + if (!bid.params.partnerId) { + utils.logWarn('Missing partnerId bid parameter'); + return false; + } + + return true; + }, + + /** + * Make a server request from the list of BidRequests. + * + * @param {validBidRequests[]} - an array of bids + * @param {bidderRequest} - + * @return ServerRequest Info describing the request to the server. + */ + buildRequests: function(validBidRequests, bidderRequest) { + utils.logMessage(validBidRequests); + utils.logMessage(bidderRequest); + let payload = { + meta: { + prebidVersion: '$prebid.version$', + pageUrl: bidderRequest.refererInfo.referer, + screen: [window.screen.width, window.screen.height].join('x'), + debug: utils.debugTurnedOn(), + uid: getUid(bidderRequest), + optedOut: hasOptedOutOfPersonalization(), + adapterVersion: '1.1.1', + uspConsent: bidderRequest.uspConsent, + gdprConsent: bidderRequest.gdprConsent + } + } + + payload.slots = validBidRequests.map(bidRequest => { + let slot = { + name: bidRequest.adUnitCode, + bidId: bidRequest.bidId, + transactionId: bidRequest.transactionId, + sizes: bidRequest.params.sizes || bidRequest.sizes, + partnerId: bidRequest.params.partnerId, + slotType: bidRequest.params.slotType, + adSlot: bidRequest.params.slot || bidRequest.adUnitCode, + placementId: bidRequest.params.placementId || '', + site: bidRequest.params.site || bidderRequest.refererInfo.referer + } + + return slot; + }); + + utils.logMessage(payload); + + return { + method: 'POST', + url: `${CONCERT_ENDPOINT}/bids/prebid`, + 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) { + utils.logMessage(serverResponse); + utils.logMessage(bidRequest); + + const serverBody = serverResponse.body; + + if (!serverBody || typeof serverBody !== 'object') { + return []; + } + + let bidResponses = []; + + bidResponses = serverBody.bids.map(bid => { + return { + requestId: bid.bidId, + cpm: bid.cpm, + width: bid.width, + height: bid.height, + ad: bid.ad, + ttl: bid.ttl, + meta: { advertiserDomains: bid && bid.adomain ? bid.adomain : [] }, + creativeId: bid.creativeId, + netRevenue: bid.netRevenue, + currency: bid.currency + } + }); + + if (utils.debugTurnedOn() && serverBody.debug) { + utils.logMessage(`CONCERT`, serverBody.debug); + } + + utils.logMessage(bidResponses); + return bidResponses; + }, + + /** + * Register the user sync pixels which should be dropped after the auction. + * + * @param {SyncOptions} syncOptions Which user syncs are allowed? + * @param {ServerResponse[]} serverResponses List of server's responses. + * @param {gdprConsent} object GDPR consent object. + * @param {uspConsent} string US Privacy String. + * @return {UserSync[]} The user syncs which should be dropped. + */ + getUserSyncs: function(syncOptions, serverResponses, gdprConsent, uspConsent) { + const syncs = [] + if (syncOptions.iframeEnabled && !hasOptedOutOfPersonalization()) { + let params = []; + + if (gdprConsent && (typeof gdprConsent.gdprApplies === 'boolean')) { + params.push(`gdpr_applies=${gdprConsent.gdprApplies ? '1' : '0'}`); + } + if (gdprConsent && (typeof gdprConsent.consentString === 'string')) { + params.push(`gdpr_consent=${gdprConsent.consentString}`); + } + if (uspConsent && (typeof uspConsent === 'string')) { + params.push(`usp_consent=${uspConsent}`); + } + + syncs.push({ + type: 'iframe', + url: USER_SYNC_URL + (params.length > 0 ? `?${params.join('&')}` : '') + }); + } + return syncs; + }, + + /** + * Register bidder specific code, which will execute if bidder timed out after an auction + * @param {data} Containing timeout specific data + */ + onTimeout: function(data) { + utils.logMessage('concert bidder timed out'); + utils.logMessage(data); + }, + + /** + * 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.logMessage('concert bidder won bid'); + utils.logMessage(bid); + } + +} + +registerBidder(spec); + +const storage = getStorageManager(); + +/** + * Check or generate a UID for the current user. + */ +function getUid(bidderRequest) { + if (hasOptedOutOfPersonalization() || !consentAllowsPpid(bidderRequest)) { + return false; + } + + const CONCERT_UID_KEY = 'c_uid'; + + let uid = storage.getDataFromLocalStorage(CONCERT_UID_KEY); + + if (!uid) { + uid = utils.generateUUID(); + storage.setDataInLocalStorage(CONCERT_UID_KEY, uid); + } + + return uid; +} + +/** + * Whether the user has opted out of personalization. + */ +function hasOptedOutOfPersonalization() { + const CONCERT_NO_PERSONALIZATION_KEY = 'c_nap'; + + return storage.getDataFromLocalStorage(CONCERT_NO_PERSONALIZATION_KEY) === 'true'; +} + +/** + * Whether the privacy consent strings allow personalization. + * + * @param {BidderRequest} bidderRequest Object which contains any data consent signals + */ +function consentAllowsPpid(bidderRequest) { + /* NOTE: We cannot easily test GDPR consent, without the + * `consent-string` npm module; so will have to rely on that + * happening on the bid-server. */ + return !(bidderRequest.uspConsent === 'string' && + bidderRequest.uspConsent.toUpperCase().substring(0, 2) === '1YY') +} diff --git a/modules/concertBidAdapter.md b/modules/concertBidAdapter.md new file mode 100644 index 00000000000..d8736082e5c --- /dev/null +++ b/modules/concertBidAdapter.md @@ -0,0 +1,37 @@ +# Overview + +``` +Module Name: Concert Bid Adapter +Module Type: Bidder Adapter +Maintainer: support@concert.io +``` + +# Description + +Module that connects to Concert demand sources + +# Test Paramters +``` + var adUnits = [ + { + code: 'desktop_leaderboard_variable', + mediaTypes: { + banner: { + sizes: [[1030, 590]] + } + } + bids: [ + { + bidder: "concert", + params: { + partnerId: 'test_partner', + site: 'site_name', + placementId: 1234567, + slot: 'slot_name', + sizes: [[1030, 590]] + } + } + ] + } + ]; +``` diff --git a/modules/connectadBidAdapter.js b/modules/connectadBidAdapter.js index 04459ec1f09..4fa2a56a004 100644 --- a/modules/connectadBidAdapter.js +++ b/modules/connectadBidAdapter.js @@ -11,6 +11,7 @@ const SUPPORTED_MEDIA_TYPES = [BANNER]; export const spec = { code: BIDDER_CODE, + gvlid: 138, aliases: [ BIDDER_CODE_ALIAS ], supportedMediaTypes: SUPPORTED_MEDIA_TYPES, @@ -71,7 +72,7 @@ export const spec = { // EIDS Support if (validBidRequests[0].userId) { - data.user.ext.eids = createEidsArray(validBidRequests[0].userId); + utils.deepSetValue(data, 'user.ext.eids', createEidsArray(validBidRequests[0].userId)); } validBidRequests.map(bid => { @@ -81,8 +82,10 @@ export const spec = { pisze: bid.mediaTypes.banner.sizes[0] || bid.sizes[0], sizes: bid.mediaTypes.banner.sizes, adTypes: getSize(bid.mediaTypes.banner.sizes || bid.sizes), - floor: getBidFloor(bid) - }, bid.params); + bidfloor: getBidFloor(bid), + siteId: bid.params.siteId, + networkId: bid.params.networkId + }); if (placement.networkId && placement.siteId) { data.placements.push(placement); @@ -134,6 +137,13 @@ export const spec = { return bidResponses; }, + transformBidParams: function (params, isOpenRtb) { + return utils.convertTypes({ + 'siteId': 'number', + 'networkId': 'number' + }, params); + }, + getUserSyncs: function(syncOptions, serverResponses, gdprConsent, uspConsent) { let syncEndpoint = 'https://cdn.connectad.io/connectmyusers.php?'; @@ -227,7 +237,7 @@ function getBidFloor(bidRequest) { }); } - let floor = floorInfo.floor || 0; + let floor = floorInfo.floor || bidRequest.params.bidfloor || bidRequest.params.floorprice || 0; return floor; } diff --git a/modules/consentManagement.js b/modules/consentManagement.js index 90fe24735db..6a13e73b8a2 100644 --- a/modules/consentManagement.js +++ b/modules/consentManagement.js @@ -100,11 +100,7 @@ function lookupIabConsent(cmpSuccess, cmpError, hookConfig) { function v2CmpResponseCallback(tcfData, success) { utils.logInfo('Received a response from CMP', tcfData); if (success) { - if (tcfData.gdprApplies === false) { - cmpSuccess(tcfData, hookConfig); - } else if (tcfData.eventStatus === 'tcloaded' || tcfData.eventStatus === 'useractioncomplete') { - cmpSuccess(tcfData, hookConfig); - } else if (tcfData.eventStatus === 'cmpuishown' && tcfData.tcString && tcfData.purposeOneTreatment === true) { + if (tcfData.gdprApplies === false || tcfData.eventStatus === 'tcloaded' || tcfData.eventStatus === 'useractioncomplete') { cmpSuccess(tcfData, hookConfig); } } else { @@ -203,29 +199,52 @@ function lookupIabConsent(cmpSuccess, cmpError, hookConfig) { function callCmpWhileInIframe(commandName, cmpFrame, moduleCallback) { let apiName = (cmpVersion === 2) ? '__tcfapi' : '__cmp'; + 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; + This function behaves (from the caller's perspective identicially to the in-frame __cmp call */ + if (cmpVersion === 2) { + window[apiName] = function (cmd, cmpVersion, callback, arg) { + let callId = Math.random() + ''; + 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, null, moduleCallback); + // call CMP + window[apiName](commandName, cmpVersion, moduleCallback); + } else { + window[apiName] = function (cmd, arg, callback) { + let callId = Math.random() + ''; + 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`; @@ -387,7 +406,7 @@ function storeConsentData(cmpConsentObject) { vendorData: (cmpConsentObject) || undefined, gdprApplies: cmpConsentObject && typeof cmpConsentObject.gdprApplies === 'boolean' ? cmpConsentObject.gdprApplies : gdprScope }; - if (cmpConsentObject.addtlConsent && utils.isStr(cmpConsentObject.addtlConsent)) { + if (cmpConsentObject && cmpConsentObject.addtlConsent && utils.isStr(cmpConsentObject.addtlConsent)) { consentData.addtlConsent = cmpConsentObject.addtlConsent; }; } @@ -455,7 +474,7 @@ export function resetConsentData() { export function setConsentConfig(config) { // if `config.gdpr` or `config.usp` exist, assume new config format. // else for backward compatability, just use `config` - config = config.gdpr || config.usp ? config.gdpr : config; + config = config && (config.gdpr || config.usp ? config.gdpr : config); if (!config || typeof config !== 'object') { utils.logWarn('consentManagement config not defined, exiting consent manager'); return; diff --git a/modules/consentManagementUsp.js b/modules/consentManagementUsp.js index e4d5c12eb46..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); - } } } @@ -269,7 +295,7 @@ export function resetConsentData() { * @param {object} config required; consentManagementUSP module config settings; usp (string), timeout (int), allowAuctionWithoutConsent (boolean) */ export function setConsentConfig(config) { - config = config.usp; + config = config && config.usp; if (!config || typeof config !== 'object') { utils.logWarn('consentManagement.usp config not defined, exiting usp consent manager'); return; diff --git a/modules/conversantBidAdapter.js b/modules/conversantBidAdapter.js index 3b3d04dc498..806f276fb72 100644 --- a/modules/conversantBidAdapter.js +++ b/modules/conversantBidAdapter.js @@ -33,10 +33,11 @@ export const spec = { } if (isVideoRequest(bid)) { - if (!bid.params.mimes) { + const mimes = bid.params.mimes || utils.deepAccess(bid, 'mediaTypes.video.mimes'); + if (!mimes) { // Give a warning but let it pass utils.logWarn(BIDDER_CODE + ': mimes should be specified for videos'); - } else if (!utils.isArray(bid.params.mimes) || !bid.params.mimes.every(s => utils.isStr(s))) { + } else if (!utils.isArray(mimes) || !mimes.every(s => utils.isStr(s))) { utils.logWarn(BIDDER_CODE + ': mimes must be an array of strings'); return false; } @@ -90,7 +91,7 @@ export const spec = { copyOptProperty(bid.params.position, video, 'pos'); copyOptProperty(bid.params.mimes || videoData.mimes, video, 'mimes'); - copyOptProperty(bid.params.maxduration, video, 'maxduration'); + copyOptProperty(bid.params.maxduration || videoData.maxduration, video, 'maxduration'); copyOptProperty(bid.params.protocols || videoData.protocols, video, 'protocols'); copyOptProperty(bid.params.api || videoData.api, video, 'api'); @@ -205,6 +206,10 @@ export const spec = { ttl: 300, netRevenue: true }; + bid.meta = {}; + if (conversantBid.adomain && conversantBid.adomain.length > 0) { + bid.meta.advertiserDomains = conversantBid.adomain; + } if (request.video) { if (responseAd.charAt(0) === '<') { @@ -331,7 +336,6 @@ function collectEids(bidRequests) { 'criteo.com': 1, 'id5-sync.com': 1, 'parrable.com': 1, - 'digitru.st': 1, 'liveintent.com': 1 }; request.userIdAsEids.forEach(function(eid) { diff --git a/modules/conversantBidAdapter.md b/modules/conversantBidAdapter.md index fba793adad2..07d9abf918b 100644 --- a/modules/conversantBidAdapter.md +++ b/modules/conversantBidAdapter.md @@ -29,17 +29,17 @@ var adUnits = [ mediaTypes: { video: { context: 'instream', - playerSize: [640, 480] + playerSize: [640, 480], + api: [2], + protocols: [1, 2], + mimes: ['video/mp4'] } }, bids: [{ bidder: "conversant", params: { site_id: '108060', - api: [2], - protocols: [1, 2], - white_label_url: 'https://web.hb.ad.cpe.dotomi.com/s2s/header/24', - mimes: ['video/mp4'] + white_label_url: 'https://web.hb.ad.cpe.dotomi.com/s2s/header/24' } }] }]; diff --git a/modules/cpmstarBidAdapter.js b/modules/cpmstarBidAdapter.js old mode 100755 new mode 100644 index b416c00c2d0..61ccc0e786b --- a/modules/cpmstarBidAdapter.js +++ b/modules/cpmstarBidAdapter.js @@ -13,6 +13,12 @@ const ENDPOINT_PRODUCTION = 'https://server.cpmstar.com/view.aspx'; const DEFAULT_TTL = 300; const DEFAULT_CURRENCY = 'USD'; +function fixedEncodeURIComponent(str) { + return encodeURIComponent(str).replace(/[!'()*]/g, function(c) { + return '%' + c.charCodeAt(0).toString(16); + }); +} + export const spec = { code: BIDDER_CODE, supportedMediaTypes: [BANNER, VIDEO], @@ -47,13 +53,29 @@ export const spec = { var mediaType = spec.getMediaType(bidRequest); var playerSize = spec.getPlayerSize(bidRequest); var videoArgs = '&fv=0' + (playerSize ? ('&w=' + playerSize[0] + '&h=' + playerSize[1]) : ''); - var url = ENDPOINT + '?media=' + mediaType + (mediaType == VIDEO ? videoArgs : '') + '&json=c_b&mv=1&poolid=' + utils.getBidIdParameter('placementId', bidRequest.params) + '&reachedTop=' + encodeURIComponent(bidderRequest.refererInfo.reachedTop) + '&requestid=' + bidRequest.bidId + '&referer=' + encodeURIComponent(referer); + if (bidRequest.schain && bidRequest.schain.nodes) { + var schain = bidRequest.schain; + var schainString = ''; + schainString += schain.ver + ',' + schain.complete; + for (var i2 = 0; i2 < schain.nodes.length; i2++) { + var node = schain.nodes[i2]; + schainString += '!' + + fixedEncodeURIComponent(node.asi || '') + ',' + + fixedEncodeURIComponent(node.sid || '') + ',' + + fixedEncodeURIComponent(node.hp || '') + ',' + + fixedEncodeURIComponent(node.rid || '') + ',' + + fixedEncodeURIComponent(node.name || '') + ',' + + fixedEncodeURIComponent(node.domain || ''); + } + url += '&schain=' + schainString + } + if (bidderRequest.gdprConsent) { if (bidderRequest.gdprConsent.consentString != null) { url += '&gdpr_consent=' + bidderRequest.gdprConsent.consentString; @@ -138,6 +160,21 @@ export const spec = { } return bidResponses; + }, + + getUserSyncs: function (syncOptions, serverResponses) { + const syncs = []; + if (serverResponses.length == 0 || !serverResponses[0].body) return syncs; + var usersyncs = serverResponses[0].body[0].syncs; + if (!usersyncs || usersyncs.length < 0) return syncs; + for (var i = 0; i < usersyncs.length; i++) { + var us = usersyncs[i]; + if ((us.type === 'image' && syncOptions.pixelEnabled) || (us.type == 'iframe' && syncOptions.iframeEnabled)) { + syncs.push(us); + } + } + return syncs; } + }; registerBidder(spec); 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..41cbb0670c8 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; @@ -28,7 +28,7 @@ export const spec = { gvlid: GVLID, supportedMediaTypes: [ BANNER, VIDEO, NATIVE ], - /** + /** f * @param {object} bid * @return {boolean} */ @@ -56,10 +56,11 @@ export const spec = { buildRequests: (bidRequests, bidderRequest) => { let url; let data; + let fpd = config.getLegacyFpd(config.getConfig('ortb2')) || {}; Object.assign(bidderRequest, { - publisherExt: config.getConfig('fpd.context'), - userExt: config.getConfig('fpd.user'), + publisherExt: fpd.context, + userExt: fpd.user, ceh: config.getConfig('criteo.ceh') }); @@ -79,10 +80,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 +130,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 +248,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))) + )); } /** @@ -284,8 +281,8 @@ function buildCdbRequest(context, bidRequests, bidderRequest) { if (bidRequest.params.zoneId) { slot.zoneid = bidRequest.params.zoneId; } - if (bidRequest.fpd && bidRequest.fpd.context) { - slot.ext = bidRequest.fpd.context; + if (utils.deepAccess(bidRequest, 'ortb2Imp.ext')) { + slot.ext = bidRequest.ortb2Imp.ext; } if (bidRequest.params.ext) { slot.ext = Object.assign({}, slot.ext, bidRequest.params.ext); @@ -416,6 +413,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/criteoBidAdapter.md b/modules/criteoBidAdapter.md index e4c441c758d..68599f05434 100644 --- a/modules/criteoBidAdapter.md +++ b/modules/criteoBidAdapter.md @@ -2,7 +2,7 @@ Module Name: Criteo Bidder Adapter Module Type: Bidder Adapter -Maintainer: pi-direct@criteo.com +Maintainer: prebid@criteo.com # Description diff --git a/modules/criteoIdSystem.js b/modules/criteoIdSystem.js index c44f0c843ae..ac26d34d529 100644 --- a/modules/criteoIdSystem.js +++ b/modules/criteoIdSystem.js @@ -11,25 +11,19 @@ import { getRefererInfo } from '../src/refererDetection.js' import { submodule } from '../src/hook.js'; import { getStorageManager } from '../src/storageManager.js'; -export const storage = getStorageManager(); +const gvlid = 91; +const bidderCode = 'criteo'; +export const storage = getStorageManager(gvlid, bidderCode); const bididStorageKey = 'cto_bidid'; const bundleStorageKey = 'cto_bundle'; -const cookieWriteableKey = 'cto_test_cookie'; const cookiesMaxAge = 13 * 30 * 24 * 60 * 60 * 1000; const pastDateString = new Date(0).toString(); const expirationString = new Date(utils.timestamp() + cookiesMaxAge).toString(); -function areCookiesWriteable() { - storage.setCookie(cookieWriteableKey, '1'); - const canWrite = storage.getCookie(cookieWriteableKey) === '1'; - storage.setCookie(cookieWriteableKey, '', pastDateString); - return canWrite; -} - function extractProtocolHost (url, returnOnlyHost = false) { - const parsedUrl = utils.parseUrl(url) + const parsedUrl = utils.parseUrl(url, {noDecodeWholeURL: true}) return returnOnlyHost ? `${parsedUrl.hostname}` : `${parsedUrl.protocol}://${parsedUrl.hostname}${parsedUrl.port ? ':' + parsedUrl.port : ''}/`; @@ -58,20 +52,22 @@ function getCriteoDataFromAllStorages() { } } -function buildCriteoUsersyncUrl(topUrl, domain, bundle, areCookiesWriteable, isPublishertagPresent, gdprString) { +function buildCriteoUsersyncUrl(topUrl, domain, bundle, areCookiesWriteable, isLocalStorageWritable, isPublishertagPresent, gdprString) { const url = 'https://gum.criteo.com/sid/json?origin=prebid' + `${topUrl ? '&topUrl=' + encodeURIComponent(topUrl) : ''}` + `${domain ? '&domain=' + encodeURIComponent(domain) : ''}` + `${bundle ? '&bundle=' + encodeURIComponent(bundle) : ''}` + `${gdprString ? '&gdprString=' + encodeURIComponent(gdprString) : ''}` + `${areCookiesWriteable ? '&cw=1' : ''}` + - `${isPublishertagPresent ? '&pbt=1' : ''}` + `${isPublishertagPresent ? '&pbt=1' : ''}` + + `${isLocalStorageWritable ? '&lsw=1' : ''}`; return url; } function callCriteoUserSync(parsedCriteoData, gdprString) { - const cw = areCookiesWriteable(); + const cw = storage.cookiesAreEnabled(); + const lsw = storage.localStorageIsEnabled(); const topUrl = extractProtocolHost(getRefererInfo().referer); const domain = extractProtocolHost(document.location.href, true); const isPublishertagPresent = typeof criteo_pubtag !== 'undefined'; // eslint-disable-line camelcase @@ -81,6 +77,7 @@ function callCriteoUserSync(parsedCriteoData, gdprString) { domain, parsedCriteoData.bundle, cw, + lsw, isPublishertagPresent, gdprString ); @@ -101,7 +98,9 @@ function callCriteoUserSync(parsedCriteoData, gdprString) { } else if (jsonResponse.bundle) { saveOnAllStorages(bundleStorageKey, jsonResponse.bundle); } - } + }, + undefined, + { method: 'GET', contentType: 'application/json', withCredentials: true } ); } @@ -111,7 +110,8 @@ export const criteoIdSubmodule = { * used to link submodule with config * @type {string} */ - name: 'criteo', + name: bidderCode, + gvlid: gvlid, /** * decode the stored id value for passing to bid requests * @function @@ -123,11 +123,11 @@ export const criteoIdSubmodule = { /** * get the Criteo Id from local storages and initiate a new user sync * @function - * @param {SubmoduleParams} [configParams] + * @param {SubmoduleConfig} [config] * @param {ConsentData} [consentData] * @returns {{id: {criteoId: string} | undefined}}} */ - getId(configParams, consentData) { + getId(config, consentData) { const hasGdprData = consentData && typeof consentData.gdprApplies === 'boolean' && consentData.gdprApplies; const gdprConsentString = hasGdprData ? consentData.consentString : undefined; diff --git a/modules/decenteradsBidAdapter.js b/modules/decenteradsBidAdapter.js new file mode 100644 index 00000000000..823a59a3768 --- /dev/null +++ b/modules/decenteradsBidAdapter.js @@ -0,0 +1,90 @@ +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 = 'decenterads' +const URL = 'https://supply.decenterads.com/?c=o&m=multi' +const URL_SYNC = 'https://supply.decenterads.com/?c=o&m=cookie' + +export const spec = { + code: BIDDER_CODE, + supportedMediaTypes: [BANNER, VIDEO, NATIVE], + + isBidRequestValid: function (opts) { + return Boolean(opts.bidId && opts.params && !isNaN(opts.params.placementId)) + }, + + buildRequests: function (validBidRequests) { + validBidRequests = validBidRequests || [] + let winTop = window + try { + window.top.location.toString() + winTop = window.top + } catch (e) { utils.logMessage(e) } + + const location = utils.getWindowLocation() + const placements = [] + + for (let i = 0; i < validBidRequests.length; i++) { + const p = validBidRequests[i] + + placements.push({ + placementId: p.params.placementId, + bidId: p.bidId, + traffic: p.params.traffic || BANNER + }) + } + + return { + method: 'POST', + url: URL, + data: { + deviceWidth: winTop.screen.width, + deviceHeight: winTop.screen.height, + language: (navigator && navigator.language) ? navigator.language : '', + secure: +(location.protocol === 'https:'), + host: location.hostname, + page: location.pathname, + placements: placements + } + } + }, + + interpretResponse: function (opts) { + const body = opts.body + const response = [] + + for (let i = 0; i < body.length; i++) { + const item = body[i] + if (isBidResponseValid(item)) { + delete item.mediaType + response.push(item) + } + } + + return response + }, + + getUserSyncs: function (syncOptions, serverResponses) { + return [{ type: 'image', url: URL_SYNC }] + } +} + +registerBidder(spec) + +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.title && bid.image && bid.impressionTrackers) + default: + return false + } +} diff --git a/modules/deepintentBidAdapter.js b/modules/deepintentBidAdapter.js index c4dc23cf912..a6a6cac6570 100644 --- a/modules/deepintentBidAdapter.js +++ b/modules/deepintentBidAdapter.js @@ -49,6 +49,8 @@ export const spec = { utils.deepSetValue(openRtbBidRequest, 'regs.ext.gdpr', (bidderRequest.gdprConsent.gdprApplies ? 1 : 0)); } + injectEids(openRtbBidRequest, validBidRequests); + return { method: 'POST', url: BIDDER_ENDPOINT, @@ -86,6 +88,9 @@ function formatResponse(bid) { width: bid && bid.w ? bid.w : 0, height: bid && bid.h ? bid.h : 0, ad: bid && bid.adm ? bid.adm : '', + meta: { + advertiserDomains: bid && bid.adomain ? bid.adomain : [] + }, creativeId: bid && bid.crid ? bid.crid : undefined, netRevenue: false, currency: bid && bid.cur ? bid.cur : 'USD', @@ -128,6 +133,13 @@ function buildUser(bid) { } } +function injectEids(openRtbBidRequest, validBidRequests) { + const bidUserIdAsEids = utils.deepAccess(validBidRequests, '0.userIdAsEids'); + if (utils.isArray(bidUserIdAsEids) && bidUserIdAsEids.length > 0) { + utils.deepSetValue(openRtbBidRequest, 'user.eids', bidUserIdAsEids); + } +} + function buildBanner(bid) { if (utils.deepAccess(bid, 'mediaTypes.banner')) { // Get Sizes from MediaTypes Object, Will always take first size, will be overrided by params for exact w,h diff --git a/modules/deepintentDpesIdSystem.js b/modules/deepintentDpesIdSystem.js new file mode 100644 index 00000000000..375c8c07ed1 --- /dev/null +++ b/modules/deepintentDpesIdSystem.js @@ -0,0 +1,45 @@ +/** + * This module adds DPES to the User ID module + * The {@link module:modules/userId} module is required + * @module modules/deepintentDpesSystem + * @requires module:modules/userId + */ + +import { submodule } from '../src/hook.js'; +import { getStorageManager } from '../src/storageManager.js'; + +const MODULE_NAME = 'deepintentId'; +export const storage = getStorageManager(null, MODULE_NAME); + +/** @type {Submodule} */ +export const deepintentDpesSubmodule = { + /** + * used to link submodule with config + * @type {string} + */ + name: MODULE_NAME, + /** + * decode the stored id value for passing to bid requests + * @function + * @param {{value:string}} value + * @returns {{deepintentId:Object}} + */ + decode(value, config) { + return value ? { 'deepintentId': value } : undefined; + }, + + /** + * performs action to obtain id and return a value in the callback's response argument + * @function + * @param {SubmoduleConfig} config + * @param {ConsentData|undefined} consentData + * @param {Object} cacheIdObj - existing id, if any + * @return {{id: string | undefined} | undefined} + */ + getId(config, consentData, cacheIdObj) { + return cacheIdObj; + } + +}; + +submodule('userId', deepintentDpesSubmodule); diff --git a/modules/deepintentDpesIdSystem.md b/modules/deepintentDpesIdSystem.md new file mode 100644 index 00000000000..2af0fe7446e --- /dev/null +++ b/modules/deepintentDpesIdSystem.md @@ -0,0 +1,43 @@ +# Deepintent DPES ID + +The Deepintent Id is a shared, healthcare identifier which helps publisher in absence of the 3rd Party cookie matching. This lets publishers set and bid with healthcare identity . Deepintent lets users protect their privacy through advertising value chain, where Healthcare identity when setting the identity takes in consideration of users choices, as well as when passing identity on the cookie itself privacy consent strings are checked. The healthcare identity when set is not stored on Deepintent's servers but is stored on users browsers itself. User can still opt out of the ads by https://option.deepintent.com/adchoices. + +## Deepintent DPES ID Registration + +The Deepintent DPES ID is free to use, but requires a simple registration with Deepintent. Please reach to prebid@deepintent.com to get started. +Once publisher registers with deepintents platform for healthcare identity Deepintent provides the Tag code to be placed on the page, this tag code works to capture and store information as per publishers and users agreement. DPES User ID module uses this stored id and passes it on the deepintent prebid adapter. + + +## Deepintent DPES ID Configuration + +First, make sure to add the Deepintent submodule to your Prebid.js package with: + +``` +gulp build --modules=deepintentDpesIdSystem,userId +``` + +The following configuration parameters are available: + +```javascript +pbjs.setConfig({ + userSync: { + userIds: [{ + name: 'deepintentId', + storage: { + type: 'cookie', + name: '_dpes_id', + expires: 90 // storage lasts for 90 days, optional if storage type is html5 + } + }], + auctionDelay: 50 // 50ms maximum auction delay, applies to all userId modules + } +}); +``` + +| Param under userSync.userIds[] | Scope | Type | Description | Example | +| --- | --- | --- | --- | --- | +| name | Required | String | The name of this module: `"deepintentId"` | `"deepintentId"` | +| storage | Required | Object | Storage settings for how the User Id module will cache the Deepintent ID locally | | +| storage.type | Required | String | This is where the results of the user ID will be stored. Deepintent`"html5"` or `"cookie"`. | `"html5"` | +| storage.name | Required | String | The name of the local storage where the user ID will be stored. | `"_dpes_id"` | +| storage.expires | Optional | Integer | How long (in days) the user ID information will be stored. Deepintent recommends `90`. | `90` | \ No newline at end of file diff --git a/modules/dfpAdServerVideo.js b/modules/dfpAdServerVideo.js index 1d999fdbacc..22c904b604c 100644 --- a/modules/dfpAdServerVideo.js +++ b/modules/dfpAdServerVideo.js @@ -8,6 +8,9 @@ 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'; /** * @typedef {Object} DfpVideoParams @@ -42,7 +45,7 @@ import { auctionManager } from '../src/auctionManager.js'; const defaultParamConstants = { env: 'vp', gdfp_req: 1, - output: 'xml_vast3', + output: 'vast', unviewed_position_start: 1, }; @@ -98,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', @@ -181,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', @@ -245,17 +268,20 @@ function getCustParams(bid, options) { allTargetingData = (allTargeting) ? allTargeting[adUnit.code] : {}; } - const optCustParams = deepAccess(options, 'params.cust_params'); - let customParams = Object.assign({}, + const prebidTargetingSet = Object.assign({}, // Why are we adding standard keys here ? Refer https://github.com/prebid/Prebid.js/issues/3664 { hb_uuid: bid && bid.videoCacheKey }, // hb_uuid will be deprecated and replaced by hb_cache_id { hb_cache_id: bid && bid.videoCacheKey }, allTargetingData, adserverTargeting, - optCustParams, ); - return encodeURIComponent(formatQS(customParams)); + events.emit(CONSTANTS.EVENTS.SET_TARGETING, {[adUnit.code]: prebidTargetingSet}); + + // merge the prebid + publisher targeting sets + const publisherTargetingSet = deepAccess(options, 'params.cust_params'); + const targetingSet = Object.assign({}, prebidTargetingSet, publisherTargetingSet); + return encodeURIComponent(formatQS(targetingSet)); } registerVideoSupport('dfp', { diff --git a/modules/districtmDMXBidAdapter.js b/modules/districtmDMXBidAdapter.js index 03dca0b607d..ec0a0a2f2e6 100644 --- a/modules/districtmDMXBidAdapter.js +++ b/modules/districtmDMXBidAdapter.js @@ -1,46 +1,71 @@ import * as utils from '../src/utils.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; -import {config} from '../src/config.js'; +import { config } from '../src/config.js'; +import { BANNER, VIDEO } from '../src/mediaTypes.js'; 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, + 'auto_play_sound_off': 2, + 'click_to_play': 3, + 'mouse_over': 4, + 'viewport_sound_on': 5, + 'viewport_sound_off': 6 + } +}; export const spec = { code: BIDDER_CODE, - supportedFormat: ['banner'], + 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 || {}; if (response.seatbid) { if (utils.isArray(response.seatbid)) { - const {seatbid} = response; + const { seatbid } = response; let winners = seatbid.reduce((bid, ads) => { - let ad = ads.bid.reduce(function(oBid, nBid) { + let ad = ads.bid.reduce(function (oBid, nBid) { if (oBid.price < nBid.price) { const bid = matchRequest(nBid.impid, bidRequest); - const {width, height} = defaultSize(bid); + const { width, height } = defaultSize(bid); nBid.cpm = parseFloat(nBid.price).toFixed(2); nBid.bidId = nBid.impid; nBid.requestId = nBid.impid; nBid.width = nBid.w || width; nBid.height = nBid.h || height; + nBid.ttl = 300; + nBid.mediaType = bid.mediaTypes && bid.mediaTypes.video ? 'video' : 'banner'; + if (nBid.mediaType === 'video') { + nBid.vastXml = cleanVast(nBid.adm, nBid.nurl); + nBid.ttl = 3600; + } if (nBid.dealid) { nBid.dealId = nBid.dealid; } + nBid.uuid = nBid.bidId; nBid.ad = nBid.adm; nBid.netRevenue = true; nBid.creativeId = nBid.crid; nBid.currency = 'USD'; - nBid.ttl = 60; + nBid.meta = nBid.meta || {}; + if (nBid.adomain && nBid.adomain.length > 0) { + nBid.meta.advertiserDomains = nBid.adomain; + } return nBid; } else { oBid.cpm = oBid.price; return oBid; } - }, {price: 0}); + }, { price: 0 }); if (ad.adm) { bid.push(ad) } @@ -77,18 +102,25 @@ export const spec = { let params = config.getConfig('dmx'); dmxRequest.user = params.user || {}; let site = params.site || {}; - dmxRequest.site = {...dmxRequest.site, ...site} + dmxRequest.site = { ...dmxRequest.site, ...site } } catch (e) { } let eids = []; - if (bidRequest && bidRequest.userId) { - bindUserId(eids, utils.deepAccess(bidRequest, `userId.idl_env`), 'liveramp.com', 1); - bindUserId(eids, utils.deepAccess(bidRequest, `userId.digitrustid.data.id`), 'digitru.st', 1); - bindUserId(eids, utils.deepAccess(bidRequest, `userId.id5id`), 'id5-sync.com', 1); - bindUserId(eids, utils.deepAccess(bidRequest, `userId.pubcid`), 'pubcid.org', 1); - bindUserId(eids, utils.deepAccess(bidRequest, `userId.tdid`), 'adserver.org', 1); + if (bidRequest[0] && bidRequest[0].userId) { + bindUserId(eids, utils.deepAccess(bidRequest[0], `userId.idl_env`), 'liveramp.com', 1); + bindUserId(eids, utils.deepAccess(bidRequest[0], `userId.id5id.uid`), 'id5-sync.com', 1); + bindUserId(eids, utils.deepAccess(bidRequest[0], `userId.pubcid`), 'pubcid.org', 1); + bindUserId(eids, utils.deepAccess(bidRequest[0], `userId.tdid`), 'adserver.org', 1); + bindUserId(eids, utils.deepAccess(bidRequest[0], `userId.criteoId`), 'criteo.com', 1); + bindUserId(eids, utils.deepAccess(bidRequest[0], `userId.britepoolid`), 'britepool.com', 1); + bindUserId(eids, utils.deepAccess(bidRequest[0], `userId.lipb.lipbid`), 'liveintent.com', 1); + bindUserId(eids, utils.deepAccess(bidRequest[0], `userId.intentiqid`), 'intentiq.com', 1); + bindUserId(eids, utils.deepAccess(bidRequest[0], `userId.lotamePanoramaId`), 'lotame.com', 1); + bindUserId(eids, utils.deepAccess(bidRequest[0], `userId.parrableId`), 'parrable.com', 1); + bindUserId(eids, utils.deepAccess(bidRequest[0], `userId.netId`), 'netid.de', 1); + bindUserId(eids, utils.deepAccess(bidRequest[0], `userId.sharedid`), 'sharedid.org', 1); dmxRequest.user = dmxRequest.user || {}; dmxRequest.user.ext = dmxRequest.user.ext || {}; dmxRequest.user.ext.eids = eids; @@ -100,9 +132,12 @@ export const spec = { dmxRequest.regs = {}; dmxRequest.regs.ext = {}; dmxRequest.regs.ext.gdpr = bidderRequest.gdprConsent.gdprApplies === true ? 1 : 0; - dmxRequest.user = {}; - dmxRequest.user.ext = {}; - dmxRequest.user.ext.consent = bidderRequest.gdprConsent.consentString; + + if (bidderRequest.gdprConsent.gdprApplies === true) { + dmxRequest.user = {}; + dmxRequest.user.ext = {}; + dmxRequest.user.ext.consent = bidderRequest.gdprConsent.consentString; + } } dmxRequest.regs = dmxRequest.regs || {}; dmxRequest.regs.coppa = config.getConfig('coppa') === true ? 1 : 0; @@ -116,20 +151,37 @@ export const spec = { dmxRequest.source = {}; dmxRequest.source.ext = {}; dmxRequest.source.ext.schain = schain || {} - } catch (e) {} + } catch (e) { } 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.banner = { - topframe: 1, - w: cleanSizes(dmx.sizes, 'w'), - h: cleanSizes(dmx.sizes, 'h'), - format: cleanSizes(dmx.sizes).map(s => { - return {w: s[0], h: s[1]}; - }).filter(obj => typeof obj.w === 'number' && typeof obj.h === 'number') - }; + obj.bidfloor = getFloor(dmx); + if (dmx.mediaTypes && dmx.mediaTypes.video) { + obj.video = { + topframe: 1, + skip: dmx.mediaTypes.video.skip || 0, + linearity: dmx.mediaTypes.video.linearity || 1, + minduration: dmx.mediaTypes.video.minduration || 5, + maxduration: dmx.mediaTypes.video.maxduration || 60, + playbackmethod: dmx.mediaTypes.video.playbackmethod || [2], + api: getApi(dmx.mediaTypes.video), + mimes: dmx.mediaTypes.video.mimes || ['video/mp4'], + protocols: getProtocols(dmx.mediaTypes.video), + h: dmx.mediaTypes.video.playerSize[0][1], + w: dmx.mediaTypes.video.playerSize[0][0] + }; + } else { + obj.banner = { + topframe: 1, + w: cleanSizes(dmx.sizes, 'w'), + h: cleanSizes(dmx.sizes, 'h'), + format: cleanSizes(dmx.sizes).map(s => { + return { w: s[0], h: s[1] }; + }).filter(obj => typeof obj.w === 'number' && typeof obj.h === 'number') + }; + } return obj; }); @@ -169,6 +221,27 @@ export const spec = { } } +export function getFloor(bid) { + let floor = null; + if (typeof bid.getFloor === 'function') { + const floorInfo = bid.getFloor({ + currency: 'USD', + mediaType: bid.mediaTypes.video ? 'video' : 'banner', + size: bid.sizes.map(size => { + return { + w: size[0], + h: size[1] + } + }) + }); + if (typeof floorInfo === 'object' && + floorInfo.currency === 'USD' && !isNaN(parseFloat(floorInfo.floor))) { + floor = parseFloat(floorInfo.floor); + } + } + return floor !== null ? floor : bid.params.floor; +} + export function cleanSizes(sizes, value) { const supportedSize = [ { @@ -228,7 +301,7 @@ export function shuffle(sizes, list) { } results.push(current); results = list.filter(l => results.map(r => `${r[0]}x${r[1]}`).indexOf(`${l.size[0]}x${l.size[1]}`) !== -1); - results = results.sort(function(a, b) { + results = results.sort(function (a, b) { return b.s - a.s; }) return results.map(r => r.size); @@ -274,7 +347,7 @@ export function upto5(allimps, dmxRequest, bidderRequest, DMXURI) { * */ export function matchRequest(id, bidRequest) { - const {bids} = bidRequest.bidderRequest; + const { bids } = bidRequest.bidderRequest; const [returnValue] = bids.filter(bid => bid.bidId === id); return returnValue; } @@ -290,7 +363,7 @@ export function checkDeepArray(Arr) { } } export function defaultSize(thebidObj) { - const {sizes} = thebidObj; + const { sizes } = thebidObj; const returnObject = {}; returnObject.width = checkDeepArray(sizes)[0]; returnObject.height = checkDeepArray(sizes)[1]; @@ -310,4 +383,52 @@ export function bindUserId(eids, value, source, atype) { }) } } + +export function getApi({ api }) { + let defaultValue = [2]; + if (api && Array.isArray(api) && api.length > 0) { + return api + } else { + return defaultValue; + } +} +export function getPlaybackmethod(playback) { + if (Array.isArray(playback) && playback.length > 0) { + return playback.map(label => { + return VIDEO_MAPPING.playback_method[label] + }) + } + return [2] +} + +export function getProtocols({ protocols }) { + let defaultValue = [2, 3, 5, 6, 7, 8]; + if (protocols && Array.isArray(protocols) && protocols.length > 0) { + return protocols; + } else { + return defaultValue; + } +} + +export function cleanVast(str, nurl) { + try { + const toberemove = /]*?src\s*=\s*['\"]([^'\"]*?)['\"][^>]*?>/ + const [img, url] = str.match(toberemove) + str = str.replace(toberemove, '') + if (img) { + if (url) { + const insrt = `` + str = str.replace('', `${insrt}`) + } + } + return str; + } catch (e) { + if (!nurl) { + return str + } + const insrt = `` + str = str.replace('', `${insrt}`) + return str + } +} registerBidder(spec); diff --git a/modules/districtmDmxBidAdapter.md b/modules/districtmDmxBidAdapter.md index 792cf2e7305..5d5dd2affe6 100644 --- a/modules/districtmDmxBidAdapter.md +++ b/modules/districtmDmxBidAdapter.md @@ -20,13 +20,14 @@ The `districtmDmxAdapter` module allows publishers to include DMX Exchange deman ## Media Types * Banner - +* Video ## Bidder Parameters | Key | Scope | Type | Description | --- | --- | --- | --- | `dmxid` | Mandatory | Integer | Unique identifier of the placement, dmxid can be obtained in the district m Boost platform. | `memberid` | Mandatory | Integer | Unique identifier for your account, memberid can be obtained in the district m Boost platform. +| `floor` | Optional | float | Most placement can have floor set in our platform, but this can now be set on the request too. # Ad Unit Configuration Example @@ -48,6 +49,35 @@ The `districtmDmxAdapter` module allows publishers to include DMX Exchange deman }]; ``` +# Ad Unit Configuration Example for video request + +```javascript + var videoAdUnit = { + code: 'video1', + sizes: [640,480], + mediaTypes: { video: {context: 'instream', //or 'outstream' + playerSize: [[640, 480]], + skipppable: true, + minduration: 5, + maxduration: 45, + playback_method: ['auto_play_sound_off', 'viewport_sound_off'], + mimes: ["application/javascript", + "video/mp4"], + + } }, + bids: [ + { + bidder: 'districtmDMX', + params: { + dmxid: '100001', + memberid: '100003', + } + } + + ] + }; +``` + # Ad Unit Configuration when COPPA is needed @@ -117,6 +147,35 @@ Our demand and adapter supports multiple sizes per placement, as such a single d }]; ``` +Our bidder only supports instream context at the moment and we strongly like to put the media types and setting in the ad unit settings. +If no value is set the default value will be applied. + +```javascript + var videoAdUnit = { + code: 'video1', + sizes: [640,480], + mediaTypes: { video: {context: 'instream', //or 'outstream' + playerSize: [[640, 480]], + skipppable: true, + minduration: 5, + maxduration: 45, + playback_method: ['auto_play_sound_off', 'viewport_sound_off'], + mimes: ["application/javascript", + "video/mp4"], + + } }, + bids: [ + { + bidder: 'districtmDMX', + params: { + dmxid: '250258', + memberid: '100600', + } + } + ] + }; +``` + ###### 4. Implementation Checking Once the bidder is live in your Prebid configuration you may confirm it is making requests to our end point by looking for requests to `https://dmx.districtm.io/b/v1`. diff --git a/modules/dmdIdSystem.js b/modules/dmdIdSystem.js new file mode 100644 index 00000000000..7cf7b9fac95 --- /dev/null +++ b/modules/dmdIdSystem.js @@ -0,0 +1,58 @@ +/** + * This module adds dmdId to the User ID module + * The {@link module:modules/userId} module is required + * @module modules/dmdIdSystem + * @requires module:modules/userId + */ + +import * as utils from '../src/utils.js'; +import { submodule } from '../src/hook.js'; + +/** @type {Submodule} */ +export const dmdIdSubmodule = { + /** + * used to link submodule with config + * @type {string} + */ + name: 'dmdId', + + /** + * decode the stored id value for passing to bid requests + * @function decode + * @param {(Object|string)} value + * @returns {(Object|undefined)} + */ + decode(value) { + return value && typeof value === 'string' + ? { 'dmdId': value } + : undefined; + }, + + /** + * performs action to obtain id and return a value in the callback's response argument + * @function getId + * @param {SubmoduleConfig} [config] + * @param {ConsentData} + * @param {Object} cacheIdObj - existing id, if any consentData] + * @returns {IdResponse|undefined} + */ + getId(config, consentData, cacheIdObj) { + try { + const configParams = (config && config.params) || {}; + if ( + !configParams || + !configParams.api_key || + typeof configParams.api_key !== 'string' + ) { + utils.logError('dmd submodule requires an api_key.'); + return; + } else { + return cacheIdObj; + } + } catch (e) { + utils.logError(`dmdIdSystem encountered an error`, e); + } + }, +}; + +submodule('userId', dmdIdSubmodule); diff --git a/modules/dmdIdSystem.md b/modules/dmdIdSystem.md new file mode 100644 index 00000000000..f2a5b76ade7 --- /dev/null +++ b/modules/dmdIdSystem.md @@ -0,0 +1,26 @@ +pbjs.setConfig({ + userSync: { + userIds: [{ + name: 'dmdId', + storage: { + name: 'dmd-dgid', + type: 'cookie', + expires: 30 + }, + params: { + api_key: '3fdbe297-3690-4f5c-9e11-ee9186a6d77c', // provided by DMD + } + }] + } +}); + +#### DMD ID Configuration + +{: .table .table-bordered .table-striped } +| Param under userSync.userIds[] | Scope | Type | Description | Example | +| --- | --- | --- | --- | --- | +| name | Required | String | The name of Module | `"dmdId"` | +| storage | Required | Object | | +| storage.name | Required | String | `dmd-dgid` | +| params | Required | Object | Container of all module params. | | +| params.api_key | Required | String | This is your `api_key` as provided by DMD Marketing Corp. | `3fdbe297-3690-4f5c-9e11-ee9186a6d77c` | \ No newline at end of file diff --git a/modules/docereeBidAdapter.js b/modules/docereeBidAdapter.js new file mode 100644 index 00000000000..f9f3e1bcc70 --- /dev/null +++ b/modules/docereeBidAdapter.js @@ -0,0 +1,65 @@ +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 = 'doceree'; +const END_POINT = 'https://bidder.doceree.com' + +export const spec = { + code: BIDDER_CODE, + url: '', + supportedMediaTypes: [ BANNER ], + + isBidRequestValid: (bid) => { + const { placementId } = bid.params; + return !!placementId + }, + buildRequests: (validBidRequests) => { + const serverRequests = []; + const { data } = config.getConfig('doceree.user') + const { page, domain, token } = config.getConfig('doceree.context') + const encodedUserInfo = window.btoa(encodeURIComponent(JSON.stringify(data))) + + validBidRequests.forEach(function(validBidRequest) { + const { publisherUrl, placementId } = validBidRequest.params; + const url = publisherUrl || page + let queryString = ''; + queryString = utils.tryAppendQueryString(queryString, 'id', placementId); + queryString = utils.tryAppendQueryString(queryString, 'publisherDomain', domain); + queryString = utils.tryAppendQueryString(queryString, 'pubRequestedURL', encodeURIComponent(url)); + queryString = utils.tryAppendQueryString(queryString, 'loggedInUser', encodedUserInfo); + queryString = utils.tryAppendQueryString(queryString, 'currentUrl', url); + queryString = utils.tryAppendQueryString(queryString, 'prebidjs', true); + queryString = utils.tryAppendQueryString(queryString, 'token', token); + queryString = utils.tryAppendQueryString(queryString, 'requestId', validBidRequest.bidId); + + serverRequests.push({ + method: 'GET', + url: END_POINT + '/v1/adrequest?' + queryString + }) + }) + return serverRequests; + }, + interpretResponse: (serverResponse, request) => { + const responseJson = serverResponse ? serverResponse.body : {}; + const placementId = responseJson.DIVID; + const bidResponse = { + ad: responseJson.sourceHTML, + width: Number(responseJson.width), + height: Number(responseJson.height), + requestId: responseJson.guid, + netRevenue: true, + ttl: 30, + cpm: responseJson.cpmBid, + currency: responseJson.currency, + mediaType: 'banner', + creativeId: placementId, + meta: { + advertiserDomains: [responseJson.advertiserDomain] + } + }; + return [bidResponse]; + } +}; + +registerBidder(spec); diff --git a/modules/docereeBidAdapter.md b/modules/docereeBidAdapter.md index 9e3d4bbe1b8..d977e11f40a 100644 --- a/modules/docereeBidAdapter.md +++ b/modules/docereeBidAdapter.md @@ -1,32 +1,33 @@ # Overview -Module Name: Doceree Bidder Adapter -Module Type: Bidder Adapter - -# Description +``` +Module Name: Doceree Bidder Adapter +Module Type: Bidder Adapter +Maintainer: sourbh.gupta@doceree.com +``` + Connects to Doceree demand source to fetch bids. -Please use ```doceree``` as the bidder code. +Please use ```doceree``` as the bidder code. + # Test Parameters ``` - var adUnits = [ +var adUnits = [ + { + code: 'doceree', + sizes: [ + [300, 250] + ], + bids: [ { - code: 'desktop-banner-ad-div', - sizes: [[300, 250]], - bids: [ - { - bidder: "doceree", - params: { - accountID: '167283', - zoneID: '445501', - domain: 'adbserver.doceree.com', - extra: { - tuid: '1234-abcd' - } - } - } - ] - }, - ]; + bidder: "doceree", + params: { + placementId: 'DOC_7jm9j5eqkl0xvc5w', //required + publisherUrl: document.URL || window.location.href, //optional + } + } + ] + } +]; ``` diff --git a/modules/dspxBidAdapter.js b/modules/dspxBidAdapter.js index d05549601e1..dd49a744225 100644 --- a/modules/dspxBidAdapter.js +++ b/modules/dspxBidAdapter.js @@ -123,7 +123,44 @@ export const spec = { bidResponses.push(bidResponse); } return bidResponses; + }, + getUserSyncs: function(syncOptions, serverResponses, gdprConsent, uspConsent) { + if (!serverResponses || serverResponses.length === 0) { + return []; + } + + const syncs = [] + + let gdprParams = ''; + if (gdprConsent) { + if ('gdprApplies' in gdprConsent && typeof gdprConsent.gdprApplies === 'boolean') { + gdprParams = `gdpr=${Number(gdprConsent.gdprApplies)}&gdpr_consent=${gdprConsent.consentString}`; + } else { + gdprParams = `gdpr_consent=${gdprConsent.consentString}`; + } + } + + if (syncOptions.iframeEnabled) { + serverResponses[0].body.userSync.iframeUrl.forEach((url) => syncs.push({ + type: 'iframe', + url: appendToUrl(url, gdprParams) + })); + } + if (syncOptions.pixelEnabled && serverResponses.length > 0) { + serverResponses[0].body.userSync.imageUrl.forEach((url) => syncs.push({ + type: 'image', + url: appendToUrl(url, gdprParams) + })); + } + return syncs; + } +} + +function appendToUrl(url, what) { + if (!what) { + return url; } + return url + (url.indexOf('?') !== -1 ? '&' : '?') + what; } function objectToQueryString(obj, prefix) { diff --git a/modules/emx_digitalBidAdapter.js b/modules/emx_digitalBidAdapter.js index fa58481548a..41a5af6d703 100644 --- a/modules/emx_digitalBidAdapter.js +++ b/modules/emx_digitalBidAdapter.js @@ -156,6 +156,17 @@ export const emxAdapter = { }; } + return emxData; + }, + getSupplyChain: (bidderRequest, emxData) => { + if (bidderRequest.bids[0] && bidderRequest.bids[0].schain) { + emxData.source = { + ext: { + schain: bidderRequest.bids[0].schain + } + }; + } + return emxData; } }; @@ -237,6 +248,7 @@ export const spec = { }; emxData = emxAdapter.getGdpr(bidderRequest, Object.assign({}, emxData)); + emxData = emxAdapter.getSupplyChain(bidderRequest, Object.assign({}, emxData)); if (bidderRequest && bidderRequest.uspConsent) { emxData.us_privacy = bidderRequest.uspConsent } 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/enrichmentFpdModule.js b/modules/enrichmentFpdModule.js new file mode 100644 index 00000000000..a1d815917e0 --- /dev/null +++ b/modules/enrichmentFpdModule.js @@ -0,0 +1,107 @@ +/** + * This module sets default values and validates ortb2 first part data + * @module modules/firstPartyData + */ +import * as utils from '../src/utils.js'; +import { submodule } from '../src/hook.js' +import { getRefererInfo } from '../src/refererDetection.js' + +let ortb2 = {}; +let win = (window === window.top) ? window : window.top; + +/** + * Checks for referer and if exists merges into ortb2 global data + */ +function setReferer() { + if (getRefererInfo().referer) utils.mergeDeep(ortb2, { site: { ref: getRefererInfo().referer } }); +} + +/** + * Checks for canonical url and if exists merges into ortb2 global data + */ +function setPage() { + if (getRefererInfo().canonicalUrl) utils.mergeDeep(ortb2, { site: { page: getRefererInfo().canonicalUrl } }); +} + +/** + * Checks for canonical url and if exists retrieves domain and merges into ortb2 global data + */ +function setDomain() { + let parseDomain = function(url) { + if (!url || typeof url !== 'string' || url.length === 0) return; + + var match = url.match(/^(?:https?:\/\/)?(?:www\.)?(.*?(?=(\?|\#|\/|$)))/i); + + return match && match[1]; + }; + + let domain = parseDomain(getRefererInfo().canonicalUrl) + + if (domain) utils.mergeDeep(ortb2, { site: { domain: domain } }); +} + +/** + * Checks for screen/device width and height and sets dimensions + */ +function setDimensions() { + let width; + let height; + + try { + width = win.innerWidth || win.document.documentElement.clientWidth || win.document.body.clientWidth; + height = win.innerHeight || win.document.documentElement.clientHeight || win.document.body.clientHeight; + } catch (e) { + width = window.innerWidth || window.document.documentElement.clientWidth || window.document.body.clientWidth; + height = window.innerHeight || window.document.documentElement.clientHeight || window.document.body.clientHeight; + } + + utils.mergeDeep(ortb2, { device: { w: width, h: height } }); +} + +/** + * Scans page for meta keywords, and if exists, merges into site.keywords + */ +function setKeywords() { + let keywords; + + try { + keywords = win.document.querySelector("meta[name='keywords']"); + } catch (e) { + keywords = window.document.querySelector("meta[name='keywords']"); + } + + if (keywords && keywords.content) utils.mergeDeep(ortb2, { site: { keywords: keywords.content.replace(/\s/g, '') } }); +} + +/** + * Resets modules global ortb2 data + */ +const resetOrtb2 = () => { ortb2 = {} }; + +function runEnrichments() { + setReferer(); + setPage(); + setDomain(); + setDimensions(); + setKeywords(); + + return ortb2; +} + +/** + * Sets default values to ortb2 if exists and adds currency and ortb2 setConfig callbacks on init + */ +export function initSubmodule(fpdConf, data) { + resetOrtb2(); + + return (!fpdConf.skipEnrichments) ? utils.mergeDeep(runEnrichments(), data) : data; +} + +/** @type {firstPartyDataSubmodule} */ +export const enrichmentsSubmodule = { + name: 'enrichments', + queue: 2, + init: initSubmodule +} + +submodule('firstPartyData', enrichmentsSubmodule) diff --git a/modules/eplanningBidAdapter.js b/modules/eplanningBidAdapter.js index ac5ba659ad7..98a0e290575 100644 --- a/modules/eplanningBidAdapter.js +++ b/modules/eplanningBidAdapter.js @@ -1,4 +1,5 @@ import * as utils from '../src/utils.js'; +import { getGlobal } from '../src/prebidGlobal.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; import { getStorageManager } from '../src/storageManager.js'; @@ -8,7 +9,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 +17,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, @@ -43,7 +47,7 @@ export const spec = { url = 'https://' + urlConfig.isv + '/layers/t_pbjs_2.json'; params = {}; } else { - url = 'https://' + (urlConfig.sv || DEFAULT_SV) + '/hb/1/' + urlConfig.ci + '/' + dfpClientId + '/' + getDomain(pageUrl) + '/' + sec; + url = 'https://' + (urlConfig.sv || DEFAULT_SV) + '/pbjs/1/' + urlConfig.ci + '/' + dfpClientId + '/' + getDomain(pageUrl) + '/' + sec; const referrerUrl = bidderRequest.refererInfo.referer.reachedTop ? window.top.document.referrer : bidderRequest.refererInfo.referer; if (storage.hasLocalStorage()) { @@ -54,7 +58,6 @@ export const spec = { rnd: rnd, e: spaces.str, ur: pageUrl || FILE, - r: 'pbjs', pbv: '$prebid.version$', ncb: '1', vs: spaces.vs @@ -79,6 +82,10 @@ export const spec = { if (bidderRequest && bidderRequest.uspConsent) { params.ccpa = bidderRequest.uspConsent; } + const userIds = (getGlobal()).getUserIds(); + for (var id in userIds) { + params['e_' + id] = (typeof userIds[id] === 'object') ? encodeURIComponent(JSON.stringify(userIds[id])) : encodeURIComponent(userIds[id]); + } } return { @@ -140,6 +147,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 +192,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 +240,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/etargetBidAdapter.js b/modules/etargetBidAdapter.js index 5e07561044a..42c991a17a4 100644 --- a/modules/etargetBidAdapter.js +++ b/modules/etargetBidAdapter.js @@ -96,6 +96,7 @@ export const spec = { currency: data.win_cur, netRevenue: true, ttl: 360, + reason: data.reason ? data.reason : 'none', ad: data.banner, vastXml: data.vast_content, vastUrl: data.vast_link, diff --git a/modules/fabrickIdSystem.js b/modules/fabrickIdSystem.js new file mode 100644 index 00000000000..bb838788f07 --- /dev/null +++ b/modules/fabrickIdSystem.js @@ -0,0 +1,183 @@ +/** + * This module adds neustar's fabrickId to the User ID module + * The {@link module:modules/userId} module is required + * @module modules/fabrickIdSystem + * @requires module:modules/userId + */ + +import * as utils from '../src/utils.js' +import { ajax } from '../src/ajax.js'; +import { submodule } from '../src/hook.js'; +import { getRefererInfo } from '../src/refererDetection.js'; + +/** @type {Submodule} */ +export const fabrickIdSubmodule = { + /** + * used to link submodule with config + * @type {string} + */ + name: 'fabrickId', + + /** + * decode the stored id value for passing to bid requests + * @function decode + * @param {(Object|string)} value + * @returns {(Object|undefined)} + */ + decode(value) { + if (value && value.fabrickId) { + return { 'fabrickId': value.fabrickId }; + } else { + return undefined; + } + }, + + /** + * performs action to obtain id and return a value in the callback's response argument + * @function getId + * @param {SubmoduleConfig} [config] + * @param {ConsentData} + * @param {Object} cacheIdObj - existing id, if any consentData] + * @returns {IdResponse|undefined} + */ + getId(config, consentData, cacheIdObj) { + try { + const configParams = (config && config.params) || {}; + if (window.fabrickMod1) { + window.fabrickMod1(configParams, consentData, cacheIdObj); + } + if (!configParams || !configParams.apiKey || typeof configParams.apiKey !== 'string') { + utils.logError('fabrick submodule requires an apiKey.'); + return; + } + try { + let url = _getBaseUrl(configParams); + let keysArr = Object.keys(configParams); + for (let i in keysArr) { + let k = keysArr[i]; + 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) { + if (typeof v[j] === 'string' || typeof v[j] === 'number') { + url += `${k}=${v[j]}&`; + } + } + } 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 refs = new Map(); + _setReferrer(refs, referer.referer); + if (referer.stack && referer.stack[0]) { + _setReferrer(refs, referer.stack[0]); + } + _setReferrer(refs, referer.canonicalUrl); + _setReferrer(refs, window.location.href); + + refs.forEach(v => { + url = appendUrl(url, 'r', v, configParams); + }); + + const resp = function (callback) { + const callbacks = { + success: response => { + if (window.fabrickMod2) { + return window.fabrickMod2( + callback, response, configParams, consentData, cacheIdObj); + } else { + let responseObj; + if (response) { + try { + responseObj = JSON.parse(response); + } catch (error) { + utils.logError(error); + responseObj = {}; + } + } + callback(responseObj); + } + }, + error: error => { + utils.logError(`fabrickId fetch encountered an error`, error); + callback(); + } + }; + ajax(url, callbacks, null, {method: 'GET', withCredentials: true}); + }; + return {callback: resp}; + } catch (e) { + utils.logError(`fabrickIdSystem encountered an error`, e); + } + } catch (e) { + utils.logError(`fabrickIdSystem encountered an error`, e); + } + } +}; + +function _getRefererInfo(configParams) { + if (configParams.refererInfo) { + return configParams.refererInfo; + } else { + return getRefererInfo(); + } +} + +function _getBaseUrl(configParams) { + if (configParams.url) { + return configParams.url; + } else { + return `https://fid.agkn.com/f?`; + } +} + +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; + } + + 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; + } +} + +submodule('userId', fabrickIdSubmodule); diff --git a/modules/fabrickIdSystem.md b/modules/fabrickIdSystem.md new file mode 100644 index 00000000000..268c861710a --- /dev/null +++ b/modules/fabrickIdSystem.md @@ -0,0 +1,24 @@ +## Neustar Fabrick User ID Submodule + +Fabrick ID Module - https://www.home.neustar/fabrick +Product and Sales Inquiries: 1-855-898-0036 + +## Example configuration for publishers: +``` +pbjs.setConfig({ + userSync: { + userIds: [{ + name: 'fabrickId', + storage: { + name: 'pbjs_fabrickId', + type: 'cookie', + expires: 7 + }, + params: { + apiKey: 'your apiKey', // provided to you by Neustar + e: '31c5543c1734d25c7206f5fd591525d0295bec6fe84ff82f946a34fe970a1e66' // example hash identifier (sha256) + } + }] + } +}); +``` diff --git a/modules/feedadBidAdapter.js b/modules/feedadBidAdapter.js index 3992f2db5e0..54a4ef0c998 100644 --- a/modules/feedadBidAdapter.js +++ b/modules/feedadBidAdapter.js @@ -1,13 +1,13 @@ import * as utils from '../src/utils.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; -import {BANNER, VIDEO} from '../src/mediaTypes.js'; +import {BANNER} from '../src/mediaTypes.js'; import {ajax} from '../src/ajax.js'; /** * Version of the FeedAd bid adapter * @type {string} */ -const VERSION = '1.0.0'; +const VERSION = '1.0.2'; /** * @typedef {object} FeedAdApiBidRequest @@ -61,6 +61,11 @@ const VERSION = '1.0.0'; * @property [device_platform] {1|2|3} 1 - Android | 2 - iOS | 3 - Windows */ +/** + * The IAB TCF 2.0 vendor ID for the FeedAd GmbH + */ +const TCF_VENDOR_ID = 781; + /** * Bidder network identity code * @type {string} @@ -71,7 +76,7 @@ const BIDDER_CODE = 'feedad'; * The media types supported by FeedAd * @type {MediaType[]} */ -const MEDIA_TYPES = [VIDEO, BANNER]; +const MEDIA_TYPES = [BANNER]; /** * Tag for logging @@ -204,6 +209,10 @@ function buildRequests(validBidRequests, bidderRequest) { referer: data.refererInfo.referer, transactionId: bid.transactionId }); + if (bidderRequest.gdprConsent) { + data.consentIabTcf = bidderRequest.gdprConsent.consentString; + data.gdprApplies = bidderRequest.gdprConsent.gdprApplies; + } return { method: 'POST', url: `${API_ENDPOINT}${API_PATH_BID_REQUEST}`, @@ -279,6 +288,7 @@ function trackingHandlerFactory(klass) { */ export const spec = { code: BIDDER_CODE, + gvlid: TCF_VENDOR_ID, supportedMediaTypes: MEDIA_TYPES, isBidRequestValid, buildRequests, diff --git a/modules/feedadBidAdapter.md b/modules/feedadBidAdapter.md index fd57025c29e..6f705df36b5 100644 --- a/modules/feedadBidAdapter.md +++ b/modules/feedadBidAdapter.md @@ -18,9 +18,6 @@ Prebid.JS adapter that connects to the FeedAd demand sources. mediaTypes: { banner: { // supports all banner sizes sizes: [[300, 250]], - }, - video: { // supports only outstream video - context: 'outstream' } }, bids: [ 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/flocIdSystem.js b/modules/flocIdSystem.js new file mode 100644 index 00000000000..e4bd31e49df --- /dev/null +++ b/modules/flocIdSystem.js @@ -0,0 +1,110 @@ +/** + * This module adds flocId to the User ID module + * The {@link module:modules/userId} module is required + * @module modules/flocId + * @requires module:modules/userId + */ + +import * as utils from '../src/utils.js' +import {submodule} from '../src/hook.js' + +const MODULE_NAME = 'flocId'; + +/** + * Add meta tag to support enabling of floc origin trial + * @function + * @param {string} token - configured token for origin-trial + */ +function enableOriginTrial(token) { + const tokenElement = document.createElement('meta'); + tokenElement.httpEquiv = 'origin-trial'; + tokenElement.content = token; + document.head.appendChild(tokenElement); +} + +/** + * Get the interest cohort. + * @param successCallback + * @param errorCallback + */ +function getFlocData(successCallback, errorCallback) { + document.interestCohort() + .then((data) => { + successCallback(data); + }).catch((error) => { + errorCallback(error); + }); +} + +/** + * Encode the id + * @param value + * @returns {string|*} + */ +function encodeId(value) { + const result = {}; + if (value) { + result.flocId = value; + utils.logInfo('Decoded value ' + JSON.stringify(result)); + return result; + } + return undefined; +} + +/** @type {Submodule} */ +export const flocIdSubmodule = { + /** + * used to link submodule with config + * @type {string} + */ + name: MODULE_NAME, + + /** + * decode the stored id value for passing to bid requests + * @function + * @param {string} value + * @returns {{flocId:{ id: string }} or undefined if value doesn't exists + */ + decode(value) { + return (value) ? encodeId(value) : undefined; + }, + /** + * If chrome and cohort enabled performs action to obtain id and return a value in the callback's response argument + * @function + * @param {SubmoduleConfig} [config] + * @returns {IdResponse|undefined} + */ + getId(config) { + // Block usage of storage of cohort ID + const checkStorage = (config && config.storage); + if (checkStorage) { + utils.logError('User ID - flocId submodule storage should not defined'); + return; + } + // Validate feature is enabled + const isFlocEnabled = !!window.chrome && (!!window.chrome.webstore || !!window.chrome.runtime) && !!document.featurePolicy && !!document.featurePolicy.features() && document.featurePolicy.features().includes('interest-cohort'); + + if (isFlocEnabled) { + const configParams = (config && config.params) || {}; + if (configParams && (typeof configParams.token === 'string')) { + // Insert meta-tag with token from configuration + enableOriginTrial(configParams.token); + } + // Example expected output { "id": "14159", "version": "chrome.1.0" } + let returnCallback = (cb) => { + getFlocData((data) => { + returnCallback = () => { return data; } + utils.logInfo('Cohort id: ' + JSON.stringify(data)); + cb(data); + }, (err) => { + utils.logInfo(err); + cb(undefined); + }); + }; + + return {callback: returnCallback}; + } + } +}; + +submodule('userId', flocIdSubmodule); diff --git a/modules/flocIdSystem.md b/modules/flocIdSystem.md new file mode 100644 index 00000000000..07184700a14 --- /dev/null +++ b/modules/flocIdSystem.md @@ -0,0 +1,34 @@ +## FloC ID User ID Submodule + +### Building Prebid with Floc Id Support +Your Prebid build must include the modules for both **userId** and **flocIdSystem** submodule. Follow the build instructions for Prebid as +explained in the top level README.md file of the Prebid source tree. + +ex: $ gulp build --modules=userId,flocIdSystem + +### Prebid Params + +Individual params may be set for the FloC ID User ID Submodule. +``` +pbjs.setConfig({ + userSync: { + userIds: [{ + name: 'flocId', + params: { + token: "Registered token or default sharedid.org token" + } + }] + } +}); +``` + +### Parameter Descriptions for the `userSync` Configuration Section +The below parameters apply only to the FloC ID User ID Module integration. + +| Params under usersync.userIds[]| Scope | Type | Description | Example | +| --- | --- | --- | --- | --- | +| name | Required | String | ID value for the Floc ID module - `"flocId"` | `"flocId"` | +| params | Optional | Object | Details for flocId syncing. | | +| params.token | Optional | Object | Publisher registered token.To get new token, register https://developer.chrome.com/origintrials/#/trials/active for Federated Learning of Cohorts. Default sharedid.org token: token: "A3dHTSoNUMjjERBLlrvJSelNnwWUCwVQhZ5tNQ+sll7y+LkPPVZXtB77u2y7CweRIxiYaGwGXNlW1/dFp8VMEgIAAAB+eyJvcmlnaW4iOiJodHRwczovL3NoYXJlZGlkLm9yZzo0NDMiLCJmZWF0dXJlIjoiSW50ZXJlc3RDb2hvcnRBUEkiLCJleHBpcnkiOjE2MjYyMjA3OTksImlzU3ViZG9tYWluIjp0cnVlLCJpc1RoaXJkUGFydHkiOnRydWV9"| token: "A3dHTSoNUMjjERBLlrvJSelNnwWUCwVQhZ5tNQ+sll7y+LkPPVZXtB77u2y7CweRIxiYaGwGXNlW1/dFp8VMEgIAAAB+eyJvcmlnaW4iOiJodHRwczovL3NoYXJlZGlkLm9yZzo0NDMiLCJmZWF0dXJlIjoiSW50ZXJlc3RDb2hvcnRBUEkiLCJleHBpcnkiOjE2MjYyMjA3OTksImlzU3ViZG9tYWluIjp0cnVlLCJpc1RoaXJkUGFydHkiOnRydWV9" + | +| storage | Not Allowed | Object | Will ask browser for cohort everytime. Setting storage will fail id lookup || diff --git a/modules/fpdModule/index.js b/modules/fpdModule/index.js new file mode 100644 index 00000000000..427547a4e4d --- /dev/null +++ b/modules/fpdModule/index.js @@ -0,0 +1,58 @@ +/** + * This module sets default values and validates ortb2 first part data + * @module modules/firstPartyData + */ +import { config } from '../../src/config.js'; +import { module, getHook } from '../../src/hook.js'; +import { getGlobal } from '../../src/prebidGlobal.js'; +import { addBidderRequests } from '../../src/auction.js'; + +let submodules = []; + +/** + * enable submodule in User ID + * @param {RtdSubmodule} submodule + */ +export function registerSubmodules(submodule) { + submodules.push(submodule); +} + +export function init() { + let modConf = config.getConfig('firstPartyData') || {}; + let ortb2 = config.getConfig('ortb2') || {}; + + submodules.sort((a, b) => { + return ((a.queue || 1) - (b.queue || 1)); + }).forEach(submodule => { + ortb2 = submodule.init(modConf, ortb2); + }); + + config.setConfig({ortb2}); +} + +/** + * BidderRequests hook to intiate module and reset modules ortb2 data object + */ +function addBidderRequestHook(fn, bidderRequests) { + init(); + fn.call(this, bidderRequests); + // Removes hook after run + addBidderRequests.getHooks({ hook: addBidderRequestHook }).remove(); +} + +/** + * Sets bidderRequests hook + */ +function setupHook() { + getHook('addBidderRequests').before(addBidderRequestHook); +} + +module('firstPartyData', registerSubmodules); + +// Runs setupHook on initial load +setupHook(); + +/** + * Global function to reinitiate module + */ +(getGlobal()).refreshFpd = setupHook; diff --git a/modules/fpdModule/index.md b/modules/fpdModule/index.md new file mode 100644 index 00000000000..638c966883a --- /dev/null +++ b/modules/fpdModule/index.md @@ -0,0 +1,46 @@ +# Overview + +``` +Module Name: First Party Data Module +``` + +# Description + +Module to perform the following functions to allow for consistent set of first party data using the following submodules. + +Enrichment Submodule: +- populate available data into object: referer, meta-keywords, cur + +Validation Submodule: +- verify OpenRTB datatypes, remove/warn any that are likely to choke downstream readers +- verify that certain OpenRTB attributes are not specified +- optionally suppress user FPD based on the existence of _pubcid_optout + + +1. Module initializes on first load and set bidRequestHook +2. When hook runs, corresponding submodule init functions are run to perform enrichments/validations dependant on submodule +3. After hook complete, it is disabled - meaning module only runs on first auction +4. To reinitiate the module, run pbjs.refreshFPD(), which allows module to rerun as if initial load + + +This module will automatically run first party data enrichments and validations dependant on which submodules are included. There is no configuration required. In order to load the module and submodule(s) and opt out of either enrichements or validations, use the below opt out configuration + +# Module Control Configuration + +``` + +pbjs.setConfig({ + firstPartyData: { + skipValidations: true, // default to false + skipEnrichments: true // default to false + } +}); + +``` + +# Requirements + +At least one of the submodules must be included in order to successfully run the corresponding above operations. + +enrichmentFpdModule +validationFpdModule \ No newline at end of file diff --git a/modules/freewheel-sspBidAdapter.js b/modules/freewheel-sspBidAdapter.js index dce678362cb..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; +} + +function getDealId(xmlNode) { + return getValueFromKeyInImpressionNode(xmlNode, 'dealId'); +} + +function getBannerId(xmlNode) { + return getValueFromKeyInImpressionNode(xmlNode, 'adId'); +} - return dealId; +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) { @@ -407,16 +437,25 @@ export const spec = { return bidResponses; }, - getUserSyncs: function(syncOptions) { + getUserSyncs: function(syncOptions, responses, gdprConsent, usPrivacy) { + var gdprParams = ''; + if (gdprConsent) { + if (typeof gdprConsent.gdprApplies === 'boolean') { + gdprParams = `?gdpr=${Number(gdprConsent.gdprApplies)}&gdpr_consent=${gdprConsent.consentString}`; + } else { + gdprParams = `?gdpr_consent=${gdprConsent.consentString}`; + } + } + if (syncOptions && syncOptions.pixelEnabled) { return [{ type: 'image', - url: USER_SYNC_URL + url: USER_SYNC_URL + gdprParams }]; } else { return []; } }, +}; -} registerBidder(spec); diff --git a/modules/gamoshiBidAdapter.js b/modules/gamoshiBidAdapter.js index 1316d74e430..de839219897 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' @@ -42,7 +43,7 @@ export const helper = { export const spec = { code: 'gamoshi', - aliases: ['gambid', 'cleanmedia', '9MediaOnline'], + aliases: ['gambid', '9MediaOnline'], supportedMediaTypes: ['banner', 'video'], isBidRequestValid: function (bid) { @@ -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) { @@ -157,7 +158,7 @@ export const spec = { let eids = []; if (bidRequest && bidRequest.userId) { - addExternalUserId(eids, utils.deepAccess(bidRequest, `userId.id5id`), 'id5-sync.com', 'ID5ID'); + addExternalUserId(eids, utils.deepAccess(bidRequest, `userId.id5id.uid`), 'id5-sync.com', 'ID5ID'); addExternalUserId(eids, utils.deepAccess(bidRequest, `userId.tdid`), 'adserver.org', 'TDID'); } if (eids.length > 0) { diff --git a/modules/gamoshiBidAdapter.md b/modules/gamoshiBidAdapter.md index 6e930375059..49b727cecae 100644 --- a/modules/gamoshiBidAdapter.md +++ b/modules/gamoshiBidAdapter.md @@ -3,7 +3,7 @@ ``` Module Name: Gamoshi Bid Adapter Module Type: Bidder Adapter -Maintainer: salomon@gamoshi.com +Maintainer: dev@gamoshi.com ``` # Description diff --git a/modules/gdprEnforcement.js b/modules/gdprEnforcement.js index 97eaedd92be..02a2da3a7a4 100644 --- a/modules/gdprEnforcement.js +++ b/modules/gdprEnforcement.js @@ -12,7 +12,7 @@ import { registerSyncInner } from '../src/adapters/bidderFactory.js'; import { getHook } from '../src/hook.js'; import { validateStorageEnforcement } from '../src/storageManager.js'; import events from '../src/events.js'; -import { EVENTS } from '../src/constants.json'; +import CONSTANTS from '../src/constants.json'; const TCF2 = { 'purpose1': { id: 1, name: 'storage' }, @@ -47,58 +47,75 @@ const analyticsBlocked = []; let addedDeviceAccessHook = false; +// Helps in stubbing these functions in unit tests. +export const internal = { + getGvlidForBidAdapter, + getGvlidForUserIdModule, + getGvlidForAnalyticsAdapter +}; + /** - * Returns gvlId for Bid Adapters. If a bidder does not have an associated gvlId, it returns 'undefined'. - * @param {string=} bidderCode - The 'code' property on the Bidder spec. - * @retuns {number} gvlId + * Returns GVL ID for a Bid adapter / an USERID submodule / an Analytics adapter. + * If modules of different types have the same moduleCode: For example, 'appnexus' is the code for both Bid adapter and Analytics adapter, + * then, we assume that their GVL IDs are same. This function first checks if GVL ID is defined for a Bid adapter, if not found, tries to find User ID + * submodule's GVL ID, if not found, tries to find Analytics adapter's GVL ID. In this process, as soon as it finds a GVL ID, it returns it + * without going to the next check. + * @param {{string|Object}} - module + * @return {number} - GVL ID */ -function getGvlid(bidderCode) { - let gvlid; +export function getGvlid(module) { + let gvlid = null; + if (module) { + // Check user defined GVL Mapping in pbjs.setConfig() + const gvlMapping = config.getConfig('gvlMapping'); + + // For USER ID Module, we pass the submodule object itself as the "module" parameter, this check is required to grab the module code + const moduleCode = typeof module === 'string' ? module : module.name; + + // Return GVL ID from user defined gvlMapping + if (gvlMapping && gvlMapping[moduleCode]) { + gvlid = gvlMapping[moduleCode]; + return gvlid; + } + + gvlid = internal.getGvlidForBidAdapter(moduleCode) || internal.getGvlidForUserIdModule(module) || internal.getGvlidForAnalyticsAdapter(moduleCode); + } + return gvlid; +} + +/** + * Returns GVL ID for a bid adapter. If the adapter does not have an associated GVL ID, it returns 'null'. + * @param {string=} bidderCode - The 'code' property of the Bidder spec. + * @return {number} GVL ID + */ +function getGvlidForBidAdapter(bidderCode) { + let gvlid = null; bidderCode = bidderCode || config.getCurrentBidder(); if (bidderCode) { - const gvlMapping = config.getConfig('gvlMapping'); - if (gvlMapping && gvlMapping[bidderCode]) { - gvlid = gvlMapping[bidderCode]; - } else { - const bidder = adapterManager.getBidAdapter(bidderCode); - if (bidder && bidder.getSpec) { - gvlid = bidder.getSpec().gvlid; - } + const bidder = adapterManager.getBidAdapter(bidderCode); + if (bidder && bidder.getSpec) { + gvlid = bidder.getSpec().gvlid; } } return gvlid; } /** - * Returns gvlId for userId module. If a userId modules does not have an associated gvlId, it returns 'undefined'. + * Returns GVL ID for an userId submodule. If an userId submodules does not have an associated GVL ID, it returns 'null'. * @param {Object} userIdModule - * @retuns {number} gvlId + * @return {number} GVL ID */ function getGvlidForUserIdModule(userIdModule) { - let gvlId; - const gvlMapping = config.getConfig('gvlMapping'); - if (gvlMapping && gvlMapping[userIdModule.name]) { - gvlId = gvlMapping[userIdModule.name]; - } else { - gvlId = userIdModule.gvlid; - } - return gvlId; + return (typeof userIdModule === 'object' ? userIdModule.gvlid : null); } /** - * Returns gvlId for analytics adapters. If a analytics adapter does not have an associated gvlId, it returns 'undefined'. + * Returns GVL ID for an analytics adapter. If an analytics adapter does not have an associated GVL ID, it returns 'null'. * @param {string} code - 'provider' property on the analytics adapter config - * @returns {number} gvlId + * @return {number} GVL ID */ function getGvlidForAnalyticsAdapter(code) { - let gvlId; - const gvlMapping = config.getConfig('gvlMapping'); - if (gvlMapping && gvlMapping[code]) { - gvlId = gvlMapping[code]; - } else { - gvlId = adapterManager.getAnalyticsAdapter(code).gvlid; - } - return gvlId; + return adapterManager.getAnalyticsAdapter(code) && (adapterManager.getAnalyticsAdapter(code).gvlid || null); } /** @@ -165,7 +182,7 @@ export function deviceAccessHook(fn, gvlid, moduleName, result) { if (curBidder && (curBidder != moduleName) && adapterManager.aliasRegistry[curBidder] === moduleName) { gvlid = getGvlid(curBidder); } else { - gvlid = getGvlid(moduleName); + gvlid = getGvlid(moduleName) || gvlid; } const curModule = moduleName || curBidder; let isAllowed = validateRules(purpose1Rule, consentData, curModule, gvlid); @@ -199,8 +216,8 @@ export function userSyncHook(fn, ...args) { const consentData = gdprDataHandler.getConsentData(); if (consentData && consentData.gdprApplies) { if (consentData.apiVersion === 2) { - const gvlid = getGvlid(); const curBidder = config.getCurrentBidder(); + const gvlid = getGvlid(curBidder); let isAllowed = validateRules(purpose1Rule, consentData, curBidder, gvlid); if (isAllowed) { fn.call(this, ...args); @@ -227,7 +244,7 @@ export function userIdHook(fn, submodules, consentData) { if (consentData && consentData.gdprApplies) { if (consentData.apiVersion === 2) { let userIdModules = submodules.map((submodule) => { - const gvlid = getGvlidForUserIdModule(submodule.submodule); + const gvlid = getGvlid(submodule.submodule); const moduleName = submodule.submodule.name; let isAllowed = validateRules(purpose1Rule, consentData, moduleName, gvlid); if (isAllowed) { @@ -296,7 +313,7 @@ export function enableAnalyticsHook(fn, config) { } config = config.filter(conf => { const analyticsAdapterCode = conf.provider; - const gvlid = getGvlidForAnalyticsAdapter(analyticsAdapterCode); + const gvlid = getGvlid(analyticsAdapterCode); const isAllowed = !!validateRules(purpose7Rule, consentData, analyticsAdapterCode, gvlid); if (!isAllowed) { analyticsBlocked.push(analyticsAdapterCode); @@ -328,10 +345,10 @@ function emitTCF2FinalResults() { analyticsBlocked: formatArray(analyticsBlocked) }; - events.emit(EVENTS.TCF2_ENFORCEMENT, tcf2FinalResults); + events.emit(CONSTANTS.EVENTS.TCF2_ENFORCEMENT, tcf2FinalResults); } -events.on(EVENTS.AUCTION_END, emitTCF2FinalResults); +events.on(CONSTANTS.EVENTS.AUCTION_END, emitTCF2FinalResults); /* Set of callback functions used to detect presence of a TCF rule, passed as the second argument to find(). @@ -347,7 +364,7 @@ const hasPurpose7 = (rule) => { return rule.purpose === TCF2.purpose7.name } export function setEnforcementConfig(config) { const rules = utils.deepAccess(config, 'gdpr.rules'); if (!rules) { - utils.logWarn('TCF2: enforcing P1 and P2'); + utils.logWarn('TCF2: enforcing P1 and P2 by default'); enforcementRules = DEFAULT_RULES; } else { enforcementRules = rules; diff --git a/modules/geoedgeRtdProvider.js b/modules/geoedgeRtdProvider.js new file mode 100644 index 00000000000..001ef67b66a --- /dev/null +++ b/modules/geoedgeRtdProvider.js @@ -0,0 +1,213 @@ +/** + * This module adds geoedge provider to the real time data module + * The {@link module:modules/realTimeData} module is required + * The module will fetch creative wrapper from geoedge server + * The module will place geoedge RUM client on bid responses markup + * @module modules/geoedgeProvider + * @requires module:modules/realTimeData + */ + +/** + * @typedef {Object} ModuleParams + * @property {string} key + * @property {?Object} bidders + * @property {?boolean} wap + * @property {?string} keyName + */ + +import { submodule } from '../src/hook.js'; +import { ajax } from '../src/ajax.js'; +import { generateUUID, insertElement, isEmpty, logError } from '../src/utils.js'; + +/** @type {string} */ +const SUBMODULE_NAME = 'geoedge'; +/** @type {string} */ +export const WRAPPER_URL = 'https://wrappers.geoedge.be/wrapper.html'; +/** @type {string} */ +/* eslint-disable no-template-curly-in-string */ +export const HTML_PLACEHOLDER = '${creative}'; +/** @type {string} */ +const PV_ID = generateUUID(); +/** @type {string} */ +const HOST_NAME = 'https://rumcdn.geoedge.be'; +/** @type {string} */ +const FILE_NAME = 'grumi.js'; +/** @type {function} */ +export let getClientUrl = (key) => `${HOST_NAME}/${key}/${FILE_NAME}`; +/** @type {string} */ +export let wrapper +/** @type {boolean} */; +let wrapperReady; +/** @type {boolean} */; +let preloaded; + +/** + * fetches the creative wrapper + * @param {function} sucess - success callback + */ +export function fetchWrapper(success) { + if (wrapperReady) { + return success(wrapper); + } + ajax(WRAPPER_URL, success); +} + +/** + * sets the wrapper and calls preload client + * @param {string} responseText + */ +export function setWrapper(responseText) { + wrapperReady = true; + wrapper = responseText; +} + +/** + * preloads the client + * @param {string} key + */ +export function preloadClient(key) { + let link = document.createElement('link'); + link.rel = 'preload'; + link.as = 'script'; + link.href = getClientUrl(key); + link.onload = () => { preloaded = true }; + insertElement(link); +} + +/** + * creates identity function for string replace without special replacement patterns + * @param {string} str + * @return {function} + */ +function replacer(str) { + return function () { + return str; + } +} + +export function wrapHtml(wrapper, html) { + return wrapper.replace(HTML_PLACEHOLDER, replacer(html)); +} + +/** + * generate macros dictionary from bid response + * @param {Object} bid + * @param {string} key + * @return {Object} + */ +function getMacros(bid, key) { + return { + '${key}': key, + '%%ADUNIT%%': bid.adUnitCode, + '%%WIDTH%%': bid.width, + '%%HEIGHT%%': bid.height, + '%%PATTERN:hb_adid%%': bid.adId, + '%%PATTERN:hb_bidder%%': bid.bidderCode, + '%_isHb!': true, + '%_hbcid!': bid.creativeId || '', + '%%PATTERN:hb_pb%%': bid.pbHg, + '%%SITE%%': location.hostname, + '%_pimp%': PV_ID + }; +} + +/** + * replace macro placeholders in a string with values from a dictionary + * @param {string} wrapper + * @param {Object} macros + * @return {string} + */ +function replaceMacros(wrapper, macros) { + var re = new RegExp('\\' + Object.keys(macros).join('|'), 'gi'); + + return wrapper.replace(re, function(matched) { + return macros[matched]; + }); +} + +/** + * build final creative html with creative wrapper + * @param {Object} bid + * @param {string} wrapper + * @param {string} html + * @return {string} + */ +function buildHtml(bid, wrapper, html, key) { + let macros = getMacros(bid, key); + wrapper = replaceMacros(wrapper, macros); + return wrapHtml(wrapper, html); +} + +/** + * muatates the bid ad property + * @param {Object} bid + * @param {string} ad + */ +function mutateBid(bid, ad) { + bid.ad = ad; +} + +/** + * wraps a bid object with the creative wrapper + * @param {Object} bid + * @param {string} key + */ +export function wrapBidResponse(bid, key) { + let wrapped = buildHtml(bid, wrapper, bid.ad, key); + mutateBid(bid, wrapped); +} + +/** + * checks if bidder's bids should be monitored + * @param {string} bidder + * @return {boolean} + */ +function isSupportedBidder(bidder, paramsBidders) { + return isEmpty(paramsBidders) || paramsBidders[bidder] === true; +} + +/** + * checks if bid should be monitored + * @param {Object} bid + * @return {boolean} + */ +function shouldWrap(bid, params) { + let supportedBidder = isSupportedBidder(bid.bidderCode, params.bidders); + let donePreload = params.wap ? preloaded : true; + return wrapperReady && supportedBidder && donePreload; +} + +function conditionallyWrap(bidResponse, config, userConsent) { + let params = config.params; + if (shouldWrap(bidResponse, params)) { + wrapBidResponse(bidResponse, params.key); + } +} + +function init(config, userConsent) { + let params = config.params; + if (!params || !params.key) { + logError('missing key for geoedge RTD module provider'); + return false; + } + preloadClient(params.key); + return true; +} + +/** @type {RtdSubmodule} */ +export const geoedgeSubmodule = { + /** + * used to link submodule with realTimeData + * @type {string} + */ + name: SUBMODULE_NAME, + init, + onBidResponseEvent: conditionallyWrap +}; + +export function beforeInit() { + fetchWrapper(setWrapper); + submodule('realTimeData', geoedgeSubmodule); +} + +beforeInit(); diff --git a/modules/geoedgeRtdProvider.md b/modules/geoedgeRtdProvider.md new file mode 100644 index 00000000000..5414606612c --- /dev/null +++ b/modules/geoedgeRtdProvider.md @@ -0,0 +1,67 @@ +## Overview + +Module Name: Geoedge Rtd provider +Module Type: Rtd Provider +Maintainer: guy.books@geoedge.com + +The Geoedge Realtime module lets publishers block bad ads such as automatic redirects, malware, offensive creatives and landing pages. +To use this module, you'll need to work with [Geoedge](https://www.geoedge.com/publishers-real-time-protection/) to get an account and cutomer key. + +## Integration + +1) Build the geoedge RTD module into the Prebid.js package with: + +``` +gulp build --modules=geoedgeRtdProvider,... +``` + +2) Use `setConfig` to instruct Prebid.js to initilize the geoedge module, as specified below. + +## Configuration + +This module is configured as part of the `realTimeData.dataProviders` object: + +```javascript +pbjs.setConfig({ + realTimeData: { + dataProviders: [{ + name: 'geoedge', + params: { + key: '123123', + bidders: { + 'bidderA': true, // monitor bids form this bidder + 'bidderB': false // do not monitor bids form this bidder. + }, + wap: true + } + }] + } +}); +``` + +Parameters details: + +{: .table .table-bordered .table-striped } +|Name |Type |Description |Notes | +| :------------ | :------------ | :------------ |:------------ | +|name | String | Real time data module name |Required, always 'geoedge' | +|params | Object | | | +|params.key | String | Customer key |Required, contact Geoedge to get your key | +|params.bidders | Object | Bidders to monitor |Optional, list of bidder to include / exclude from monitoring. Omitting this will monitor bids from all bidders. | +|params.wap |Boolean |Wrap after preload |Optional, defaults to `false`. Set to `true` if you want to monitor only after the module has preloaded the monitoring client. | + +## Example + +To view an integration example: + +1) in your cli run: + +``` +gulp serve --modules=appnexusBidAdapter,geoedgeRtdProvider +``` + +2) in your browser, navigate to: + +``` +http://localhost:9999/integrationExamples/gpt/geoedgeRtdProvider_example.html +``` diff --git a/modules/gjirafaBidAdapter.js b/modules/gjirafaBidAdapter.js new file mode 100644 index 00000000000..77589cd9071 --- /dev/null +++ b/modules/gjirafaBidAdapter.js @@ -0,0 +1,116 @@ +import { registerBidder } from '../src/adapters/bidderFactory.js'; +import { BANNER, VIDEO } from '../src/mediaTypes.js'; + +const BIDDER_CODE = 'gjirafa'; +const ENDPOINT_URL = 'https://central.gjirafa.com/bid'; +const DIMENSION_SEPARATOR = 'x'; +const SIZE_SEPARATOR = ';'; + +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: function (bid) { + return !!(bid.params.propertyId && bid.params.placementId); + }, + /** + * Make a server request from the list of BidRequests. + * + * @param {validBidRequests[]} - an array of bids + * @return ServerRequest Info describing the request to the server. + */ + buildRequests: function (validBidRequests, bidderRequest) { + let propertyId = ''; + let pageViewGuid = ''; + let storageId = ''; + let bidderRequestId = ''; + let url = ''; + let contents = []; + let data = {}; + + let placements = validBidRequests.map(bidRequest => { + if (!propertyId) { propertyId = bidRequest.params.propertyId; } + if (!pageViewGuid && bidRequest.params) { pageViewGuid = bidRequest.params.pageViewGuid || ''; } + 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 (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; + let sizes = generateSizeParam(bidRequest.sizes); + + return { + sizes: sizes, + adUnitId: adUnitId, + placementId: placementId, + bidid: bidRequest.bidId, + count: bidRequest.params.count, + skipTime: bidRequest.params.skipTime + }; + }); + + let body = { + propertyId: propertyId, + pageViewGuid: pageViewGuid, + storageId: storageId, + url: url, + requestid: bidderRequestId, + placements: placements, + contents: contents, + data: data + } + + return [{ + method: 'POST', + url: ENDPOINT_URL, + data: body + }]; + }, + /** + * Unpack the response from the server into a list of bids. + * + * @param {ServerResponse} serverResponse A successful response from the server. + * @return {Bid[]} An array of bids which were nested inside the server. + */ + interpretResponse: function (serverResponse) { + const responses = serverResponse.body; + const bidResponses = []; + for (var i = 0; i < responses.length; i++) { + const bidResponse = { + requestId: responses[i].BidId, + cpm: responses[i].CPM, + width: responses[i].Width, + height: responses[i].Height, + creativeId: responses[i].CreativeId, + currency: responses[i].Currency, + netRevenue: responses[i].NetRevenue, + ttl: responses[i].TTL, + referrer: responses[i].Referrer, + ad: responses[i].Ad, + vastUrl: responses[i].VastUrl, + mediaType: responses[i].MediaType + }; + bidResponses.push(bidResponse); + } + return bidResponses; + } +} + +/** +* Generate size param for bid request using sizes array +* +* @param {Array} sizes Possible sizes for the ad unit. +* @return {string} Processed sizes param to be used for the bid request. +*/ +function generateSizeParam(sizes) { + return sizes.map(size => size.join(DIMENSION_SEPARATOR)).join(SIZE_SEPARATOR); +} + +registerBidder(spec); diff --git a/modules/gjirafaBidAdapter.md b/modules/gjirafaBidAdapter.md index 1ec8222d8de..fb4960d61f6 100644 --- a/modules/gjirafaBidAdapter.md +++ b/modules/gjirafaBidAdapter.md @@ -1,36 +1,67 @@ # Overview -Module Name: Gjirafa Bidder Adapter Module -Type: Bidder Adapter -Maintainer: agonq@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', - sizes: [[728, 90]], // leaderboard - bids: [ - { - bidder: 'gjirafa', - params: { - placementId: '71-3' - } - } - ] -},{ - code: 'test-div', - sizes: [[300, 250]], // mobile rectangle - bids: [ - { - bidder: 'gjirafa', - params: { - minCPM: 0.0001, - minCPC: 0.001, - explicit: true - } - } - ] -} -]; \ No newline at end of file + { + code: 'test-div', + mediaTypes: { + banner: { + sizes: [ + [728, 90] + ] + } + }, + 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: { + context: 'instream' + } + }, + 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/gnetBidAdapter.js b/modules/gnetBidAdapter.js new file mode 100644 index 00000000000..3469c897a6a --- /dev/null +++ b/modules/gnetBidAdapter.js @@ -0,0 +1,101 @@ +import { registerBidder } from '../src/adapters/bidderFactory.js'; +import * as utils from '../src/utils.js'; +import { BANNER } from '../src/mediaTypes.js'; + +const BIDDER_CODE = 'gnet'; +const ENDPOINT = 'https://adserver.gnetproject.com/prebid.php'; + +export const spec = { + code: BIDDER_CODE, + supportedMediaTypes: [BANNER], + + /** + * Determines whether or not the given bid request is valid. + * + * @param {BidRequest} bid The bid params to validate. + * @return boolean True if this is a valid bid, and false otherwise. + */ + isBidRequestValid: function (bid) { + return !!(bid.params.websiteId && bid.params.externalId); + }, + + /** + * Make a server request from the list of BidRequests. + * + * @param {validBidRequests[]} - an array of bids + * @return ServerRequest Info describing the request to the server. + */ + buildRequests: function (validBidRequests, bidderRequest) { + const bidRequests = []; + const referer = bidderRequest.refererInfo.referer; + + utils._each(validBidRequests, (request) => { + const data = {}; + + data.referer = referer; + data.adUnitCode = request.adUnitCode; + data.bidId = request.bidId; + data.transactionId = request.transactionId; + + data.sizes = utils.parseSizesInput(request.sizes); + + data.params = request.params; + + const payloadString = JSON.stringify(data); + + bidRequests.push({ + method: 'POST', + url: ENDPOINT, + mode: 'no-cors', + options: { + withCredentials: false, + }, + data: payloadString + }); + }); + + return bidRequests; + }, + + /** + * Unpack the response from the server into a list of bids. + * + * @param {*} serverResponse A successful response from the server. + * @return {Bid[]} An array of bids which were nested inside the server. + */ + interpretResponse: function (serverResponse, requests) { + if (typeof serverResponse !== 'object') { + return []; + } + + const res = serverResponse && serverResponse.body; + + if (utils.isEmpty(res)) { + return []; + } + + if (res.bids) { + const bids = []; + utils._each(res.bids, (bidData) => { + const bid = { + requestId: bidData.bidId, + cpm: bidData.cpm, + currency: bidData.currency, + width: bidData.width, + height: bidData.height, + ad: bidData.ad, + ttl: 300, + creativeId: bidData.creativeId, + netRevenue: true, + }; + bids.push(bid); + }); + + return bids; + } + + return []; + }, +}; + +registerBidder(spec); diff --git a/modules/gnetBidAdapter.md b/modules/gnetBidAdapter.md new file mode 100644 index 00000000000..6dac9be17b6 --- /dev/null +++ b/modules/gnetBidAdapter.md @@ -0,0 +1,33 @@ +# Overview + +``` +Module Name: Gnet Bidder Adapter +Module Type: Bidder Adapter +Maintainer: roberto.wu@grumft.com +``` + +# Description + +Module that connects to Example's demand sources + +# Test Parameters +``` + var adUnits = [ + { + code: '/150790500/4_ZONA_IAB_300x250_5', + mediaTypes: { + banner: { + sizes: [[300, 250]], + } + }, + bids: [ + { + bidder: 'gnet', + params: { + websiteId: '4', + externalId: '4d52cccf30309282256012cf30309282' + } + } + ] + } + ]; \ No newline at end of file diff --git a/modules/gothamadsBidAdapter.js b/modules/gothamadsBidAdapter.js new file mode 100644 index 00000000000..ff6fa5221f3 --- /dev/null +++ b/modules/gothamadsBidAdapter.js @@ -0,0 +1,342 @@ +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 + }, + regs: { + coppa: config.getConfig('coppa') === true ? 1 : 0, + ext: {} + }, + tmax: bidRequest.timeout, + imp: [impObject], + }; + + if (bidRequest.gdprConsent && bidRequest.gdprConsent.gdprApplies) { + utils.deepSetValue(data, 'regs.ext.gdpr', bidRequest.gdprConsent.gdprApplies ? 1 : 0); + utils.deepSetValue(data, 'user.ext.consent', bidRequest.gdprConsent.consentString); + } + + if (bidRequest.uspConsent !== undefined) { + utils.deepSetValue(data, 'regs.ext.us_privacy', bidRequest.uspConsent); + } + + 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 GothamAdsResponse = serverResponse.body; + + let bids = []; + for (let response of GothamAdsResponse) { + let mediaType = response.seatbid[0].bid[0].ext && response.seatbid[0].bid[0].ext.mediaType ? response.seatbid[0].bid[0].ext.mediaType : BANNER; + + let bid = { + requestId: response.id, + cpm: response.seatbid[0].bid[0].price, + width: response.seatbid[0].bid[0].w, + height: response.seatbid[0].bid[0].h, + ttl: response.ttl || 1200, + currency: response.cur || 'USD', + netRevenue: true, + creativeId: response.seatbid[0].bid[0].crid, + dealId: response.seatbid[0].bid[0].dealid, + mediaType: mediaType + }; + + bid.meta = {}; + if (response.seatbid[0].bid[0].adomain && response.seatbid[0].bid[0].adomain.length > 0) { + bid.meta.advertiserDomains = response.seatbid[0].bid[0].adomain; + } + + switch (mediaType) { + case VIDEO: + bid.vastXml = response.seatbid[0].bid[0].adm; + bid.vastUrl = response.seatbid[0].bid[0].ext.vastUrl; + break; + case NATIVE: + bid.native = parseNative(response.seatbid[0].bid[0].adm); + break; + default: + bid.ad = response.seatbid[0].bid[0].adm; + } + + bids.push(bid); + } + + 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/gptPreAuction.js b/modules/gptPreAuction.js index 48b72671d6a..ee2b5406453 100644 --- a/modules/gptPreAuction.js +++ b/modules/gptPreAuction.js @@ -26,46 +26,48 @@ export const appendGptSlots = adUnits => { if (matchingAdUnitCode) { const adUnit = adUnitMap[matchingAdUnitCode]; - adUnit.fpd = adUnit.fpd || {}; - adUnit.fpd.context = adUnit.fpd.context || {}; + adUnit.ortb2Imp = adUnit.ortb2Imp || {}; + adUnit.ortb2Imp.ext = adUnit.ortb2Imp.ext || {}; + adUnit.ortb2Imp.ext.data = adUnit.ortb2Imp.ext.data || {}; - const context = adUnit.fpd.context; - context.adServer = context.adServer || {}; - context.adServer.name = 'gam'; - context.adServer.adSlot = slot.getAdUnitPath(); + const context = adUnit.ortb2Imp.ext.data; + context.adserver = context.adserver || {}; + context.adserver.name = 'gam'; + context.adserver.adslot = slot.getAdUnitPath(); } }); }; export const appendPbAdSlot = adUnit => { - adUnit.fpd = adUnit.fpd || {}; - adUnit.fpd.context = adUnit.fpd.context || {}; - const context = adUnit.fpd.context; + adUnit.ortb2Imp = adUnit.ortb2Imp || {}; + adUnit.ortb2Imp.ext = adUnit.ortb2Imp.ext || {}; + adUnit.ortb2Imp.ext.data = adUnit.ortb2Imp.ext.data || {}; + const context = adUnit.ortb2Imp.ext.data; const { customPbAdSlot } = _currentConfig; if (customPbAdSlot) { - context.pbAdSlot = customPbAdSlot(adUnit.code, utils.deepAccess(context, 'adServer.adSlot')); + context.pbadslot = customPbAdSlot(adUnit.code, utils.deepAccess(context, 'adserver.adslot')); return; } // use context.pbAdSlot if set - if (context.pbAdSlot) { + if (context.pbadslot) { return; } // use data attribute 'data-adslotid' if set try { const adUnitCodeDiv = document.getElementById(adUnit.code); if (adUnitCodeDiv.dataset.adslotid) { - context.pbAdSlot = adUnitCodeDiv.dataset.adslotid; + context.pbadslot = adUnitCodeDiv.dataset.adslotid; return; } } catch (e) {} // banner adUnit, use GPT adunit if defined - if (context.adServer) { - context.pbAdSlot = context.adServer.adSlot; + if (utils.deepAccess(context, 'adserver.adslot')) { + context.pbadslot = context.adserver.adslot; return; } - context.pbAdSlot = adUnit.code; + context.pbadslot = adUnit.code; }; export const makeBidRequestsHook = (fn, adUnits, ...args) => { diff --git a/modules/gridBidAdapter.js b/modules/gridBidAdapter.js index b4b741ac783..955aeff7168 100644 --- a/modules/gridBidAdapter.js +++ b/modules/gridBidAdapter.js @@ -1,12 +1,11 @@ import * as utils from '../src/utils.js'; -import {registerBidder} from '../src/adapters/bidderFactory.js'; +import { registerBidder } from '../src/adapters/bidderFactory.js'; import { Renderer } from '../src/Renderer.js'; import { VIDEO, BANNER } from '../src/mediaTypes.js'; -import {config} from '../src/config.js'; +import { config } from '../src/config.js'; const BIDDER_CODE = 'grid'; -const ENDPOINT_URL = 'https://grid.bidswitch.net/hb'; -const NEW_ENDPOINT_URL = 'https://grid.bidswitch.net/hbjson'; +const ENDPOINT_URL = 'https://grid.bidswitch.net/hbjson'; const SYNC_URL = 'https://x.bidswitch.net/sync?ssp=themediagrid'; const TIME_TO_LIVE = 360; const RENDERER_URL = 'https://acdn.adnxs.com/video/outstream/ANOutstreamVideo.js'; @@ -44,19 +43,196 @@ export const spec = { * @return {ServerRequest[]} Info describing the request to the server. */ buildRequests: function(validBidRequests, bidderRequest) { - const oldFormatBids = []; - const newFormatBids = []; + if (!validBidRequests.length) { + return null; + } + let pageKeywords = null; + let jwpseg = null; + 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 || {}; + + const referer = refererInfo ? encodeURIComponent(refererInfo.referer) : ''; + const imp = []; + const bidsMap = {}; + validBidRequests.forEach((bid) => { - bid.params.useNewFormat ? newFormatBids.push(bid) : oldFormatBids.push(bid); + if (!bidderRequestId) { + bidderRequestId = bid.bidderRequestId; + } + if (!auctionId) { + auctionId = bid.auctionId; + } + if (!schain) { + schain = bid.schain; + } + if (!userId) { + userId = bid.userId; + } + if (!userIdAsEids) { + userIdAsEids = bid.userIdAsEids; + } + const {params: {uid, keywords}, mediaTypes, bidId, adUnitCode, rtd, ortb2Imp} = bid; + bidsMap[bidId] = bid; + if (!pageKeywords && !utils.isEmpty(keywords)) { + pageKeywords = utils.transformBidderParamKeywords(keywords); + } + const bidFloor = _getFloor(mediaTypes || {}, bid); + const jwTargeting = rtd && rtd.jwplayer && rtd.jwplayer.targeting; + if (jwTargeting) { + if (!jwpseg && jwTargeting.segments) { + jwpseg = jwTargeting.segments; + } + if (!content && jwTargeting.content) { + content = jwTargeting.content; + } + } + let impObj = { + id: bidId, + tagid: uid.toString(), + ext: { + divid: adUnitCode + } + }; + if (ortb2Imp && ortb2Imp.ext && ortb2Imp.ext.data) { + impObj.ext.data = ortb2Imp.ext.data; + if (impObj.ext.data.adserver && impObj.ext.data.adserver.adslot) { + impObj.ext.gpid = impObj.ext.data.adserver.adslot; + } + } + + if (bidFloor) { + impObj.bidfloor = bidFloor; + } + + if (!mediaTypes || mediaTypes[BANNER]) { + const banner = createBannerRequest(bid, mediaTypes ? mediaTypes[BANNER] : {}); + if (banner) { + impObj.banner = banner; + } + } + if (mediaTypes && mediaTypes[VIDEO]) { + const video = createVideoRequest(bid, mediaTypes[VIDEO]); + if (video) { + impObj.video = video; + } + } + + if (impObj.banner || impObj.video) { + imp.push(impObj); + } }); - const requests = []; - if (newFormatBids.length) { - requests.push(buildNewRequest(newFormatBids, bidderRequest)); + + const source = { + tid: auctionId, + ext: { + wrapper: 'Prebid_js', + wrapper_version: '$prebid.version$' + } + }; + + if (schain) { + source.ext.schain = schain; + } + + const bidderTimeout = config.getConfig('bidderTimeout') || timeout; + const tmax = timeout ? Math.min(bidderTimeout, timeout) : bidderTimeout; + + let request = { + id: bidderRequestId, + site: { + page: referer + }, + tmax, + source, + imp + }; + + if (content) { + request.site.content = content; + } + + if (jwpseg && jwpseg.length) { + user = { + data: [{ + name: 'iow_labs_pub_data', + segment: jwpseg.map((seg) => { + return {name: 'jwpseg', value: seg}; + }) + }] + }; + } + + if (gdprConsent && gdprConsent.consentString) { + userExt = {consent: gdprConsent.consentString}; } - if (oldFormatBids.length) { - requests.push(buildOldRequest(oldFormatBids, bidderRequest)); + + if (userIdAsEids && userIdAsEids.length) { + userExt = userExt || {}; + userExt.eids = [...userIdAsEids]; } - return requests; + + if (userExt && Object.keys(userExt).length) { + user = user || {}; + user.ext = userExt; + } + + if (user) { + request.user = user; + } + + const configKeywords = utils.transformBidderParamKeywords({ + 'user': utils.deepAccess(config.getConfig('ortb2.user'), 'keywords') || null, + 'context': utils.deepAccess(config.getConfig('ortb2.site'), 'keywords') || null + }); + + if (configKeywords.length) { + pageKeywords = (pageKeywords || []).concat(configKeywords); + } + + if (pageKeywords && pageKeywords.length > 0) { + pageKeywords.forEach(deleteValues); + } + + if (pageKeywords) { + request.ext = { + keywords: pageKeywords + }; + } + + if (gdprConsent && gdprConsent.gdprApplies) { + request.regs = { + ext: { + gdpr: gdprConsent.gdprApplies ? 1 : 0 + } + } + } + + if (uspConsent) { + if (!request.regs) { + request.regs = {ext: {}}; + } + request.regs.ext.us_privacy = uspConsent; + } + + if (config.getConfig('coppa') === true) { + if (!request.regs) { + request.regs = {}; + } + request.regs.coppa = 1; + } + + return { + method: 'POST', + url: ENDPOINT_URL, + data: JSON.stringify(request), + newFormat: true, + bidsMap + }; }, /** * Unpack the response from the server into a list of bids. @@ -108,6 +284,33 @@ export const spec = { } }; +/** + * Gets bidfloor + * @param {Object} mediaTypes + * @param {Object} bid + * @returns {Number} floor + */ +function _getFloor (mediaTypes, bid) { + const curMediaType = mediaTypes.video ? 'video' : 'banner'; + let floor = bid.params.bidFloor || 0; + + if (typeof bid.getFloor === 'function') { + const floorInfo = bid.getFloor({ + currency: 'USD', + mediaType: curMediaType, + size: bid.sizes.map(([w, h]) => ({w, h})) + }); + + if (typeof floorInfo === 'object' && + floorInfo.currency === 'USD' && + !isNaN(parseFloat(floorInfo.floor))) { + floor = Math.max(floor, parseFloat(floorInfo.floor)); + } + } + + return floor; +} + function isPopulatedArray(arr) { return !!(utils.isArray(arr) && arr.length > 0); } @@ -135,28 +338,10 @@ function _addBidResponse(serverBid, bidRequest, bidResponses) { if (!serverBid.auid) errorMessage = LOG_ERROR_MESS.noAuid + JSON.stringify(serverBid); if (!serverBid.adm) errorMessage = LOG_ERROR_MESS.noAdm + JSON.stringify(serverBid); else { - let bid = null; - let slot = null; - const bidsMap = bidRequest.bidsMap; - if (bidRequest.newFormat) { - bid = bidsMap[serverBid.impid]; - } else { - const awaitingBids = bidsMap[serverBid.auid]; - if (awaitingBids) { - const sizeId = `${serverBid.w}x${serverBid.h}`; - if (awaitingBids[sizeId]) { - slot = awaitingBids[sizeId][0]; - bid = slot.bids.shift(); - } - } else { - errorMessage = LOG_ERROR_MESS.noPlacementCode + serverBid.auid; - } - } - + const bid = bidRequest.bidsMap[serverBid.impid]; if (bid) { const bidResponse = { requestId: bid.bidId, // bid.bidderRequestId, - bidderCode: spec.code, cpm: serverBid.price, width: serverBid.w, height: serverBid.h, @@ -164,6 +349,9 @@ function _addBidResponse(serverBid, bidRequest, bidResponses) { currency: 'USD', netRevenue: false, ttl: TIME_TO_LIVE, + meta: { + advertiserDomains: serverBid && serverBid.adomain ? serverBid.adomain : [] + }, dealId: serverBid.dealid }; @@ -184,21 +372,6 @@ function _addBidResponse(serverBid, bidRequest, bidResponses) { bidResponse.mediaType = BANNER; } bidResponses.push(bidResponse); - - if (slot && !slot.bids.length) { - slot.parents.forEach(({parent, key, uid}) => { - const index = parent[key].indexOf(slot); - if (index > -1) { - parent[key].splice(index, 1); - } - if (!parent[key].length) { - delete parent[key]; - if (!utils.getKeys(parent).length) { - delete bidsMap[uid]; - } - } - }); - } } } if (errorMessage) { @@ -206,296 +379,8 @@ function _addBidResponse(serverBid, bidRequest, bidResponses) { } } -function buildOldRequest(validBidRequests, bidderRequest) { - const auids = []; - const bidsMap = {}; - const slotsMapByUid = {}; - const sizeMap = {}; - const bids = validBidRequests || []; - let pageKeywords = null; - let reqId; - - bids.forEach(bid => { - reqId = bid.bidderRequestId; - const {params: {uid}, adUnitCode, mediaTypes} = bid; - auids.push(uid); - const sizesId = utils.parseSizesInput(bid.sizes); - - if (!pageKeywords && !utils.isEmpty(bid.params.keywords)) { - pageKeywords = utils.transformBidderParamKeywords(bid.params.keywords); - } - - const addedSizes = {}; - sizesId.forEach((sizeId) => { - addedSizes[sizeId] = true; - }); - const bannerSizesId = utils.parseSizesInput(utils.deepAccess(mediaTypes, 'banner.sizes')); - const videoSizesId = utils.parseSizesInput(utils.deepAccess(mediaTypes, 'video.playerSize')); - bannerSizesId.concat(videoSizesId).forEach((sizeId) => { - if (!addedSizes[sizeId]) { - addedSizes[sizeId] = true; - sizesId.push(sizeId); - } - }); - - if (!slotsMapByUid[uid]) { - slotsMapByUid[uid] = {}; - } - const slotsMap = slotsMapByUid[uid]; - if (!slotsMap[adUnitCode]) { - slotsMap[adUnitCode] = {adUnitCode, bids: [bid], parents: []}; - } else { - slotsMap[adUnitCode].bids.push(bid); - } - const slot = slotsMap[adUnitCode]; - - sizesId.forEach((sizeId) => { - sizeMap[sizeId] = true; - if (!bidsMap[uid]) { - bidsMap[uid] = {}; - } - - if (!bidsMap[uid][sizeId]) { - bidsMap[uid][sizeId] = [slot]; - } else { - bidsMap[uid][sizeId].push(slot); - } - slot.parents.push({parent: bidsMap[uid], key: sizeId, uid}); - }); - }); - - const configKeywords = utils.transformBidderParamKeywords({ - 'user': utils.deepAccess(config.getConfig('fpd.user'), 'keywords') || null, - 'context': utils.deepAccess(config.getConfig('fpd.context'), 'keywords') || null - }); - - if (configKeywords.length) { - pageKeywords = (pageKeywords || []).concat(configKeywords); - } - - if (pageKeywords && pageKeywords.length > 0) { - pageKeywords.forEach(deleteValues); - } - - const payload = { - auids: auids.join(','), - sizes: utils.getKeys(sizeMap).join(','), - r: reqId, - wrapperType: 'Prebid_js', - wrapperVersion: '$prebid.version$' - }; - - if (pageKeywords) { - payload.keywords = JSON.stringify(pageKeywords); - } - - if (bidderRequest) { - if (bidderRequest.refererInfo && bidderRequest.refererInfo.referer) { - payload.u = bidderRequest.refererInfo.referer; - } - if (bidderRequest.timeout) { - payload.wtimeout = bidderRequest.timeout; - } - if (bidderRequest.gdprConsent) { - if (bidderRequest.gdprConsent.consentString) { - payload.gdpr_consent = bidderRequest.gdprConsent.consentString; - } - payload.gdpr_applies = - (typeof bidderRequest.gdprConsent.gdprApplies === 'boolean') - ? Number(bidderRequest.gdprConsent.gdprApplies) : 1; - } - if (bidderRequest.uspConsent) { - payload.us_privacy = bidderRequest.uspConsent; - } - } - - return { - method: 'GET', - url: ENDPOINT_URL, - data: utils.parseQueryStringParameters(payload).replace(/\&$/, ''), - bidsMap: bidsMap - } -} - -function buildNewRequest(validBidRequests, bidderRequest) { - let pageKeywords = null; - let jwpseg = null; - let content = null; - let schain = null; - let userId = null; - let user = null; - let userExt = null; - let {bidderRequestId, auctionId, gdprConsent, uspConsent, timeout, refererInfo} = bidderRequest; - - const referer = refererInfo ? encodeURIComponent(refererInfo.referer) : ''; - const imp = []; - const bidsMap = {}; - - validBidRequests.forEach((bid) => { - if (!bidderRequestId) { - bidderRequestId = bid.bidderRequestId; - } - if (!auctionId) { - auctionId = bid.auctionId; - } - if (!schain) { - schain = bid.schain; - } - if (!userId) { - userId = bid.userId; - } - const {params: {uid, keywords}, mediaTypes, bidId, adUnitCode, realTimeData} = bid; - bidsMap[bidId] = bid; - if (!pageKeywords && !utils.isEmpty(keywords)) { - pageKeywords = utils.transformBidderParamKeywords(keywords); - } - if (realTimeData && realTimeData.jwTargeting) { - if (!jwpseg && realTimeData.jwTargeting.segments) { - jwpseg = realTimeData.segments; - } - if (!content && realTimeData.content) { - content = realTimeData.content; - } - } - let impObj = { - id: bidId, - tagid: uid.toString(), - ext: { - divid: adUnitCode - } - }; - - if (!mediaTypes || mediaTypes[BANNER]) { - const banner = createBannerRequest(bid, mediaTypes ? mediaTypes[BANNER] : {}); - if (banner) { - impObj.banner = banner; - } - } - if (mediaTypes && mediaTypes[VIDEO]) { - const video = createVideoRequest(bid, mediaTypes[VIDEO]); - if (video) { - impObj.video = video; - } - } - - if (impObj.banner || impObj.video) { - imp.push(impObj); - } - }); - - const source = { - tid: auctionId, - ext: { - wrapper: 'Prebid_js', - wrapper_version: '$prebid.version$' - } - }; - - if (schain) { - source.ext.schain = schain; - } - - const tmax = config.getConfig('bidderTimeout') || timeout; - - let request = { - id: bidderRequestId, - site: { - page: referer - }, - tmax, - source, - imp - }; - - if (content) { - request.site.content = content; - } - - if (jwpseg && jwpseg.length) { - user = { - data: [{ - name: 'iow_labs_pub_data', - segment: jwpseg.map((seg) => { - return {name: 'jwpseg', value: seg}; - }) - }] - }; - } - - if (gdprConsent && gdprConsent.consentString) { - userExt = {consent: gdprConsent.consentString}; - } - - if (userId) { - userExt = userExt || {}; - if (userId.tdid) { - userExt.unifiedid = userId.tdid; - } - if (userId.id5id) { - userExt.id5id = userId.id5id; - } - if (userId.digitrustid && userId.digitrustid.data && userId.digitrustid.data.id) { - userExt.digitrustid = userId.digitrustid.data.id; - } - if (userId.lipb && userId.lipb.lipbid) { - userExt.liveintentid = userId.lipb.lipbid; - } - } - - if (userExt && Object.keys(userExt).length) { - user = user || {}; - user.ext = userExt; - } - - if (user) { - request.user = user; - } - - const configKeywords = utils.transformBidderParamKeywords({ - 'user': utils.deepAccess(config.getConfig('fpd.user'), 'keywords') || null, - 'context': utils.deepAccess(config.getConfig('fpd.context'), 'keywords') || null - }); - - if (configKeywords.length) { - pageKeywords = (pageKeywords || []).concat(configKeywords); - } - - if (pageKeywords && pageKeywords.length > 0) { - pageKeywords.forEach(deleteValues); - } - - if (pageKeywords) { - request.ext = { - keywords: pageKeywords - }; - } - - if (gdprConsent && gdprConsent.gdprApplies) { - request.regs = { - ext: { - gdpr: gdprConsent.gdprApplies ? 1 : 0 - } - } - } - - if (uspConsent) { - if (!request.regs) { - request.regs = {ext: {}}; - } - request.regs.ext.us_privacy = uspConsent; - } - - return { - method: 'POST', - url: NEW_ENDPOINT_URL, - data: JSON.stringify(request), - newFormat: true, - bidsMap - }; -} - function createVideoRequest(bid, mediaType) { - const {playerSize, mimes, durationRangeSec} = mediaType; + const {playerSize, mimes, durationRangeSec, protocols} = mediaType; const size = (playerSize || bid.sizes || [])[0]; if (!size) return; @@ -510,6 +395,10 @@ function createVideoRequest(bid, mediaType) { result.maxduration = durationRangeSec[1]; } + if (protocols && protocols.length) { + result.protocols = protocols; + } + return result; } diff --git a/modules/gridBidAdapter.md b/modules/gridBidAdapter.md index 77b9bbf0f36..6a7075ccb00 100644 --- a/modules/gridBidAdapter.md +++ b/modules/gridBidAdapter.md @@ -20,7 +20,7 @@ Grid bid adapter supports Banner and Video (instream and outstream). bidder: "grid", params: { uid: '1', - priceType: 'gross' // by default is 'net' + bidFloor: 0.5 } } ] @@ -32,7 +32,6 @@ Grid bid adapter supports Banner and Video (instream and outstream). bidder: "grid", params: { uid: 2, - priceType: 'gross', keywords: { brandsafety: ['disaster'], topic: ['stress', 'fear'] diff --git a/modules/gridNMBidAdapter.js b/modules/gridNMBidAdapter.js index ffd6c1b250c..af1e9f84f43 100644 --- a/modules/gridNMBidAdapter.js +++ b/modules/gridNMBidAdapter.js @@ -134,7 +134,6 @@ export const spec = { } const bidResponse = { requestId: bid.bidId, - bidderCode: spec.code, cpm: serverBid.price, width: serverBid.w, height: serverBid.h, diff --git a/modules/gumgumBidAdapter.js b/modules/gumgumBidAdapter.js index ebeb46e3c44..4786fd04b15 100644 --- a/modules/gumgumBidAdapter.js +++ b/modules/gumgumBidAdapter.js @@ -135,7 +135,8 @@ function isBidRequestValid (bid) { params, adUnitCode } = bid; - const id = params.inScreen || params.inScreenPubID || params.inSlot || params.ICV || params.video || params.inVideo; + const legacyParamID = params.inScreen || params.inScreenPubID || params.inSlot || params.ICV || params.video || params.inVideo; + const id = legacyParamID || params.slot || params.native || params.zone || params.pubID; if (invalidRequestIds[id]) { utils.logWarn(`[GumGum] Please check the implementation for ${id} for the placement ${adUnitCode}`); @@ -143,6 +144,8 @@ function isBidRequestValid (bid) { } switch (true) { + case !!(params.zone): break; + case !!(params.pubId): break; case !!(params.inScreen): break; case !!(params.inScreenPubID): break; case !!(params.inSlot): break; @@ -205,25 +208,24 @@ function _getVidParams (attributes) { * @param {Object} bid * @returns {Number} floor */ -function _getFloor (mediaTypes, bidfloor, bid) { +function _getFloor (mediaTypes, staticBidfloor, bid) { const curMediaType = Object.keys(mediaTypes)[0] || 'banner'; - let floor = bidfloor || 0; + const bidFloor = { floor: 0, currency: 'USD' }; if (typeof bid.getFloor === 'function') { - const floorInfo = bid.getFloor({ - currency: 'USD', + const { currency, floor } = bid.getFloor({ mediaType: curMediaType, size: '*' }); + floor && (bidFloor.floor = floor); + currency && (bidFloor.currency = currency); - if (typeof floorInfo === 'object' && - floorInfo.currency === 'USD' && - !isNaN(parseFloat(floorInfo.floor))) { - floor = Math.max(floor, parseFloat(floorInfo.floor)); + if (staticBidfloor && floor && currency === 'USD') { + bidFloor.floor = Math.max(staticBidfloor, parseFloat(floor)); } } - return floor; + return bidFloor; } /** @@ -247,7 +249,7 @@ function buildRequests (validBidRequests, bidderRequest) { transactionId, userId = {} } = bidRequest; - const bidFloor = _getFloor(mediaTypes, params.bidfloor, bidRequest); + const { currency, floor } = _getFloor(mediaTypes, params.bidfloor, bidRequest); let sizes = [1, 1]; let data = {}; @@ -255,47 +257,45 @@ function buildRequests (validBidRequests, bidderRequest) { sizes = mediaTypes.banner.sizes; } else if (mediaTypes.video) { sizes = mediaTypes.video.playerSize; + data = _getVidParams(mediaTypes.video); } if (pageViewId) { data.pv = pageViewId; } - if (bidFloor) { - data.fp = bidFloor; + if (floor) { + data.fp = floor; + data.fpc = currency; } - if (params.inScreenPubID) { - data.pubId = params.inScreenPubID; - data.pi = 2; + if (params.iriscat && typeof params.iriscat === 'string') { + data.iriscat = params.iriscat; } - if (params.inScreen) { - data.t = params.inScreen; - data.pi = 2; - } - if (params.inSlot) { - data.si = parseInt(params.inSlot, 10); - data.pi = 3; - } - if (params.ICV) { - data.ni = parseInt(params.ICV, 10); - data.pi = 5; - } - if (params.videoPubID) { - data = Object.assign(data, _getVidParams(mediaTypes.video)); - data.pubId = params.videoPubID; - data.pi = 7; - } - if (params.video) { - data = Object.assign(data, _getVidParams(mediaTypes.video)); - data.t = params.video; - data.pi = 7; + + if (params.irisid && typeof params.irisid === 'string') { + data.irisid = params.irisid; } - if (params.inVideo) { - data = Object.assign(data, _getVidParams(mediaTypes.video)); - data.t = params.inVideo; - data.pi = 6; + + 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) { + data.si = parseInt(params.slot, 10); + data.pi = 3; + data.bf = sizes.reduce((acc, curSlotDim) => `${acc}${acc && ','}${curSlotDim[0]}x${curSlotDim[1]}`, ''); + } else if (params.native) { + data.ni = parseInt(params.native, 10); + data.pi = 5; + } else if (mediaTypes.video) { + data.pi = mediaTypes.video.linearity === 2 ? 6 : 7; // invideo : video + } + } else { // legacy params + data = { ...data, ...handleLegacyParams(params, sizes) } } + if (gdprConsent) { data.gdprApplies = gdprConsent.gdprApplies ? 1 : 0; } @@ -324,6 +324,40 @@ function buildRequests (validBidRequests, bidderRequest) { return bids; } +function handleLegacyParams (params, sizes) { + const data = {}; + if (params.inScreenPubID) { + data.pubId = params.inScreenPubID; + data.pi = 2; + } + if (params.inScreen) { + data.t = params.inScreen; + data.pi = 2; + } + if (params.inSlot) { + data.si = parseInt(params.inSlot, 10); + data.pi = 3; + data.bf = sizes.reduce((acc, curSlotDim) => `${acc}${acc && ','}${curSlotDim[0]}x${curSlotDim[1]}`, ''); + } + if (params.ICV) { + data.ni = parseInt(params.ICV, 10); + data.pi = 5; + } + if (params.videoPubID) { + data.pubId = params.videoPubID; + data.pi = 7; + } + if (params.video) { + data.t = params.video; + data.pi = 7; + } + if (params.inVideo) { + data.t = params.inVideo; + data.pi = 6; + } + return data; +} + /** * Unpack the response from the server into a list of bids. * @@ -336,7 +370,7 @@ function interpretResponse (serverResponse, bidRequest) { if (!serverResponseBody || serverResponseBody.err) { const data = bidRequest.data || {} - const id = data.t || data.si || data.ni || data.pubId; + const id = data.si || data.ni || data.t || data.pubId; const delayTime = serverResponseBody ? serverResponseBody.err.drt : DELAY_REQUEST_TIME; invalidRequestIds[id] = { productId: data.pi, timestamp: new Date().getTime() }; @@ -350,10 +384,16 @@ function interpretResponse (serverResponse, bidRequest) { ad: { price: 0, id: 0, - markup: '' + markup: '', + width: 0, + height: 0 }, pag: { pvid: 0 + }, + meta: { + adomain: [], + mediaType: '' } } const { @@ -361,19 +401,31 @@ function interpretResponse (serverResponse, bidRequest) { price: cpm, id: creativeId, markup, - cur + cur, + width: responseWidth, + height: responseHeight }, cw: wrapper, pag: { pvid }, - jcsi + jcsi, + meta: { + adomain: advertiserDomains, + mediaType: type + } } = 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) + // use response sizes if available + let sizes = responseWidth && responseHeight ? [`${responseWidth}x${responseHeight}`] : utils.parseSizesInput(bidRequest.sizes) let [width, height] = sizes[0].split('x') + let metaData = { + advertiserDomains: advertiserDomains || [], + mediaType: type || mediaType + } // return 1x1 when breakout expected if ((product === 2 || product === 5) && includes(sizes, '1x1')) { @@ -392,9 +444,9 @@ function interpretResponse (serverResponse, bidRequest) { bidResponses.push({ // dealId: DEAL_ID, // referrer: REFERER, - ...(product === 7 && { vastXml: markup }), 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', @@ -402,7 +454,8 @@ function interpretResponse (serverResponse, bidRequest) { netRevenue: true, requestId: bidRequest.id, ttl: TIME_TO_LIVE, - width + width, + meta: metaData }) } return bidResponses 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/h12mediaBidAdapter.js b/modules/h12mediaBidAdapter.js index 0d2c22a3f68..7b736780226 100644 --- a/modules/h12mediaBidAdapter.js +++ b/modules/h12mediaBidAdapter.js @@ -1,6 +1,5 @@ import * as utils from '../src/utils.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; -import find from 'core-js-pure/features/array/find.js'; const BIDDER_CODE = 'h12media'; const DEFAULT_URL = 'https://bidder.h12-media.com/prebid/'; const DEFAULT_CURRENCY = 'USD'; @@ -16,21 +15,31 @@ export const spec = { }, buildRequests: function(validBidRequests, bidderRequest) { - const requestUrl = validBidRequests[0].params.endpointdom || DEFAULT_URL; - const isiframe = !((window.self === window.top) || window.frameElement); + const isiframe = utils.inIframe(); const screenSize = getClientDimensions(); const docSize = getDocumentDimensions(); - const bidrequests = validBidRequests.map((bidRequest) => { + return validBidRequests.map((bidRequest) => { const bidderParams = bidRequest.params; - const adUnitElement = document.getElementById(bidRequest.adUnitCode); + const requestUrl = bidderParams.endpointdom || DEFAULT_URL; + let pubsubid = bidderParams.pubsubid || ''; + if (pubsubid && pubsubid.length > 32) { + utils.logError('Bidder param \'pubsubid\' should be not more than 32 chars.'); + pubsubid = ''; + } + const pubcontainerid = bidderParams.pubcontainerid; + const adUnitElement = document.getElementById(pubcontainerid || bidRequest.adUnitCode); const ishidden = !isVisible(adUnitElement); - const coords = { + const framePos = getFramePos(); + const coords = isiframe ? { + x: framePos[0], + y: framePos[1], + } : { x: adUnitElement && adUnitElement.getBoundingClientRect().x, y: adUnitElement && adUnitElement.getBoundingClientRect().y, }; - return { + const bidrequest = { bidId: bidRequest.bidId, transactionId: bidRequest.transactionId, adunitId: bidRequest.adUnitCode, @@ -40,33 +49,46 @@ export const spec = { adunitSize: bidRequest.mediaTypes.banner.sizes || [], coords, ishidden, + pubsubid, + pubcontainerid, }; - }); - return { - method: 'POST', - url: requestUrl, - options: {withCredentials: false}, - data: { - gdpr: utils.deepAccess(bidderRequest, 'gdprConsent.gdprApplies') ? Boolean(bidderRequest.gdprConsent.gdprApplies & 1) : false, - gdpr_cs: utils.deepAccess(bidderRequest, 'gdprConsent.gdprApplies') ? bidderRequest.gdprConsent.consentString : '', - topLevelUrl: window.top.location.href, - refererUrl: bidderRequest.refererInfo ? bidderRequest.refererInfo.referer : '', - isiframe, - version: '$prebid.version$', - visitorInfo: { - localTime: getLocalDateFormatted(), - dayOfWeek: new Date().getDay(), - screenWidth: screenSize[0], - screenHeight: screenSize[1], - docWidth: docSize[0], - docHeight: docSize[1], - scrollbarx: window.scrollX, - scrollbary: window.scrollY, + let windowTop; + try { + windowTop = window.top; + } catch (e) { + utils.logMessage(e); + windowTop = window; + } + + return { + method: 'POST', + url: requestUrl, + options: {withCredentials: true}, + data: { + gdpr: !!utils.deepAccess(bidderRequest, 'gdprConsent.gdprApplies', false), + gdpr_cs: utils.deepAccess(bidderRequest, 'gdprConsent.consentString', ''), + usp: !!utils.deepAccess(bidderRequest, 'uspConsent', false), + usp_cs: utils.deepAccess(bidderRequest, 'uspConsent', ''), + topLevelUrl: utils.deepAccess(bidderRequest, 'refererInfo.referer', ''), + refererUrl: windowTop.document.referrer, + isiframe, + version: '$prebid.version$', + ExtUserIDs: bidRequest.userId, + visitorInfo: { + localTime: getLocalDateFormatted(), + dayOfWeek: new Date().getDay(), + screenWidth: screenSize[0], + screenHeight: screenSize[1], + docWidth: docSize[0], + docHeight: docSize[1], + scrollbarx: windowTop.scrollX, + scrollbary: windowTop.scrollY, + }, + bidrequest, }, - bidrequests, - }, - }; + }; + }); }, interpretResponse: function(serverResponse, bidRequests) { @@ -74,29 +96,28 @@ export const spec = { try { const serverBody = serverResponse.body; if (serverBody) { - if (serverBody.bids) { - serverBody.bids.forEach(bidBody => { - const bidRequest = find(bidRequests.data.bidrequests, bid => bid.bidId === bidBody.bidId); - const bidResponse = { - currency: serverBody.currency || DEFAULT_CURRENCY, - netRevenue: serverBody.netRevenue || DEFAULT_NET_REVENUE, - ttl: serverBody.ttl || DEFAULT_TTL, - requestId: bidBody.bidId, - cpm: bidBody.cpm, - width: bidBody.width, - height: bidBody.height, - creativeId: bidBody.creativeId, - ad: bidBody.ad, - meta: bidBody.meta, - mediaType: 'banner', - }; - if (bidRequest) { - bidResponse.pubid = bidRequest.pubid; - bidResponse.placementid = bidRequest.placementid; - bidResponse.size = bidRequest.size; - } - bidResponses.push(bidResponse); - }); + if (serverBody.bid) { + const bidBody = serverBody.bid; + const bidRequest = bidRequests.data.bidrequest; + const bidResponse = { + currency: serverBody.currency || DEFAULT_CURRENCY, + netRevenue: serverBody.netRevenue || DEFAULT_NET_REVENUE, + ttl: serverBody.ttl || DEFAULT_TTL, + requestId: bidBody.bidId, + cpm: bidBody.cpm, + width: bidBody.width, + height: bidBody.height, + creativeId: bidBody.creativeId, + ad: bidBody.ad, + meta: bidBody.meta, + mediaType: 'banner', + }; + if (bidRequest) { + bidResponse.pubid = bidRequest.pubid; + bidResponse.placementid = bidRequest.placementid; + bidResponse.size = bidRequest.size; + } + bidResponses.push(bidResponse); } } return bidResponses; @@ -105,47 +126,50 @@ export const spec = { } }, - getUserSyncs: function(syncOptions, serverResponses, gdprConsent) { - const serverBody = serverResponses[0].body; + getUserSyncs: function(syncOptions, serverResponses, gdprConsent, usPrivacy) { const syncs = []; + const uspApplies = !!utils.deepAccess(usPrivacy, 'uspConsent', false); + const uspString = utils.deepAccess(usPrivacy, 'uspConsent', ''); gdprConsent = gdprConsent || { gdprApplies: false, consentString: '', }; - if (serverBody) { - if (serverBody.bids) { - serverBody.bids.forEach(bidBody => { - const userSyncUrls = bidBody.usersync || []; - const userSyncUrlProcess = url => { - return url - .replace('{gdpr}', gdprConsent.gdprApplies) - .replace('{gdpr_cs}', gdprConsent.consentString); - } + const userSyncUrlProcess = url => { + return url + .replace('{gdpr}', gdprConsent.gdprApplies) + .replace('{gdpr_cs}', gdprConsent.consentString) + .replace('{usp}', uspApplies) + .replace('{usp_cs}', uspString); + } - userSyncUrls.forEach(sync => { - if (syncOptions.iframeEnabled && sync.type === 'iframe' && sync.url) { - syncs.push({ - type: 'iframe', - url: userSyncUrlProcess(sync.url), - }); - } - if (syncOptions.pixelEnabled && sync.type === 'image' && sync.url) { - syncs.push({ - type: 'image', - url: userSyncUrlProcess(sync.url), - }); - } + serverResponses.forEach(serverResponse => { + const userSyncUrls = serverResponse.body.usersync || []; + userSyncUrls.forEach(sync => { + if (syncOptions.iframeEnabled && sync.type === 'iframe' && sync.url) { + syncs.push({ + type: 'iframe', + url: userSyncUrlProcess(sync.url), }); - }); - } - } + } + if (syncOptions.pixelEnabled && sync.type === 'image' && sync.url) { + syncs.push({ + type: 'image', + url: userSyncUrlProcess(sync.url), + }); + } + }) + }); return syncs; }, } function getContext(elem) { - return elem && window.document.body.contains(elem) ? window : (window.top.document.body.contains(elem) ? top : undefined); + try { + return elem && window.document.body.contains(elem) ? window : (window.top.document.body.contains(elem) ? top : undefined); + } catch (e) { + return undefined; + } } function isDefined(val) { @@ -206,4 +230,24 @@ function getLocalDateFormatted() { return `${d.getFullYear()}-${two(d.getMonth() + 1)}-${two(d.getDate())} ${two(d.getHours())}:${two(d.getMinutes())}:${two(d.getSeconds())}`; } +function getFramePos() { + let t = window; + let m = 0; + let frmLeft = 0; + let frmTop = 0; + do { + m = m + 1; + try { + if (m > 1) { + t = t.parent + } + frmLeft = frmLeft + t.frameElement.getBoundingClientRect().left; + frmTop = frmTop + t.frameElement.getBoundingClientRect().top; + } catch (o) { /* keep looping */ + } + } while ((m < 100) && (t.parent !== t.self)) + + return [frmLeft, frmTop]; +} + registerBidder(spec); diff --git a/modules/haloIdSystem.js b/modules/haloIdSystem.js new file mode 100644 index 00000000000..4a0330367f5 --- /dev/null +++ b/modules/haloIdSystem.js @@ -0,0 +1,77 @@ +/** + * This module adds HaloID to the User ID module + * The {@link module:modules/userId} module is required + * @module modules/haloIdSystem + * @requires module:modules/userId + */ + +import {ajax} from '../src/ajax.js'; +import {getStorageManager} from '../src/storageManager.js'; +import {submodule} from '../src/hook.js'; +import * as utils from '../src/utils.js'; + +const MODULE_NAME = 'haloId'; +const AU_GVLID = 561; + +export const storage = getStorageManager(AU_GVLID, 'halo'); + +/** @type {Submodule} */ +export const haloIdSubmodule = { + /** + * used to link submodule with config + * @type {string} + */ + name: MODULE_NAME, + /** + * decode the stored id value for passing to bid requests + * @function + * @param {{value:string}} value + * @returns {{haloId:Object}} + */ + decode(value) { + let haloId = storage.getDataFromLocalStorage('auHaloId'); + if (utils.isStr(haloId)) { + return {haloId: haloId}; + } + return (value && typeof value['haloId'] === 'string') ? { 'haloId': value['haloId'] } : undefined; + }, + /** + * performs action to obtain id and return a value in the callback's response argument + * @function + * @param {SubmoduleConfig} [config] + * @returns {IdResponse|undefined} + */ + getId(config) { + const url = `https://id.halo.ad.gt/api/v1/pbhid`; + + const resp = function (callback) { + let haloId = storage.getDataFromLocalStorage('auHaloId'); + if (utils.isStr(haloId)) { + const responseObj = {haloId: haloId}; + callback(responseObj); + } else { + const callbacks = { + success: response => { + let responseObj; + if (response) { + try { + responseObj = JSON.parse(response); + } catch (error) { + utils.logError(error); + } + } + callback(responseObj); + }, + error: error => { + utils.logError(`${MODULE_NAME}: ID fetch encountered an error`, error); + callback(); + } + }; + ajax(url, callbacks, undefined, {method: 'GET'}); + } + }; + return {callback: resp}; + } +}; + +submodule('userId', haloIdSubmodule); diff --git a/modules/haloIdSystem.md b/modules/haloIdSystem.md new file mode 100644 index 00000000000..0be0be27f5d --- /dev/null +++ b/modules/haloIdSystem.md @@ -0,0 +1,32 @@ +## Audigent Halo User ID Submodule + +Audigent Halo ID Module. For assistance setting up your module please contact us at [prebid@audigent.com](prebid@audigent.com). + +### Prebid Params + +Individual params may be set for the Audigent Halo ID Submodule. At least one identifier must be set in the params. + +``` +pbjs.setConfig({ + usersync: { + userIds: [{ + name: 'haloId', + storage: { + name: 'haloId', + type: 'html5' + } + }] + } +}); +``` +## Parameter Descriptions for the `usersync` Configuration Section +The below parameters apply only to the HaloID User ID Module integration. + +| Param under usersync.userIds[] | Scope | Type | Description | Example | +| --- | --- | --- | --- | --- | +| name | Required | String | ID value for the HaloID module - `"haloId"` | `"haloId"` | +| storage | Required | Object | The publisher must specify the local storage in which to store the results of the call to get the user ID. This can be either cookie or HTML5 storage. | | +| storage.type | Required | String | This is where the results of the user ID will be stored. The recommended method is `localStorage` by specifying `html5`. | `"html5"` | +| storage.name | Required | String | The name of the cookie or html5 local storage where the user ID will be stored. | `"haloid"` | +| storage.expires | Optional | Integer | How long (in days) the user ID information will be stored. | `365` | +| value | Optional | Object | Used only if the page has a separate mechanism for storing the Halo ID. The value is an object containing the values to be sent to the adapters. In this scenario, no URL is called and nothing is added to local storage | `{"haloId": "eb33b0cb-8d35-4722-b9c0-1a31d4064888"}` | diff --git a/modules/haloRtdProvider.js b/modules/haloRtdProvider.js new file mode 100644 index 00000000000..b57787aab14 --- /dev/null +++ b/modules/haloRtdProvider.js @@ -0,0 +1,192 @@ +/** + * This module adds the Audigent Halo provider to the real time data module + * The {@link module:modules/realTimeData} module is required + * The module will fetch real-time data from Audigent + * @module modules/haloRtdProvider + * @requires module:modules/realTimeData + */ +import {ajax} from '../src/ajax.js'; +import {config} from '../src/config.js'; +import {getGlobal} from '../src/prebidGlobal.js'; +import {getStorageManager} from '../src/storageManager.js'; +import {submodule} from '../src/hook.js'; +import {isFn, isStr, isPlainObject, mergeDeep, logError} from '../src/utils.js'; + +const MODULE_NAME = 'realTimeData'; +const SUBMODULE_NAME = 'halo'; +const AU_GVLID = 561; + +export const HALOID_LOCAL_NAME = 'auHaloId'; +export const RTD_LOCAL_NAME = 'auHaloRtd'; +export const storage = getStorageManager(AU_GVLID, SUBMODULE_NAME); + +/** + * Deep set an object unless value present. + * @param {Object} obj + * @param {String} path + * @param {Object} val + */ +const set = (obj, path, val) => { + const keys = path.split('.'); + const lastKey = keys.pop(); + const lastObj = keys.reduce((obj, key) => obj[key] = obj[key] || {}, obj); + lastObj[lastKey] = lastObj[lastKey] || val; +}; + +/** + * Lazy merge objects. + * @param {String} target + * @param {String} source + */ +function mergeLazy(target, source) { + if (!isPlainObject(target)) { + target = {}; + } + if (!isPlainObject(source)) { + source = {}; + } + return mergeDeep(target, source); +} + +/** + * Param or default. + * @param {String} param + * @param {String} defaultVal + */ +function paramOrDefault(param, defaultVal, arg) { + if (isFn(param)) { + return param(arg); + } else if (isStr(param)) { + return param; + } + return defaultVal; +} + +/** + * Add real-time data & merge segments. + * @param {Object} bidConfig + * @param {Object} rtd + * @param {Object} rtdConfig + */ +export function addRealTimeData(bidConfig, rtd, rtdConfig) { + let ortb2 = config.getConfig('ortb2') || {}; + + if (rtdConfig.params && rtdConfig.params.handleRtd) { + rtdConfig.params.handleRtd(bidConfig, rtd, rtdConfig, config); + } else if (rtd.ortb2) { + config.setConfig({ortb2: mergeLazy(ortb2, rtd.ortb2)}); + } +} + +/** + * Real-time data retrieval from Audigent + * @param {Object} reqBidsConfigObj + * @param {function} onDone + * @param {Object} rtdConfig + * @param {Object} userConsent + */ +export function getRealTimeData(bidConfig, onDone, rtdConfig, userConsent) { + if (rtdConfig && isPlainObject(rtdConfig.params) && rtdConfig.params.segmentCache) { + let jsonData = storage.getDataFromLocalStorage(RTD_LOCAL_NAME); + + if (jsonData) { + let data = JSON.parse(jsonData); + + if (data.rtd) { + addRealTimeData(bidConfig, data.rtd, rtdConfig); + onDone(); + return; + } + } + } + + const userIds = (getGlobal()).getUserIds(); + + let haloId = storage.getDataFromLocalStorage(HALOID_LOCAL_NAME); + if (isStr(haloId)) { + userIds.haloId = haloId; + getRealTimeDataAsync(bidConfig, onDone, rtdConfig, userConsent, userIds); + } else { + var script = document.createElement('script'); + script.type = 'text/javascript'; + + window.pubHaloCb = (haloId) => { + userIds.haloId = haloId; + getRealTimeDataAsync(bidConfig, onDone, rtdConfig, userConsent, userIds); + } + + const haloIdUrl = rtdConfig.params && rtdConfig.params.haloIdUrl; + script.src = paramOrDefault(haloIdUrl, 'https://id.halo.ad.gt/api/v1/haloid', userIds); + document.getElementsByTagName('head')[0].appendChild(script); + } +} + +/** + * Async rtd retrieval from Audigent + * @param {function} onDone + * @param {Object} rtdConfig + * @param {Object} userConsent + * @param {Object} userIds + */ +export function getRealTimeDataAsync(bidConfig, onDone, rtdConfig, userConsent, userIds) { + let reqParams = {}; + + if (isPlainObject(rtdConfig)) { + set(rtdConfig, 'params.requestParams.ortb2', config.getConfig('ortb2')); + reqParams = rtdConfig.params.requestParams; + } + + if (isPlainObject(window.pubHaloPm)) { + reqParams.pubHaloPm = window.pubHaloPm; + } + + const url = `https://seg.halo.ad.gt/api/v1/rtd`; + ajax(url, { + success: function (response, req) { + if (req.status === 200) { + try { + const data = JSON.parse(response); + if (data && data.rtd) { + addRealTimeData(bidConfig, data.rtd, rtdConfig); + onDone(); + storage.setDataInLocalStorage(RTD_LOCAL_NAME, JSON.stringify(data)); + } else { + onDone(); + } + } catch (err) { + logError('unable to parse audigent segment data'); + onDone(); + } + } else if (req.status === 204) { + // unrecognized partner config + onDone(); + } + }, + error: function () { + onDone(); + logError('unable to get audigent segment data'); + } + }, + JSON.stringify({'userIds': userIds, 'config': reqParams}), + {contentType: 'application/json'} + ); +} + +/** + * Module init + * @param {Object} provider + * @param {Objkect} userConsent + * @return {boolean} + */ +function init(provider, userConsent) { + return true; +} + +/** @type {RtdSubmodule} */ +export const haloSubmodule = { + name: SUBMODULE_NAME, + getBidRequestData: getRealTimeData, + init: init +}; + +submodule(MODULE_NAME, haloSubmodule); diff --git a/modules/haloRtdProvider.md b/modules/haloRtdProvider.md new file mode 100644 index 00000000000..4307618bb60 --- /dev/null +++ b/modules/haloRtdProvider.md @@ -0,0 +1,123 @@ +## Audigent Halo Real-time Data Submodule + +Audigent is a next-generation data management platform and a first-of-a-kind +"data agency" containing some of the most exclusive content-consuming audiences +across desktop, mobile and social platforms. + +This real-time data module provides quality segmentation that can be +provided to bid request objects destined for different SSPs in order to optimize +targeting. Audigent maintains a large database of first-party Tradedesk Unified +ID, Audigent Halo ID and other id provider mappings to various third-party +segment types that are utilizable across different SSPs. With this module, +these segments and other data can be retrieved and supplied to your pages +and the bidstream in real-time during the bid request cycle. + +### Publisher Usage + +Compile the Halo RTD module into your Prebid build: + +`gulp build --modules=userId,unifiedIdSystem,rtdModule,haloRtdProvider,appnexusBidAdapter` + +Add the Halo RTD provider to your Prebid config. In this example we will configure +publisher 1234 to retrieve segments from Audigent. See the +"Parameter Descriptions" below for more detailed information of the +configuration parameters. Please work with your Audigent Prebid support team +(prebid@audigent.com) on which version of Prebid.js supports different bidder +and segment configurations. + +``` +pbjs.setConfig( + ... + realTimeData: { + auctionDelay: auctionDelay, + dataProviders: [ + { + name: "halo", + waitForIt: true, + params: { + segmentCache: false, + requestParams: { + publisherId: 1234 + } + } + } + ] + } + ... +} +``` + +### Parameter Descriptions for the Halo Configuration Section + +| Name |Type | Description | Notes | +| :------------ | :------------ | :------------ |:------------ | +| name | String | Real time data module name | Always 'halo' | +| waitForIt | Boolean | Required to ensure that the auction is delayed until prefetch is complete | Optional. Defaults to false | +| params | Object | | | +| params.handleRtd | Function | A passable RTD handler that allows custom adunit and ortb2 logic to be configured. The function signature is (bidConfig, rtd, rtdConfig, pbConfig) => {}. | Optional | +| params.segmentCache | Boolean | This parameter tells the Halo RTD module to attempt reading segments from a local storage cache instead of always requesting them from the Audigent server. | Optional. Defaults to false. | +| params.requestParams | Object | Publisher partner specific configuration options, such as optional publisher id and other segment query related metadata to be submitted to Audigent's backend with each request. Contact prebid@audigent.com for more information. | Optional | +| params.haloIdUrl | String | Parameter to specify alternate haloid endpoint url. | Optional | + +### Publisher Customized RTD Handling +As indicated above, it is possible to provide your own bid augmentation +functions rather than simply merging supplied data. This is useful if you +want to perform custom bid augmentation and logic with Halo real-time data +prior to the bid request being sent. Simply add your custom logic to the +optional handleRtd parameter and provide your custom RTD handling logic there. + +Please see the following example, which provides a function to modify bids for +a bid adapter called adBuzz and perform custom logic on bidder parameters. + +``` +pbjs.setConfig( + ... + realTimeData: { + auctionDelay: auctionDelay, + dataProviders: [ + { + name: "halo", + waitForIt: true, + params: { + handleRtd: function(bidConfig, rtd, rtdConfig, pbConfig) { + var adUnits = bidConfig.adUnits; + for (var i = 0; i < adUnits.length; i++) { + var adUnit = adUnits[i]; + for (var j = 0; j < adUnit.bids.length; j++) { + var bid = adUnit.bids[j]; + if (bid.bidder == 'adBuzz' && rtd['adBuzz'][0].value != 'excludeSeg') { + bid.params.adBuzzCustomSegments.push(rtd['adBuzz'][0].id); + } + } + } + }, + segmentCache: false, + requestParams: { + publisherId: 1234 + } + } + } + ] + } + ... +} +``` + +The handleRtd function can also be used to configure custom ortb2 data +processing. Please see the examples available in the haloRtdProvider_spec.js +tests and work with your Audigent Prebid integration team (prebid@audigent.com) +on how to best configure your own Halo RTD & Open RTB data handlers. + +### Testing + +To view an example of available segments returned by Audigent's backends: + +`gulp serve --modules=userId,unifiedIdSystem,rtdModule,haloRtdProvider,appnexusBidAdapter` + +and then point your browser at: + +`http://localhost:9999/integrationExamples/gpt/haloRtdProvider_example.html` + + + + diff --git a/modules/haxmediaBidAdapter.js b/modules/haxmediaBidAdapter.js new file mode 100644 index 00000000000..c4ce2eb3663 --- /dev/null +++ b/modules/haxmediaBidAdapter.js @@ -0,0 +1,107 @@ +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 = 'haxmedia'; +const AD_URL = 'https://balancer.haxmedia.io/?c=o&m=multi'; + +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) => { + let winTop = window; + let location; + try { + location = new URL(bidderRequest.refererInfo.referer) + winTop = window.top; + } catch (e) { + location = winTop.location; + utils.logMessage(e); + }; + + 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; + }, +}; + +registerBidder(spec); diff --git a/modules/haxmediaBidAdapter.md b/modules/haxmediaBidAdapter.md new file mode 100644 index 00000000000..f661a9e4e71 --- /dev/null +++ b/modules/haxmediaBidAdapter.md @@ -0,0 +1,72 @@ +# Overview + +``` +Module Name: haxmedia Bidder Adapter +Module Type: haxmedia Bidder Adapter +Maintainer: haxmixqk@haxmediapartners.io +``` + +# Description + +Module that connects to haxmedia demand sources + +# Test Parameters +``` + var adUnits = [ + { + code:'1', + mediaTypes:{ + banner: { + sizes: [[300, 250]], + } + }, + bids:[ + { + bidder: 'haxmedia', + params: { + placementId: 0 + } + } + ] + }, + { + code:'1', + mediaTypes:{ + video: { + playerSize: [640, 480], + context: 'instream' + } + }, + bids:[ + { + bidder: 'haxmedia', + params: { + placementId: 0 + } + } + ] + }, + { + code:'1', + mediaTypes:{ + native: { + title: { + required: true + }, + icon: { + required: true, + size: [64, 64] + } + } + }, + bids:[ + { + bidder: 'haxmedia', + params: { + placementId: 0 + } + } + ] + } + ]; +``` diff --git a/modules/hybridBidAdapter.js b/modules/hybridBidAdapter.js index 5671756e0b2..e7281086a92 100644 --- a/modules/hybridBidAdapter.js +++ b/modules/hybridBidAdapter.js @@ -10,12 +10,14 @@ const DSP_ENDPOINT = 'https://hbe198.hybrid.ai/prebidhb'; const TRAFFIC_TYPE_WEB = 1; const PLACEMENT_TYPE_BANNER = 1; const PLACEMENT_TYPE_VIDEO = 2; +const PLACEMENT_TYPE_IN_IMAGE = 3; const TTL = 60; const RENDERER_URL = 'https://acdn.adnxs.com/video/outstream/ANOutstreamVideo.js'; const placementTypes = { 'banner': PLACEMENT_TYPE_BANNER, - 'video': PLACEMENT_TYPE_VIDEO + 'video': PLACEMENT_TYPE_VIDEO, + 'inImage': PLACEMENT_TYPE_IN_IMAGE }; function buildBidRequests(validBidRequests) { @@ -26,7 +28,8 @@ function buildBidRequests(validBidRequests) { transactionId: validBidRequest.transactionId, sizes: validBidRequest.sizes, placement: placementTypes[params.placement], - placeId: params.placeId + placeId: params.placeId, + imageUrl: params.imageUrl || '' }; return bidRequest; @@ -94,6 +97,33 @@ function buildBid(bidData) { bid.renderer = createRenderer(bid); } } + } else if (bidData.placement === PLACEMENT_TYPE_IN_IMAGE) { + bid.mediaType = BANNER; + bid.inImageContent = { + content: { + content: bidData.content, + actionUrls: {} + } + }; + let actionUrls = bid.inImageContent.content.actionUrls; + actionUrls.loadUrls = bidData.inImage.loadtrackers || []; + actionUrls.impressionUrls = bidData.inImage.imptrackers || []; + actionUrls.scrollActUrls = bidData.inImage.startvisibilitytrackers || []; + actionUrls.viewUrls = bidData.inImage.viewtrackers || []; + actionUrls.stopAnimationUrls = bidData.inImage.stopanimationtrackers || []; + actionUrls.closeBannerUrls = bidData.inImage.closebannertrackers || []; + + if (bidData.inImage.but) { + let inImageOptions = bid.inImageContent.content.inImageOptions = {}; + inImageOptions.hasButton = true; + inImageOptions.buttonLogoUrl = bidData.inImage.but_logo; + inImageOptions.buttonProductUrl = bidData.inImage.but_prod; + inImageOptions.buttonHead = bidData.inImage.but_head; + inImageOptions.buttonHeadColor = bidData.inImage.but_head_colour; + inImageOptions.dynparams = bidData.inImage.dynparams || {}; + } + + bid.ad = wrapAd(bid, bidData); } else { bid.ad = bidData.content; bid.mediaType = BANNER; @@ -116,6 +146,30 @@ function hasVideoMandatoryParams(mediaTypes) { return isHasVideoContext && isPlayerSize; } +function wrapAd(bid, bidData) { + return ` + + + + + + + + +
+ + + `; +} + export const spec = { code: BIDDER_CODE, supportedMediaTypes: [BANNER, VIDEO], @@ -133,6 +187,7 @@ export const spec = { !!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)) ) ); diff --git a/modules/hybridBidAdapter.md b/modules/hybridBidAdapter.md index 245f010970a..098d8642415 100644 --- a/modules/hybridBidAdapter.md +++ b/modules/hybridBidAdapter.md @@ -52,3 +52,187 @@ var adUnits = [{ }]; ``` +# Sample In-Image Ad Unit + +```js +var adUnits = [{ + code: 'test-div', + mediaTypes: { + banner: { + sizes: [0, 0] + } + }, + bids: [{ + bidder: "hybrid", + params: { + placement: "inImage", + placeId: "102030405060708090000020", + imageUrl: "https://hybrid.ai/images/image.jpg" + } + }] +}]; +``` + +# 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/id5IdSystem.js b/modules/id5IdSystem.js index ae3c7e55863..8a8cd25479f 100644 --- a/modules/id5IdSystem.js +++ b/modules/id5IdSystem.js @@ -10,11 +10,20 @@ import { ajax } from '../src/ajax.js'; import { submodule } from '../src/hook.js'; import { getRefererInfo } from '../src/refererDetection.js'; import { getStorageManager } from '../src/storageManager.js'; +import { uspDataHandler } from '../src/adapterManager.js'; const MODULE_NAME = 'id5Id'; const GVLID = 131; -const BASE_NB_COOKIE_NAME = 'id5id.1st'; -const NB_COOKIE_EXP_DAYS = (30 * 24 * 60 * 60 * 1000); // 30 days +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; +const LOG_PREFIX = 'User ID - ID5 submodule: '; + +// order the legacy cookie names in reverse priority order so the last +// cookie in the array is the most preferred to use +const LEGACY_COOKIE_NAMES = [ 'pbjs-id5id', 'id5id.1st', 'id5id' ]; const storage = getStorageManager(GVLID, MODULE_NAME); @@ -36,50 +45,105 @@ 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) { - if (value && typeof value.ID5ID === 'string') { - // don't lose our legacy value from cache - return { 'id5id': value.ID5ID }; - } else if (value && typeof value.universal_uid === 'string') { - return { 'id5id': value.universal_uid }; + decode(value, config) { + let universalUid; + let linkType = 0; + + if (value && typeof value.universal_uid === 'string') { + universalUid = value.universal_uid; + linkType = value.link_type || linkType; } else { return undefined; } + + // 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(LOG_PREFIX + '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(LOG_PREFIX + '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(LOG_PREFIX + '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)); + } + + utils.logInfo(LOG_PREFIX + 'Decoded ID', responseObj); + + return responseObj; }, /** * performs action to obtain id and return a value in the callback's response argument * @function getId - * @param {SubmoduleParams} [configParams] - * @param {ConsentData} [consentData] + * @param {SubmoduleConfig} config + * @param {ConsentData} consentData * @param {(Object|undefined)} cacheIdObj * @returns {IdResponse|undefined} */ - getId(configParams, consentData, cacheIdObj) { - if (!hasRequiredParams(configParams)) { + getId(config, consentData, cacheIdObj) { + if (!hasRequiredConfig(config)) { return undefined; } + + const url = `https://id5-sync.com/g/v2/${config.params.partner}.json`; const hasGdpr = (consentData && typeof consentData.gdprApplies === 'boolean' && consentData.gdprApplies) ? 1 : 0; - const gdprConsentString = hasGdpr ? consentData.consentString : ''; - const url = `https://id5-sync.com/g/v2/${configParams.partner}.json?gdpr_consent=${gdprConsentString}&gdpr=${hasGdpr}`; + const usp = uspDataHandler.getConsentData(); const referer = getRefererInfo(); - const signature = (cacheIdObj && cacheIdObj.signature) ? cacheIdObj.signature : ''; - const pubId = (cacheIdObj && cacheIdObj.ID5ID) ? cacheIdObj.ID5ID : ''; // TODO: remove when 1puid isn't needed + const signature = (cacheIdObj && cacheIdObj.signature) ? cacheIdObj.signature : getLegacyCookieSignature(); const data = { - 'partner': configParams.partner, - '1puid': pubId, // TODO: remove when 1puid isn't needed - 'nbPage': incrementNb(configParams), + 'partner': config.params.partner, + 'gdpr': hasGdpr, + 'nbPage': incrementNb(config.params.partner), 'o': 'pbjs', - 'pd': configParams.pd || '', 'rf': referer.referer, - 's': signature, 'top': referer.reachedTop ? 1 : 0, 'u': referer.stack[0] || window.location.href, 'v': '$prebid.version$' }; + // pass in optional data, but only if populated + if (hasGdpr && typeof consentData.consentString !== 'undefined' && !utils.isEmpty(consentData.consentString) && !utils.isEmptyStr(consentData.consentString)) { + data.gdpr_consent = consentData.consentString; + } + if (typeof usp !== 'undefined' && !utils.isEmpty(usp) && !utils.isEmptyStr(usp)) { + data.us_privacy = usp; + } + if (typeof signature !== 'undefined' && !utils.isEmptyStr(signature)) { + data.s = signature; + } + if (typeof config.params.pd !== 'undefined' && !utils.isEmptyStr(config.params.pd)) { + data.pd = config.params.pd; + } + if (typeof config.params.provider !== 'undefined' && !utils.isEmptyStr(config.params.provider)) { + data.provider = config.params.provider; + } + + // pass in feature flags, if applicable + if (getAbTestingConfig(config).enabled === true) { + utils.deepSetValue(data, 'features.ab', 1); + } + const resp = function (callback) { const callbacks = { success: response => { @@ -87,18 +151,31 @@ export const id5IdSubmodule = { if (response) { try { responseObj = JSON.parse(response); - resetNb(configParams); + utils.logInfo(LOG_PREFIX + 'response received from the server', responseObj); + + 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 + if (config.storage.type === LOCAL_STORAGE) { + removeLegacyCookies(config.params.partner); + } } catch (error) { - utils.logError(error); + utils.logError(LOG_PREFIX + error); } } callback(responseObj); }, error: error => { - utils.logError(`id5Id: ID fetch encountered an error`, error); + utils.logError(LOG_PREFIX + 'getId fetch encountered an error', error); callback(); } }; + utils.logInfo(LOG_PREFIX + 'requesting an ID from the server', data); ajax(url, callbacks, JSON.stringify(data), { method: 'POST', withCredentials: true }); }; return {callback: resp}; @@ -110,43 +187,160 @@ export const id5IdSubmodule = { * If IdResponse#callback is defined, then it'll called at the end of auction. * It's permissible to return neither, one, or both fields. * @function extendId - * @param {SubmoduleParams} configParams + * @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(configParams, cacheIdObj) { - incrementNb(configParams); + extendId(config, consentData, cacheIdObj) { + hasRequiredConfig(config); + + const partnerId = (config && config.params && config.params.partner) || 0; + incrementNb(partnerId); + + utils.logInfo(LOG_PREFIX + 'using cached ID', cacheIdObj); return cacheIdObj; } }; -function hasRequiredParams(configParams) { - if (!configParams || typeof configParams.partner !== 'number') { - utils.logError(`User ID - ID5 submodule requires partner to be defined as a number`); +function hasRequiredConfig(config) { + if (!config || !config.params || !config.params.partner || typeof config.params.partner !== 'number') { + utils.logError(LOG_PREFIX + 'partner required to be defined as a number'); + return false; + } + + if (!config.storage || !config.storage.type || !config.storage.name) { + utils.logError(LOG_PREFIX + 'storage required to be set'); return false; } + + // in a future release, we may return false if storage type or name are not set as required + if (config.storage.type !== LOCAL_STORAGE) { + utils.logWarn(LOG_PREFIX + `storage type recommended to be '${LOCAL_STORAGE}'. In a future release this may become a strict requirement`); + } + // in a future release, we may return false if storage type or name are not set as required + if (config.storage.name !== ID5_STORAGE_NAME) { + utils.logWarn(LOG_PREFIX + `storage name recommended to be '${ID5_STORAGE_NAME}'. In a future release this may become a strict requirement`); + } + return true; } -function nbCookieName(configParams) { - return hasRequiredParams(configParams) ? `${BASE_NB_COOKIE_NAME}_${configParams.partner}_nb` : undefined; + +export function expDaysStr(expDays) { + return (new Date(Date.now() + (1000 * 60 * 60 * 24 * expDays))).toUTCString(); } -function nbCookieExpStr(expDays) { - return (new Date(Date.now() + expDays)).toUTCString(); + +export function nbCacheName(partnerId) { + return `${ID5_STORAGE_NAME}_${partnerId}_nb`; } -function storeNbInCookie(configParams, nb) { - storage.setCookie(nbCookieName(configParams), nb, nbCookieExpStr(NB_COOKIE_EXP_DAYS), 'Lax'); +export function storeNbInCache(partnerId, nb) { + storeInLocalStorage(nbCacheName(partnerId), nb, NB_EXP_DAYS); } -function getNbFromCookie(configParams) { - const cacheNb = storage.getCookie(nbCookieName(configParams)); +export function getNbFromCache(partnerId) { + let cacheNb = getFromLocalStorage(nbCacheName(partnerId)); return (cacheNb) ? parseInt(cacheNb) : 0; } -function incrementNb(configParams) { - const nb = (getNbFromCookie(configParams) + 1); - storeNbInCookie(configParams, nb); +function incrementNb(partnerId) { + const nb = (getNbFromCache(partnerId) + 1); + storeNbInCache(partnerId, nb); return nb; } -function resetNb(configParams) { - storeNbInCookie(configParams, 0); +function resetNb(partnerId) { + storeNbInCache(partnerId, 0); +} + +function getLegacyCookieSignature() { + let legacyStoredValue; + LEGACY_COOKIE_NAMES.forEach(function(cookie) { + if (storage.getCookie(cookie)) { + legacyStoredValue = JSON.parse(storage.getCookie(cookie)) || legacyStoredValue; + } + }); + return (legacyStoredValue && legacyStoredValue.signature) || ''; +} + +/** + * Remove our legacy cookie values. Needed until we move all publishers + * to html5 storage in a future release + * @param {integer} partnerId + */ +function removeLegacyCookies(partnerId) { + utils.logInfo(LOG_PREFIX + 'removing legacy cookies'); + LEGACY_COOKIE_NAMES.forEach(function(cookie) { + storage.setCookie(`${cookie}`, ' ', expDaysStr(-1)); + storage.setCookie(`${cookie}_nb`, ' ', expDaysStr(-1)); + storage.setCookie(`${cookie}_${partnerId}_nb`, ' ', expDaysStr(-1)); + storage.setCookie(`${cookie}_last`, ' ', expDaysStr(-1)); + }); +} + +/** + * This will make sure we check for expiration before accessing local storage + * @param {string} key + */ +export function getFromLocalStorage(key) { + const storedValueExp = storage.getDataFromLocalStorage(`${key}_exp`); + // empty string means no expiration set + if (storedValueExp === '') { + return storage.getDataFromLocalStorage(key); + } else if (storedValueExp) { + if ((new Date(storedValueExp)).getTime() - Date.now() > 0) { + return storage.getDataFromLocalStorage(key); + } + } + // if we got here, then we have an expired item or we didn't set an + // expiration initially somehow, so we need to remove the item from the + // local storage + storage.removeDataFromLocalStorage(key); + return null; +} +/** + * Ensure that we always set an expiration in local storage since + * by default it's not required + * @param {string} key + * @param {any} value + * @param {integer} expDays + */ +export function storeInLocalStorage(key, value, expDays) { + storage.setDataInLocalStorage(`${key}_exp`, expDaysStr(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 new file mode 100644 index 00000000000..6a662361492 --- /dev/null +++ b/modules/id5IdSystem.md @@ -0,0 +1,68 @@ +# 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://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 + +The ID5 Universal ID is free to use, but requires a simple registration with ID5. Please visit [id5.io/universal-id](https://id5.io/universal-id) to sign up and request your ID5 Partner Number to get started. + +The ID5 privacy policy is at [https://www.id5.io/platform-privacy-policy](https://www.id5.io/platform-privacy-policy). + +## ID5 Universal ID Configuration + +First, make sure to add the ID5 submodule to your Prebid.js package with: + +``` +gulp build --modules=id5IdSystem,userId +``` + +The following configuration parameters are available: + +```javascript +pbjs.setConfig({ + userSync: { + userIds: [{ + 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 + 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 + expires: 90, // storage lasts for 90 days + refreshInSeconds: 8*3600 // refresh ID every 8 hours to ensure it's fresh + } + }], + auctionDelay: 50 // 50ms maximum auction delay, applies to all userId modules + } +}); +``` + +| Param under userSync.userIds[] | Scope | Type | Description | Example | +| --- | --- | --- | --- | --- | +| name | Required | String | The name of this module: `"id5Id"` | `"id5Id"` | +| params | Required | Object | Details for the ID5 Universal ID. | | +| 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"` | +| storage.expires | Optional | Integer | How long (in days) the user ID information will be stored. ID5 recommends `90`. | `90` | +| 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/idImportLibrary.js b/modules/idImportLibrary.js new file mode 100644 index 00000000000..2a3a86cf270 --- /dev/null +++ b/modules/idImportLibrary.js @@ -0,0 +1,243 @@ +import {getGlobal} from '../src/prebidGlobal.js'; +import {ajax} from '../src/ajax.js'; +import {config} from '../src/config.js'; +import * as utils from '../src/utils.js'; +import MD5 from 'crypto-js/md5.js'; + +let email; +let conf; +const LOG_PRE_FIX = 'ID-Library: '; +const CONF_DEFAULT_OBSERVER_DEBOUNCE_MS = 250; +const CONF_DEFAULT_FULL_BODY_SCAN = false; +const OBSERVER_CONFIG = { + subtree: true, + attributes: true, + attributeOldValue: false, + childList: true, + attirbuteFilter: ['value'], + characterData: true, + characterDataOldValue: false +}; +const logInfo = createLogInfo(LOG_PRE_FIX); +const logError = createLogError(LOG_PRE_FIX); + +function createLogInfo(prefix) { + return function (...strings) { + utils.logInfo(prefix + ' ', ...strings); + } +} + +function createLogError(prefix) { + return function (...strings) { + utils.logError(prefix + ' ', ...strings); + } +} + +function getEmail(value) { + const matched = value.match(/([a-zA-Z0-9._-]+@[a-zA-Z0-9._-]+\.[a-zA-Z0-9._-]+)/gi); + if (!matched) { + return null; + } + logInfo('Email found: ' + matched[0]); + return matched[0]; +} + +function bodyAction(mutations, observer) { + logInfo('BODY observer on debounce called'); + // If the email is found in the input element, disconnect the observer + if (email) { + observer.disconnect(); + logInfo('Email is found, body observer disconnected'); + return; + } + + const body = document.body.innerHTML; + email = getEmail(body); + if (email !== null) { + logInfo(`Email obtained from the body ${email}`); + observer.disconnect(); + logInfo('Post data on email found in body'); + postData(); + } +} + +function targetAction(mutations, observer) { + logInfo('Target observer called'); + for (const mutation of mutations) { + for (const node of mutation.addedNodes) { + email = node.textContent; + + if (email) { + logInfo('Email obtained from the target ' + email); + observer.disconnect(); + logInfo('Post data on email found in target'); + postData(); + return; + } + } + } +} + +function addInputElementsElementListner(conf) { + logInfo('Adding input element listeners'); + const inputs = document.querySelectorAll('input[type=text], input[type=email]'); + + for (var i = 0; i < inputs.length; i++) { + logInfo(`Original Value in Input = ${inputs[i].value}`); + inputs[i].addEventListener('change', event => processInputChange(event)); + inputs[i].addEventListener('blur', event => processInputChange(event)); + } +} + +function removeInputElementsElementListner() { + logInfo('Removing input element listeners'); + const inputs = document.querySelectorAll('input[type=text], input[type=email]'); + + for (var i = 0; i < inputs.length; i++) { + inputs[i].removeEventListener('change', event => processInputChange(event)); + inputs[i].removeEventListener('blur', event => processInputChange(event)); + } +} + +function processInputChange(event) { + const value = event.target.value; + logInfo(`Modified Value of input ${event.target.value}`); + email = getEmail(value); + if (email !== null) { + logInfo('Email found in input ' + email); + postData(); + removeInputElementsElementListner(); + } +} + +function debounce(func, wait, immediate) { + var timeout; + return function () { + const context = this; + const args = arguments; + const later = function () { + timeout = null; + if (!immediate) func.apply(context, args); + }; + var callNow = immediate && !timeout; + clearTimeout(timeout); + if (callNow) { + func.apply(context, args); + } else { + logInfo('Debounce wait time ' + wait); + timeout = setTimeout(later, wait); + } + }; +}; + +function handleTargetElement() { + const targetObserver = new MutationObserver(debounce(targetAction, conf.debounce, false)); + + const targetElement = document.getElementById(conf.target); + if (targetElement) { + email = targetElement.innerText; + + if (!email) { + logInfo('Finding the email with observer'); + targetObserver.observe(targetElement, OBSERVER_CONFIG); + } else { + logInfo('Target found with target ' + email); + logInfo('Post data on email found in target with target'); + postData(); + } + } +} + +function handleBodyElements() { + if (doesInputElementsHaveEmail()) { + logInfo('Email found in input elements ' + email); + logInfo('Post data on email found in target without'); + postData(); + return; + } + email = getEmail(document.body.innerHTML); + if (email !== null) { + logInfo('Email found in body ' + email); + logInfo('Post data on email found in the body without observer'); + postData(); + return; + } + addInputElementsElementListner(); + if (conf.fullscan === true) { + const bodyObserver = new MutationObserver(debounce(bodyAction, conf.debounce, false)); + bodyObserver.observe(document.body, OBSERVER_CONFIG); + } +} + +function doesInputElementsHaveEmail() { + const inputs = document.getElementsByTagName('input'); + + for (let index = 0; index < inputs.length; ++index) { + const curInput = inputs[index]; + email = getEmail(curInput.value); + if (email !== null) { + return true; + } + } + return false; +} + +function syncCallback() { + return { + success: function () { + logInfo('Data synced successfully.'); + }, + error: function () { + logInfo('Data sync failed.'); + } + } +} + +function postData() { + (getGlobal()).refreshUserIds(); + const userIds = (getGlobal()).getUserIds(); + if (Object.keys(userIds).length === 0) { + logInfo('No user ids'); + return; + } + logInfo('Users' + userIds); + const syncPayload = {}; + syncPayload.hid = MD5(email).toString(); + syncPayload.uids = userIds; + const payloadString = JSON.stringify(syncPayload); + logInfo(payloadString); + ajax(conf.url, syncCallback(), payloadString, {method: 'POST', withCredentials: true}); +} + +function associateIds() { + if (window.MutationObserver || window.WebKitMutationObserver) { + if (conf.target) { + handleTargetElement(); + } else { + handleBodyElements(); + } + } +} + +export function setConfig(config) { + if (!config) { + logError('Required confirguration not provided'); + return; + } + if (!config.url) { + logError('The required url is not configured'); + return; + } + if (typeof config.debounce !== 'number') { + config.debounce = CONF_DEFAULT_OBSERVER_DEBOUNCE_MS; + logInfo('Set default observer debounce to ' + CONF_DEFAULT_OBSERVER_DEBOUNCE_MS); + } + if (typeof config.fullscan !== 'boolean') { + config.fullscan = CONF_DEFAULT_FULL_BODY_SCAN; + logInfo('Set default fullscan ' + CONF_DEFAULT_FULL_BODY_SCAN); + } + conf = config; + associateIds(); +} + +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/identityLinkIdSystem.js b/modules/identityLinkIdSystem.js index c516c06d11a..df7b03b4e6e 100644 --- a/modules/identityLinkIdSystem.js +++ b/modules/identityLinkIdSystem.js @@ -6,8 +6,11 @@ */ import * as utils from '../src/utils.js' -import {ajax} from '../src/ajax.js'; -import {submodule} from '../src/hook.js'; +import { ajax } from '../src/ajax.js'; +import { submodule } from '../src/hook.js'; +import {getStorageManager} from '../src/storageManager.js'; + +export const storage = getStorageManager(); /** @type {Submodule} */ export const identityLinkSubmodule = { @@ -16,6 +19,11 @@ export const identityLinkSubmodule = { * @type {string} */ name: 'identityLink', + /** + * used to specify vendor id + * @type {number} + */ + gvlid: 97, /** * decode the stored id value for passing to bid requests * @function @@ -29,43 +37,49 @@ export const identityLinkSubmodule = { * performs action to obtain id and return a value in the callback's response argument * @function * @param {ConsentData} [consentData] - * @param {SubmoduleParams} [configParams] + * @param {SubmoduleConfig} [config] * @returns {IdResponse|undefined} */ - getId(configParams, consentData) { + 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; const gdprConsentString = hasGdpr ? consentData.consentString : ''; + const tcfPolicyV2 = utils.deepAccess(consentData, 'vendorData.tcfPolicyVersion') === 2; // use protocol relative urls for http or https - const url = `https://api.rlcdn.com/api/identity/envelope?pid=${configParams.pid}${hasGdpr ? '&ct=1&cv=' + gdprConsentString : ''}`; + if (hasGdpr && (!gdprConsentString || gdprConsentString === '')) { + utils.logInfo('identityLink: Consent string is required to call envelope API.'); + return; + } + const url = `https://api.rlcdn.com/api/identity/envelope?pid=${configParams.pid}${hasGdpr ? (tcfPolicyV2 ? '&ct=4&cv=' : '&ct=1&cv=') + gdprConsentString : ''}`; let resp; - resp = function(callback) { + resp = function (callback) { // Check ats during callback so it has a chance to initialise. // If ats library is available, use it to retrieve envelope. If not use standard third party endpoint if (window.ats) { - 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); + getEnvelope(url, callback, configParams); } }); } else { - getEnvelope(url, callback); + getEnvelope(url, callback, configParams); } }; - return {callback: resp}; + return { callback: resp }; } }; // return envelope from third party endpoint -function getEnvelope(url, callback) { - utils.logInfo('A 3P retrieval is attempted!'); +function getEnvelope(url, callback, configParams) { const callbacks = { success: response => { let responseObj; @@ -79,11 +93,29 @@ 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(); } }; - ajax(url, callbacks, undefined, {method: 'GET', withCredentials: true}); + + if (!configParams.notUse3P && !storage.getCookie('_lr_retry_request')) { + setRetryCookie(); + utils.logInfo('identityLink: A 3P retrieval is attempted!'); + setEnvelopeSource(false); + ajax(url, callbacks, undefined, { method: 'GET', withCredentials: true }); + } +} + +function setRetryCookie() { + let now = new Date(); + now.setTime(now.getTime() + 3600000); + 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/idxIdSystem.js b/modules/idxIdSystem.js new file mode 100644 index 00000000000..00e8a8bc5e5 --- /dev/null +++ b/modules/idxIdSystem.js @@ -0,0 +1,61 @@ +/** + * This module adds IDx to the User ID module + * The {@link module:modules/userId} module is required + * @module modules/idxIdSystem + * @requires module:modules/userId + */ +import * as utils from '../src/utils.js' +import { submodule } from '../src/hook.js'; +import { getStorageManager } from '../src/storageManager.js'; + +const IDX_MODULE_NAME = 'idx'; +const IDX_COOKIE_NAME = '_idx'; +export const storage = getStorageManager(); + +function readIDxFromCookie() { + return storage.cookiesAreEnabled ? storage.getCookie(IDX_COOKIE_NAME) : null; +} + +function readIDxFromLocalStorage() { + return storage.localStorageIsEnabled ? storage.getDataFromLocalStorage(IDX_COOKIE_NAME) : null; +} + +/** @type {Submodule} */ +export const idxIdSubmodule = { + /** + * used to link submodule with config + * @type {string} + */ + name: IDX_MODULE_NAME, + /** + * decode the stored id value for passing to bid requests + * @function + * @param { Object | string | undefined } value + * @return { Object | string | undefined } + */ + decode(value) { + const idxVal = value ? utils.isStr(value) ? value : utils.isPlainObject(value) ? value.id : undefined : undefined; + return idxVal ? { + 'idx': idxVal + } : undefined; + }, + /** + * performs action to obtain id and return a value in the callback's response argument + * @function + * @param {SubmoduleConfig} config + * @return {{id: string | undefined } | undefined} + */ + getId() { + const idxString = readIDxFromLocalStorage() || readIDxFromCookie(); + if (typeof idxString == 'string' && idxString) { + try { + const idxObj = JSON.parse(idxString); + return idxObj && idxObj.idx ? { id: idxObj.idx } : undefined; + } catch (error) { + utils.logError(error); + } + } + return undefined; + } +}; +submodule('userId', idxIdSubmodule); diff --git a/modules/idxIdSystem.md b/modules/idxIdSystem.md new file mode 100644 index 00000000000..363120899cb --- /dev/null +++ b/modules/idxIdSystem.md @@ -0,0 +1,22 @@ +## IDx User ID Submodule + +For assistance setting up your module please contact us at [prebid@idx.lat](prebid@idx.lat). + +### Prebid Params + +Individual params may be set for the IDx Submodule. +``` +pbjs.setConfig({ + userSync: { + userIds: [{ + name: 'idx', + }] + } +}); +``` +## Parameter Descriptions for the `userSync` Configuration Section +The below parameters apply only to the IDx integration. + +| Param under usersync.userIds[] | Scope | Type | Description | Example | +| --- | --- | --- | --- | --- | +| name | Required | String | ID of the module - `"idx"` | `"idx"` | diff --git a/modules/impactifyBidAdapter.js b/modules/impactifyBidAdapter.js new file mode 100644 index 00000000000..b649b5a8a73 --- /dev/null +++ b/modules/impactifyBidAdapter.js @@ -0,0 +1,260 @@ +import { registerBidder } from '../src/adapters/bidderFactory.js'; +import * as utils from '../src/utils.js'; +import { config } from '../src/config.js'; +import {ajax} from '../src/ajax.js'; + +const BIDDER_CODE = 'impactify'; +const BIDDER_ALIAS = ['imp']; +const DEFAULT_CURRENCY = 'USD'; +const DEFAULT_VIDEO_WIDTH = 640; +const DEFAULT_VIDEO_HEIGHT = 480; +const ORIGIN = 'https://sonic.impactify.media'; +const LOGGER_URI = 'https://logger.impactify.media'; +const AUCTIONURI = '/bidder'; +const COOKIESYNCURI = '/static/cookie_sync.html'; +const GVLID = 606; +const GETCONFIG = config.getConfig; + +const getDeviceType = () => { + // OpenRTB Device type + if ((/ipad|android 3.0|xoom|sch-i800|playbook|tablet|kindle/i.test(navigator.userAgent.toLowerCase()))) { + return 5; + } + if ((/iphone|ipod|android|blackberry|opera|mini|windows\sce|palm|smartphone|iemobile/i.test(navigator.userAgent.toLowerCase()))) { + return 4; + } + return 2; +} + +const createOpenRtbRequest = (validBidRequests, bidderRequest) => { + // Create request and set imp bids inside + let request = { + id: bidderRequest.auctionId, + validBidRequests, + cur: [DEFAULT_CURRENCY], + imp: [] + }; + + // Force impactify debugging parameter + if (window.localStorage.getItem('_im_db_bidder') == 3) { + request.test = 3; + } + + // Set device/user/site + if (!request.device) request.device = {}; + if (!request.site) request.site = {}; + request.device = { + w: window.innerWidth, + h: window.innerHeight, + devicetype: getDeviceType(), + ua: navigator.userAgent, + js: 1, + dnt: (navigator.doNotTrack == 'yes' || navigator.doNotTrack == '1' || navigator.msDoNotTrack == '1') ? 1 : 0, + language: ((navigator.language || navigator.userLanguage || '').split('-'))[0] || 'en', + }; + request.site = {page: bidderRequest.refererInfo.referer}; + + // Handle privacy settings for GDPR/CCPA/COPPA + if (bidderRequest.gdprConsent) { + let gdprApplies = 0; + if (typeof bidderRequest.gdprConsent.gdprApplies === 'boolean') gdprApplies = bidderRequest.gdprConsent.gdprApplies ? 1 : 0; + utils.deepSetValue(request, 'regs.ext.gdpr', gdprApplies); + utils.deepSetValue(request, 'user.ext.consent', bidderRequest.gdprConsent.consentString); + } + + if (bidderRequest.uspConsent) { + utils.deepSetValue(request, 'regs.ext.us_privacy', bidderRequest.uspConsent); + this.syncStore.uspConsent = bidderRequest.uspConsent; + } + + if (GETCONFIG('coppa') == true) utils.deepSetValue(request, 'regs.coppa', 1); + + if (bidderRequest.uspConsent) { + utils.deepSetValue(request, 'regs.ext.us_privacy', bidderRequest.uspConsent); + } + + // Set buyer uid + utils.deepSetValue(request, 'user.buyeruid', utils.generateUUID()); + + // Create imps with bids + validBidRequests.forEach((bid) => { + let imp = { + id: bid.bidId, + bidfloor: bid.params.bidfloor ? bid.params.bidfloor : 0, + ext: { + impactify: { + appId: bid.params.appId, + format: bid.params.format, + style: bid.params.style + }, + }, + video: { + playerSize: [DEFAULT_VIDEO_WIDTH, DEFAULT_VIDEO_HEIGHT], + context: 'outstream', + mimes: ['video/mp4'], + }, + }; + if (bid.params.container) { + imp.ext.impactify.container = bid.params.container; + } + request.imp.push(imp); + }); + + return request; +}; + +export const spec = { + code: BIDDER_CODE, + gvlid: GVLID, + supportedMediaTypes: ['video'], + aliases: BIDDER_ALIAS, + /** + * Determines whether or not the given bid request is valid. + * + * @param {BidRequest} bid The bid params to validate. + * @return boolean True if this is a valid bid, and false otherwise. + */ + isBidRequestValid: function (bid) { + if (!bid.params.appId || typeof bid.params.appId != 'string' || !bid.params.format || typeof bid.params.format != 'string' || !bid.params.style || typeof bid.params.style != 'string') { + return false; + } + if (bid.params.format != 'screen' && bid.params.format != 'display') { + return false; + } + if (bid.params.style != 'inline' && bid.params.style != 'impact' && bid.params.style != 'static') { + return false; + } + + return true; + }, + /** + * Make a server request from the list of BidRequests. + * + * @param {validBidRequests[]} - an array of bids + * @param {bidderRequest} - the bidding request + * @return ServerRequest Info describing the request to the server. + */ + buildRequests: function (validBidRequests, bidderRequest) { + // Create a clean openRTB request + let request = createOpenRtbRequest(validBidRequests, bidderRequest); + + return { + method: 'POST', + url: ORIGIN + AUCTIONURI, + data: JSON.stringify(request), + }; + }, + /** + * Unpack the response from the server into a list of bids. + * + * @param {ServerResponse} serverResponse A successful response from the server. + * @return {Bid[]} An array of bids which were nested inside the server. + */ + interpretResponse: function (serverResponse, bidRequest) { + const serverBody = serverResponse.body; + let bidResponses = []; + + if (!serverBody) { + return bidResponses; + } + + if (!serverBody.seatbid || !serverBody.seatbid.length) { + return []; + } + + serverBody.seatbid.forEach((seatbid) => { + if (seatbid.bid.length) { + bidResponses = [ + ...bidResponses, + ...seatbid.bid + .filter((bid) => bid.price > 0) + .map((bid) => ({ + id: bid.id, + requestId: bid.impid, + cpm: bid.price, + currency: serverBody.cur, + netRevenue: true, + ad: bid.adm, + width: bid.w || 0, + height: bid.h || 0, + ttl: 300, + creativeId: bid.crid || 0, + hash: bid.hash, + expiry: bid.expiry + })), + ]; + } + }); + + return bidResponses; + }, + + /** + * Register the user sync pixels which should be dropped after the auction. + * + * @param {SyncOptions} syncOptions Which user syncs are allowed? + * @param {ServerResponse[]} serverResponses List of server's responses. + * @return {UserSync[]} The user syncs which should be dropped. + */ + getUserSyncs: function ( + syncOptions, + serverResponses, + gdprConsent, + uspConsent + ) { + if (!serverResponses || serverResponses.length === 0) { + return []; + } + + if (!syncOptions.iframeEnabled) { + return []; + } + + let params = ''; + if (gdprConsent && typeof gdprConsent.consentString === 'string') { + if (typeof gdprConsent.gdprApplies === 'boolean') { + params += `?gdpr=${Number(gdprConsent.gdprApplies)}&gdpr_consent=${gdprConsent.consentString}`; + } else { + params += `?gdpr_consent=${gdprConsent.consentString}`; + } + } + + if (uspConsent) { + params += `${params ? '&' : '?'}us_privacy=${encodeURIComponent(uspConsent)}`; + } + + if (document.location.search.match(/pbs_debug=true/)) params += `&pbs_debug=true`; + + return [{ + type: 'iframe', + url: ORIGIN + COOKIESYNCURI + params + }]; + }, + + /** + * Register bidder specific code, which will execute if a bid from this bidder won the auction + * @param {Bid} The bid that won the auction + */ + onBidWon: function(bid) { + ajax(`${LOGGER_URI}/log/bidder/won`, null, JSON.stringify(bid), { + method: 'POST', + contentType: 'application/json' + }); + + return true; + }, + + /** + * Register bidder specific code, which will execute if bidder timed out after an auction + * @param {data} Containing timeout specific data + */ + onTimeout: function(data) { + ajax(`${LOGGER_URI}/log/bidder/timeout`, null, JSON.stringify(data[0]), { + method: 'POST', + contentType: 'application/json' + }); + + return true; + } +}; +registerBidder(spec); diff --git a/modules/impactifyBidAdapter.md b/modules/impactifyBidAdapter.md new file mode 100644 index 00000000000..3de9a8cfb84 --- /dev/null +++ b/modules/impactifyBidAdapter.md @@ -0,0 +1,35 @@ +# Overview + +``` +Module Name: Impactify Bidder Adapter +Module Type: Bidder Adapter +Maintainer: thomas.destefano@impactify.io +``` + +# Description + +Module that connects to the Impactify solution. +The impactify bidder need 3 parameters: + - appId : This is your unique publisher identifier + - format : This is the ad format needed, can be : screen or display + - style : This is the ad style needed, can be : inline, impact or static + +# Test Parameters +``` + var adUnits = [{ + code: 'your-slot-div-id', // This is your slot div id + mediaTypes: { + video: { + context: 'outstream' + } + }, + bids: [{ + bidder: 'impactify', + params: { + appId: 'example.com', + format: 'screen', + style: 'inline' + } + }] + }]; +``` diff --git a/modules/improvedigitalBidAdapter.js b/modules/improvedigitalBidAdapter.js index 3c000258ede..dc0911ff5da 100644 --- a/modules/improvedigitalBidAdapter.js +++ b/modules/improvedigitalBidAdapter.js @@ -3,12 +3,15 @@ import { registerBidder } from '../src/adapters/bidderFactory.js'; import { config } from '../src/config.js'; import { BANNER, NATIVE, VIDEO } from '../src/mediaTypes.js'; import {Renderer} from '../src/Renderer.js'; +import { createEidsArray } from './userId/eids.js'; +import includes from 'core-js-pure/features/array/includes.js'; 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.3.0', code: BIDDER_CODE, gvlid: 253, aliases: ['id'], @@ -56,6 +59,13 @@ export const spec = { requestParameters.schain = bidRequests[0].schain; + if (bidRequests[0].userId) { + const eids = createEidsArray(bidRequests[0].userId); + if (eids.length) { + utils.deepSetValue(requestParameters, 'user.ext.eids', eids); + } + } + let requestObj = idClient.createRequest( normalizedBids, // requestObject requestParameters @@ -116,7 +126,6 @@ export const spec = { } // Common properties - bid.adId = bidObject.id; bid.cpm = parseFloat(bidObject.price); bid.creativeId = bidObject.crid; bid.currency = bidObject.currency ? bidObject.currency.toUpperCase() : 'USD'; @@ -194,6 +203,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({ @@ -247,6 +271,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 { @@ -392,7 +419,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, @@ -552,6 +579,9 @@ export function ImproveDigitalAdServerJSClient(endPoint) { if (requestParameters.schain) { impressionBidRequestObject.schain = requestParameters.schain; } + if (requestParameters.user) { + impressionBidRequestObject.user = requestParameters.user; + } if (extraRequestParameters) { for (let prop in extraRequestParameters) { impressionBidRequestObject[prop] = extraRequestParameters[prop]; @@ -598,6 +628,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 new file mode 100755 index 00000000000..e1edd935587 --- /dev/null +++ b/modules/inmarBidAdapter.js @@ -0,0 +1,110 @@ +import * as utils from '../src/utils.js'; +import { config } from '../src/config.js'; +import { registerBidder } from '../src/adapters/bidderFactory.js'; +import { BANNER, VIDEO } from '../src/mediaTypes.js'; + +const BIDDER_CODE = 'inmar'; + +export const spec = { + code: BIDDER_CODE, + aliases: ['inm'], + supportedMediaTypes: [BANNER, VIDEO], + + /** + * Determines whether or not the given bid request is valid + * + * @param {bidRequest} bid The bid params to validate. + * @returns {boolean} True if this is a valid bid, and false otherwise + */ + isBidRequestValid: function(bid) { + return !!(bid.params && bid.params.partnerId); + }, + + /** + * Build a server request from the list of valid BidRequests + * @param {validBidRequests} is an array of the valid bids + * @param {bidderRequest} bidder request object + * @returns {ServerRequest} Info describing the request to the server + */ + buildRequests: function(validBidRequests, bidderRequest) { + var payload = { + bidderCode: bidderRequest.bidderCode, + auctionId: bidderRequest.auctionId, + bidderRequestId: bidderRequest.bidderRequestId, + bidRequests: validBidRequests, + auctionStart: bidderRequest.auctionStart, + timeout: bidderRequest.timeout, + refererInfo: bidderRequest.refererInfo, + start: bidderRequest.start, + gdprConsent: bidderRequest.gdprConsent, + uspConsent: bidderRequest.uspConsent, + currencyCode: config.getConfig('currency.adServerCurrency'), + coppa: config.getConfig('coppa'), + firstPartyData: config.getLegacyFpd(config.getConfig('ortb2')), + prebidVersion: '$prebid.version$' + }; + + var payloadString = JSON.stringify(payload); + + return { + method: 'POST', + url: 'https://prebid.owneriq.net:8443/bidder/pb/bid', + data: payloadString, + }; + }, + + /** + * Read the response from the server and build a list of bids + * @param {serverResponse} Response from the server. + * @param {bidRequest} Bid request object + * @returns {bidResponses} Array of bids which were nested inside the server + */ + interpretResponse: function(serverResponse, bidRequest) { + const bidResponses = []; + var response = serverResponse.body; + + try { + if (response) { + var bidResponse = { + requestId: response.requestId, + cpm: response.cpm, + currency: response.currency, + width: response.width, + height: response.height, + ad: response.ad, + ttl: response.ttl, + creativeId: response.creativeId, + netRevenue: response.netRevenue, + vastUrl: response.vastUrl, + dealId: response.dealId, + meta: response.meta + }; + + bidResponses.push(bidResponse); + } + } catch (error) { + utils.logError('Error while parsing inmar response', error); + } + return bidResponses; + }, + + /** + * User Syncs + * + * @param {syncOptions} Publisher prebid configuration + * @param {serverResponses} Response from the server + * @returns {Array} + */ + getUserSyncs: function(syncOptions, serverResponses) { + const syncs = []; + if (syncOptions.pixelEnabled) { + syncs.push({ + type: 'image', + url: 'https://px.owneriq.net/eucm/p/pb' + }); + } + return syncs; + } +}; + +registerBidder(spec); diff --git a/modules/inmarBidAdapter.md b/modules/inmarBidAdapter.md new file mode 100644 index 00000000000..8ed6b998602 --- /dev/null +++ b/modules/inmarBidAdapter.md @@ -0,0 +1,44 @@ +# Overview + +``` +Module Name: Inmar Bidder Adapter +Module Type: Bidder Adapter +Maintainer: oiq_rtb@inmar.com +``` + +# Description + +Connects to Inmar for bids. This adapter supports Display and Video. + +The Inmar adapter requires setup and approval from the Inmar team. +Please reach out to your account manager for more information. + +# Test Parameters + +## Web +``` + var adUnits = [ + { + code: 'test-div1', + sizes: [[300, 250],[300, 600]], + bids: [{ + bidder: 'inmar', + params: { + partnerId: 12345, + position: 1 + } + }] + }, + { + code: 'test-div2', + sizes: [[728, 90],[970, 250]], + bids: [{ + bidder: 'inmar', + params: { + partnerId: 12345, + 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/instreamTracking.js b/modules/instreamTracking.js new file mode 100644 index 00000000000..68bb4be79de --- /dev/null +++ b/modules/instreamTracking.js @@ -0,0 +1,114 @@ +import { config } from '../src/config.js'; +import { auctionManager } from '../src/auctionManager.js'; +import { INSTREAM } from '../src/video.js'; +import * as events from '../src/events.js'; +import * as utils from '../src/utils.js'; +import { BID_STATUS, EVENTS, TARGETING_KEYS } from '../src/constants.json'; + +const {CACHE_ID, UUID} = TARGETING_KEYS; +const {BID_WON, AUCTION_END} = EVENTS; +const {RENDERED} = BID_STATUS; + +const INSTREAM_TRACKING_DEFAULT_CONFIG = { + enabled: false, + maxWindow: 1000 * 60, // the time in ms after which polling for instream delivery stops + pollingFreq: 500 // the frequency of polling +}; + +// Set instreamTracking default values +config.setDefaults({ + 'instreamTracking': utils.deepClone(INSTREAM_TRACKING_DEFAULT_CONFIG) +}); + +const whitelistedResources = /video|fetch|xmlhttprequest|other/; + +/** + * Here the idea is + * find all network entries via performance.getEntriesByType() + * filter it by video cache key in the url + * and exclude the ad server urls so that we dont match twice + * eg: + * dfp ads call: https://securepubads.g.doubleclick.net/gampad/ads?...hb_cache_id%3D55e85cd3-6ea4-4469-b890-84241816b131%26... + * prebid cache url: https://prebid.adnxs.com/pbc/v1/cache?uuid=55e85cd3-6ea4-4469-b890-84241816b131 + * + * if the entry exists, emit the BID_WON + * + * Note: this is a workaround till a better approach is engineered. + * + * @param {Array} adUnits + * @param {Array} bidsReceived + * @param {Array} bidderRequests + * + * @return {boolean} returns TRUE if tracking started + */ +export function trackInstreamDeliveredImpressions({adUnits, bidsReceived, bidderRequests}) { + const instreamTrackingConfig = config.getConfig('instreamTracking') || {}; + // check if instreamTracking is enabled and performance api is available + if (!instreamTrackingConfig.enabled || !window.performance || !window.performance.getEntriesByType) { + return false; + } + + // filter for video bids + const instreamBids = bidsReceived.filter(bid => { + const bidderRequest = utils.getBidRequest(bid.requestId, bidderRequests); + return bidderRequest && utils.deepAccess(bidderRequest, 'mediaTypes.video.context') === INSTREAM && bid.videoCacheKey; + }); + if (!instreamBids.length) { + return false; + } + + // find unique instream ad units + const instreamAdUnitMap = {}; + adUnits.forEach(adUnit => { + if (!instreamAdUnitMap[adUnit.code] && utils.deepAccess(adUnit, 'mediaTypes.video.context') === INSTREAM) { + instreamAdUnitMap[adUnit.code] = true; + } + }); + const instreamAdUnitsCount = Object.keys(instreamAdUnitMap).length; + + const start = Date.now(); + const {maxWindow, pollingFreq, urlPattern} = instreamTrackingConfig; + + let instreamWinningBidsCount = 0; + let lastRead = 0; // offset for performance.getEntriesByType + + function poll() { + // get network entries using the last read offset + const entries = window.performance.getEntriesByType('resource').splice(lastRead); + for (const resource of entries) { + const url = resource.name; + // check if the resource is of whitelisted resource to avoid checking img or css or script urls + if (!whitelistedResources.test(resource.initiatorType)) { + continue; + } + + instreamBids.forEach((bid) => { + // match the video cache key excluding ad server call + const matches = !(url.indexOf(CACHE_ID) !== -1 || url.indexOf(UUID) !== -1) && url.indexOf(bid.videoCacheKey) !== -1; + if (urlPattern && urlPattern instanceof RegExp && !urlPattern.test(url)) { + return; + } + if (matches && bid.status !== RENDERED) { + // video found + instreamWinningBidsCount++; + auctionManager.addWinningBid(bid); + events.emit(BID_WON, bid); + } + }); + } + // update offset + lastRead += entries.length; + + const timeElapsed = Date.now() - start; + if (timeElapsed < maxWindow && instreamWinningBidsCount < instreamAdUnitsCount) { + setTimeout(poll, pollingFreq); + } + } + + // start polling for network entries + setTimeout(poll, pollingFreq); + + return true; +} + +events.on(AUCTION_END, trackInstreamDeliveredImpressions) diff --git a/modules/intentIqIdSystem.js b/modules/intentIqIdSystem.js index 7d497ea9b1a..98732da95d5 100644 --- a/modules/intentIqIdSystem.js +++ b/modules/intentIqIdSystem.js @@ -1,68 +1,193 @@ -/** - * This module adds IntentIqId to the User ID module - * The {@link module:modules/userId} module is required - * @module modules/intentIqIdSystem - * @requires module:modules/userId - */ - -import * as utils from '../src/utils.js' -import {ajax} from '../src/ajax.js'; -import {submodule} from '../src/hook.js' - -const MODULE_NAME = 'intentIqId'; - -/** @type {Submodule} */ -export const intentIqIdSubmodule = { - /** - * used to link submodule with config - * @type {string} - */ - name: MODULE_NAME, - /** - * decode the stored id value for passing to bid requests - * @function - * @param {{ctrid:string}} value - * @returns {{intentIqId:string}} - */ - decode(value) { - return (value && typeof value['ctrid'] === 'string') ? { 'intentIqId': value['ctrid'] } : undefined; - }, - /** - * performs action to obtain id and return a value in the callback's response argument - * @function - * @param {SubmoduleParams} [configParams] - * @returns {IdResponse|undefined} - */ - getId(configParams) { - if (!configParams || typeof configParams.partner !== 'number') { - utils.logError('User ID - intentIqId submodule requires a valid partner to be defined'); - return; - } - - // use protocol relative urls for http or https - const url = `https://api.intentiq.com/profiles_engine/ProfilesEngineServlet?at=39&mi=10&dpi=${configParams.partner}&pt=17&dpn=1`; - const resp = function (callback) { - const callbacks = { - success: response => { - let responseObj; - if (response) { - try { - responseObj = JSON.parse(response); - } catch (error) { - utils.logError(error); - } - } - callback(responseObj); - }, - error: error => { - utils.logError(`${MODULE_NAME}: ID fetch encountered an error`, error); - callback(); - } - }; - ajax(url, callbacks, undefined, {method: 'GET', withCredentials: true}); - }; - return {callback: resp}; - } -}; - -submodule('userId', intentIqIdSubmodule); +/** + * This module adds IntentIqId to the User ID module + * The {@link module:modules/userId} module is required + * @module modules/intentIqIdSystem + * @requires module:modules/userId + */ + +import * as utils from '../src/utils.js' +import {ajax} from '../src/ajax.js'; +import {submodule} from '../src/hook.js' +import {getStorageManager} from '../src/storageManager.js'; + +const PCID_EXPIRY = 365; + +const MODULE_NAME = 'intentIqId'; +export const FIRST_PARTY_KEY = '_iiq_fdata'; + +export const storage = getStorageManager(undefined, MODULE_NAME); + +const NOT_AVAILABLE = 'NA'; + +/** + * Verify the response is valid - Id value or Not Found (ignore not available response) + * @param response + * @param respJson - parsed json response + * @returns {boolean} + */ +function isValidResponse(response, respJson) { + if (!response || response == '' || response === NOT_AVAILABLE) { + // Empty or NA response + return false; + } else if (respJson && (respJson.RESULT === NOT_AVAILABLE || respJson.data == '' || respJson.data === NOT_AVAILABLE)) { + // Response type is json with value NA + return false; + } else { return true; } +} + +/** + * Verify the response json is valid + * @param respJson - parsed json response + * @returns {boolean} + */ +function isValidResponseJson(respJson) { + if (respJson && 'data' in respJson) { + return true; + } else { return false; } +} + +/** + * Generate standard UUID string + * @return {string} + */ +function generateGUID() { + let d = new Date().getTime(); + const guid = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) { + const r = (d + Math.random() * 16) % 16 | 0; + d = Math.floor(d / 16); + return (c == 'x' ? r : (r & 0x3 | 0x8)).toString(16); + }); + return guid; +} + +/** + * Read Intent IQ data from cookie or local storage + * @param key + * @return {string} + */ +export function readData(key) { + try { + if (storage.hasLocalStorage()) { + return storage.getDataFromLocalStorage(key); + } + if (storage.cookiesAreEnabled()) { + return storage.getCookie(key); + } + } catch (error) { + utils.logError(error); + } +} + +/** + * Store Intent IQ data in either cookie or local storage + * expiration date: 365 days + * @param key + * @param {string} value IntentIQ ID value to sintentIqIdSystem_spec.jstore + */ +function storeData(key, value) { + try { + utils.logInfo(MODULE_NAME + ': storing data: key=' + key + ' value=' + value); + + if (value) { + if (storage.hasLocalStorage()) { + storage.setDataInLocalStorage(key, value); + } + const expiresStr = (new Date(Date.now() + (PCID_EXPIRY * (60 * 60 * 24 * 1000)))).toUTCString(); + if (storage.cookiesAreEnabled()) { + storage.setCookie(key, value, expiresStr, 'LAX'); + } + } + } catch (error) { + utils.logError(error); + } +} + +/** + * Parse json if possible, else return null + * @param data + * @param {object|null} + */ +function tryParse(data) { + try { + return JSON.parse(data); + } catch (err) { + utils.logError(err); + return null; + } +} + +/** @type {Submodule} */ +export const intentIqIdSubmodule = { + /** + * used to link submodule with config + * @type {string} + */ + name: MODULE_NAME, + /** + * decode the stored id value for passing to bid requests + * @function + * @param {{string}} value + * @returns {{intentIqId: {string}}|undefined} + */ + decode(value) { + return isValidResponse(value, undefined) ? { 'intentIqId': value } : undefined; + }, + /** + * performs action to obtain id and return a value in the callback's response argument + * @function + * @param {SubmoduleConfig} [config] + * @returns {IdResponse|undefined} + */ + getId(config) { + const configParams = (config && config.params) || {}; + if (!configParams || typeof configParams.partner !== 'number') { + utils.logError('User ID - intentIqId submodule requires a valid partner to be defined'); + return; + } + + // Read Intent IQ 1st party id or generate it if none exists + let firstPartyData = tryParse(readData(FIRST_PARTY_KEY)); + if (!firstPartyData || !firstPartyData.pcid) { + const firstPartyId = generateGUID(); + firstPartyData = { 'pcid': firstPartyId }; + storeData(FIRST_PARTY_KEY, JSON.stringify(firstPartyData)); + } + + // use protocol relative urls for http or https + let url = `https://api.intentiq.com/profiles_engine/ProfilesEngineServlet?at=39&mi=10&dpi=${configParams.partner}&pt=17&dpn=1`; + url += configParams.pcid ? '&pcid=' + encodeURIComponent(configParams.pcid) : ''; + url += configParams.pai ? '&pai=' + encodeURIComponent(configParams.pai) : ''; + if (firstPartyData) { + url += firstPartyData.pcid ? '&iiqidtype=2&iiqpcid=' + encodeURIComponent(firstPartyData.pcid) : ''; + url += firstPartyData.pid ? '&pid=' + encodeURIComponent(firstPartyData.pid) : ''; + } + + const resp = function (callback) { + const callbacks = { + success: response => { + let respJson = tryParse(response); + if (isValidResponse(response, respJson) && isValidResponseJson(respJson)) { + // Store pid field if found in response json + if (firstPartyData && 'pcid' in firstPartyData && 'pid' in respJson) { + firstPartyData = { + 'pcid': firstPartyData.pcid, + 'pid': respJson.pid } + storeData(FIRST_PARTY_KEY, JSON.stringify(firstPartyData)); + } + callback(respJson.data); + } else { + callback(); + } + }, + error: error => { + utils.logError(MODULE_NAME + ': ID fetch encountered an error', error); + callback(); + } + }; + ajax(url, callbacks, undefined, {method: 'GET', withCredentials: true}); + }; + return {callback: resp}; + } +}; + +submodule('userId', intentIqIdSubmodule); diff --git a/modules/interactiveOffersBidAdapter.js b/modules/interactiveOffersBidAdapter.js new file mode 100644 index 00000000000..44a975db837 --- /dev/null +++ b/modules/interactiveOffersBidAdapter.js @@ -0,0 +1,162 @@ +import {registerBidder} from '../src/adapters/bidderFactory.js'; +import {BANNER} from '../src/mediaTypes.js'; +import {config} from '../src/config.js'; +import * as utils from '../src/utils.js'; + +const BIDDER_CODE = 'interactiveOffers'; +const ENDPOINT = 'https://rtb.ioadx.com/bidRequest/?partnerId=4a3bab187a74ac4862920cca864d6eff195ff5e4'; + +const DEFAULT = { + 'OpenRTBBidRequest': {}, + 'OpenRTBBidRequestSite': {}, + 'OpenRTBBidRequestSitePublisher': {}, + 'OpenRTBBidRequestSiteContent': { + language: navigator.language, + }, + 'OpenRTBBidRequestSource': {}, + 'OpenRTBBidRequestDevice': { + ua: navigator.userAgent, + language: navigator.language + }, + 'OpenRTBBidRequestUser': {}, + 'OpenRTBBidRequestImp': {}, + 'OpenRTBBidRequestImpBanner': {}, + 'PrebidBid': { + currency: 'USD', + ttl: 60, + netRevenue: false + } +}; + +export const spec = { + code: BIDDER_CODE, + supportedMediaTypes: [BANNER], + + isBidRequestValid: function(bid) { + let ret = true; + if (bid && bid.params) { + if (!utils.isNumber(bid.params.pubid)) { + utils.logWarn('pubid must be a valid numeric ID'); + ret = false; + } + if (bid.params.tmax && !utils.isNumber(bid.params.tmax)) { + utils.logWarn('tmax must be a valid numeric ID'); + ret = false; + } + } else { + utils.logWarn('invalid request'); + ret = false; + } + return ret; + }, + buildRequests: function(validBidRequests, bidderRequest) { + let payload = parseRequestPrebidjsToOpenRTB(bidderRequest); + return { + method: 'POST', + url: ENDPOINT, + data: JSON.stringify(payload), + bidderRequest: bidderRequest + }; + }, + + interpretResponse: function(response, request) { + let bidResponses = []; + if (response.body && response.body.length) { + bidResponses = parseResponseOpenRTBToPrebidjs(response.body); + } + return bidResponses; + } +}; + +function parseRequestPrebidjsToOpenRTB(prebidRequest) { + let pageURL = window.location.href; + let domain = window.location.hostname; + let secure = (window.location.protocol == 'https:' ? 1 : 0); + let openRTBRequest = JSON.parse(JSON.stringify(DEFAULT['OpenRTBBidRequest'])); + openRTBRequest.id = prebidRequest.auctionId; + openRTBRequest.ext = { + auctionstart: Date.now() + }; + + openRTBRequest.site = JSON.parse(JSON.stringify(DEFAULT['OpenRTBBidRequestSite'])); + openRTBRequest.site.id = domain; + openRTBRequest.site.name = domain; + openRTBRequest.site.domain = domain; + openRTBRequest.site.page = pageURL; + openRTBRequest.site.ref = prebidRequest.refererInfo.referer; + + openRTBRequest.site.publisher = JSON.parse(JSON.stringify(DEFAULT['OpenRTBBidRequestSitePublisher'])); + openRTBRequest.site.publisher.id = 0; + openRTBRequest.site.publisher.name = config.getConfig('publisherDomain'); + openRTBRequest.site.publisher.domain = domain; + openRTBRequest.site.publisher.domain = domain; + + openRTBRequest.site.content = JSON.parse(JSON.stringify(DEFAULT['OpenRTBBidRequestSiteContent'])); + + openRTBRequest.source = JSON.parse(JSON.stringify(DEFAULT['OpenRTBBidRequestSource'])); + openRTBRequest.source.fd = 0; + openRTBRequest.source.tid = prebidRequest.auctionId; + openRTBRequest.source.pchain = ''; + + openRTBRequest.device = JSON.parse(JSON.stringify(DEFAULT['OpenRTBBidRequestDevice'])); + + openRTBRequest.user = JSON.parse(JSON.stringify(DEFAULT['OpenRTBBidRequestUser'])); + + openRTBRequest.imp = []; + prebidRequest.bids.forEach(function(bid, impId) { + impId++; + let imp = JSON.parse(JSON.stringify(DEFAULT['OpenRTBBidRequestImp'])); + imp.id = impId; + imp.secure = secure; + imp.tagid = bid.bidId; + + openRTBRequest.site.publisher.id = openRTBRequest.site.publisher.id || bid.params.pubid; + openRTBRequest.tmax = openRTBRequest.tmax || bid.params.tmax || 0; + + Object.keys(bid.mediaTypes).forEach(function(mediaType) { + if (mediaType == 'banner') { + imp.banner = JSON.parse(JSON.stringify(DEFAULT['OpenRTBBidRequestImpBanner'])); + imp.banner.w = 0; + imp.banner.h = 0; + imp.banner.format = []; + bid.mediaTypes[mediaType].sizes.forEach(function(adSize) { + if (!imp.banner.w) { + imp.banner.w = adSize[0]; + imp.banner.h = adSize[1]; + } + imp.banner.format.push({w: adSize[0], h: adSize[1]}); + }); + } + }); + openRTBRequest.imp.push(imp); + }); + return openRTBRequest; +} +function parseResponseOpenRTBToPrebidjs(openRTBResponse) { + let prebidResponse = []; + openRTBResponse.forEach(function(response) { + response.seatbid.forEach(function(seatbid) { + seatbid.bid.forEach(function(bid) { + let prebid = JSON.parse(JSON.stringify(DEFAULT['PrebidBid'])); + prebid.requestId = bid.ext.tagid; + prebid.ad = bid.adm; + prebid.creativeId = bid.crid; + prebid.cpm = bid.price; + prebid.width = bid.w; + prebid.height = bid.h; + prebid.mediaType = 'banner'; + prebid.meta = { + advertiserDomains: bid.adomain, + advertiserId: bid.adid, + mediaType: 'banner', + primaryCatId: bid.cat[0] || '', + secondaryCatIds: bid.cat + } + prebidResponse.push(prebid); + }); + }); + }); + return prebidResponse; +} + +registerBidder(spec); diff --git a/modules/interactiveOffersBidAdapter.md b/modules/interactiveOffersBidAdapter.md index 7eb440c8216..581b2e49a68 100644 --- a/modules/interactiveOffersBidAdapter.md +++ b/modules/interactiveOffersBidAdapter.md @@ -1,14 +1,14 @@ # Overview - + ``` Module Name: interactiveOffers Bidder Adapter Module Type: Bidder Adapter -Maintainer: devteam@interactiveoffers.com +Maintainer: dev@interactiveoffers.com ``` # Description -Module that connects to interactiveOffers demand sources. Param pubId is required. +Module that connects to interactiveOffers demand sources. Param pubid is required. # Test Parameters ``` @@ -24,11 +24,11 @@ Module that connects to interactiveOffers demand sources. Param pubId is require { bidder: "interactiveOffers", params: { - pubId: '10', - tmax: 5000 + pubid: 10, + tmax: 250 } } ] } ]; -``` \ No newline at end of file +``` diff --git a/modules/invibesBidAdapter.js b/modules/invibesBidAdapter.js index 220aed47e15..d4d26b1e017 100644 --- a/modules/invibesBidAdapter.js +++ b/modules/invibesBidAdapter.js @@ -1,6 +1,6 @@ import * as utils from '../src/utils.js'; -import { registerBidder } from '../src/adapters/bidderFactory.js'; -import { getStorageManager } from '../src/storageManager.js'; +import {registerBidder} from '../src/adapters/bidderFactory.js'; +import {getStorageManager} from '../src/storageManager.js'; const CONSTANTS = { BIDDER_CODE: 'invibes', @@ -8,9 +8,10 @@ const CONSTANTS = { SYNC_ENDPOINT: 'https://k.r66net.com/GetUserSync', TIME_TO_LIVE: 300, DEFAULT_CURRENCY: 'EUR', - PREBID_VERSION: 2, + PREBID_VERSION: 5, METHOD: 'GET', - INVIBES_VENDOR_ID: 436 + INVIBES_VENDOR_ID: 436, + USERID_PROVIDERS: ['pubcid', 'pubProvidedId'] }; const storage = getStorageManager(CONSTANTS.INVIBES_VENDOR_ID); @@ -39,9 +40,7 @@ export const spec = { }, getUserSyncs: function (syncOptions) { if (syncOptions.iframeEnabled) { - handlePostMessage(); const syncUrl = buildSyncUrl(); - return { type: 'iframe', url: syncUrl @@ -55,6 +54,8 @@ registerBidder(spec); // some state info is required: cookie info, unique user visit id const topWin = getTopMostWindow(); let invibes = topWin.invibes = topWin.invibes || {}; +invibes.purposes = invibes.purposes || [false, false, false, false, false, false, false, false, false, false]; +invibes.legitimateInterests = invibes.legitimateInterests || [false, false, false, false, false, false, false, false, false, false]; let _customUserSync; function isBidRequestValid(bid) { @@ -78,7 +79,7 @@ function isBidRequestValid(bid) { function buildRequest(bidRequests, bidderRequest) { bidderRequest = bidderRequest || {}; const _placementIds = []; - let _loginId, _customEndpoint; + let _loginId, _customEndpoint, _userId; let _ivAuctionStart = bidderRequest.auctionStart || Date.now(); bidRequests.forEach(function (bidRequest) { @@ -87,44 +88,50 @@ function buildRequest(bidRequests, bidderRequest) { _loginId = _loginId || bidRequest.params.loginId; _customEndpoint = _customEndpoint || bidRequest.params.customEndpoint; _customUserSync = _customUserSync || bidRequest.params.customUserSync; + _userId = _userId || bidRequest.userId; }); - invibes.visitId = invibes.visitId || generateRandomId(); + invibes.optIn = invibes.optIn || readGdprConsent(bidderRequest.gdprConsent); - cookieDomain = detectTopmostCookieDomain(); + invibes.visitId = invibes.visitId || generateRandomId(); invibes.noCookies = invibes.noCookies || invibes.getCookie('ivNoCookie'); - invibes.optIn = invibes.optIn || invibes.getCookie('ivOptIn') || readGdprConsent(bidderRequest.gdprConsent); - - initDomainId(invibes.domainOptions); + let lid = initDomainId(invibes.domainOptions); const currentQueryStringParams = parseQueryStringParams(); - + let userIdModel = getUserIds(_userId); + let bidParamsJson = { + placementIds: _placementIds, + loginId: _loginId, + auctionStartTime: _ivAuctionStart, + bidVersion: CONSTANTS.PREBID_VERSION + }; + if (userIdModel) { + bidParamsJson.userId = userIdModel; + } let data = { location: getDocumentLocation(topWin), videoAdHtmlId: generateRandomId(), showFallback: currentQueryStringParams['advs'] === '0', ivbsCampIdsLocal: invibes.getCookie('IvbsCampIdsLocal'), - bidParamsJson: JSON.stringify({ - placementIds: _placementIds, - loginId: _loginId, - auctionStartTime: _ivAuctionStart, - bidVersion: CONSTANTS.PREBID_VERSION - }), + bidParamsJson: JSON.stringify(bidParamsJson), capCounts: getCappedCampaignsAsString(), vId: invibes.visitId, width: topWin.innerWidth, height: topWin.innerHeight, - noc: !cookieDomain, oi: invibes.optIn, - kw: keywords + kw: keywords, + purposes: invibes.purposes.toString(), + li: invibes.legitimateInterests.toString(), + + tc: invibes.gdpr_consent }; - if (invibes.dom.id) { - data.lId = invibes.dom.id; + if (lid) { + data.lId = lid; } const parametersToPassForward = 'videoaddebug,advs,bvci,bvid,istop,trybvid,trybvci'.split(','); @@ -141,7 +148,7 @@ function buildRequest(bidRequests, bidderRequest) { method: CONSTANTS.METHOD, url: _customEndpoint || CONSTANTS.BID_ENDPOINT, data: data, - options: { withCredentials: true }, + options: {withCredentials: true}, // for POST: { contentType: 'application/json', withCredentials: true } bidRequests: bidRequests }; @@ -229,9 +236,26 @@ function getDocumentLocation(topWin) { return topWin.location.href.substring(0, 300).split(/[?#]/)[0]; } +function getUserIds(bidUserId) { + let userId; + if (bidUserId) { + CONSTANTS.USERID_PROVIDERS.forEach(provider => { + if (bidUserId[provider]) { + userId = userId || {}; + userId[provider] = bidUserId[provider]; + } + }); + } + + return userId; +} + function parseQueryStringParams() { let params = {}; - try { params = JSON.parse(localStorage.ivbs); } catch (e) { } + try { + params = JSON.parse(localStorage.ivbs); + } catch (e) { + } let re = /[\\?&]([^=]+)=([^\\?&#]+)/g; let m; while ((m = re.exec(window.location.href)) != null) { @@ -258,9 +282,12 @@ function getTopMostWindow() { try { while (top !== res) { - if (res.parent.location.href.length) { res = res.parent; } + if (res.parent.location.href.length) { + res = res.parent; + } } - } catch (e) { } + } catch (e) { + } return res; } @@ -278,6 +305,10 @@ function renderCreative(bidModel) { function getCappedCampaignsAsString() { const key = 'ivvcap'; + if (!invibes.optIn || !invibes.purposes[0]) { + return ''; + } + let loadData = function () { try { return JSON.parse(storage.getDataFromLocalStorage(key)) || {}; @@ -310,24 +341,31 @@ function getCappedCampaignsAsString() { clearExpired(); let data = loadData(); return Object.keys(data) - .filter(function (k) { return data.hasOwnProperty(k); }) + .filter(function (k) { + return data.hasOwnProperty(k); + }) .sort() - .map(function (k) { return [k, data[k][0]]; }); + .map(function (k) { + return [k, data[k][0]]; + }); }; return getCappedCampaigns() - .map(function (record) { return record.join('='); }) + .map(function (record) { + return record.join('='); + }) .join(','); } -const noop = function () { }; +const noop = function () { +}; function initLogger() { if (storage.hasLocalStorage() && localStorage.InvibesDEBUG) { return window.console; } - return { info: noop, error: noop, log: noop, warn: noop, debug: noop }; + return {info: noop, error: noop, log: noop, warn: noop, debug: noop}; } function buildSyncUrl() { @@ -348,49 +386,46 @@ function buildSyncUrl() { return syncUrl; } -function handlePostMessage() { - try { - if (window.addEventListener) { - window.addEventListener('message', acceptPostMessage); - } - } catch (e) { } -} - -function acceptPostMessage(e) { - let msg = e.data || {}; - if (msg.ivbscd === 1) { - invibes.setCookie(msg.name, msg.value, msg.exdays, msg.domain); - } else if (msg.ivbscd === 2) { - invibes.dom.graduate(); - } -} - function readGdprConsent(gdprConsent) { if (gdprConsent && gdprConsent.vendorData) { + invibes.gdpr_consent = getVendorConsentData(gdprConsent.vendorData); + if (!gdprConsent.vendorData.gdprApplies || gdprConsent.vendorData.hasGlobalConsent) { + var index; + for (index = 0; index < invibes.purposes; ++index) { + invibes.purposes[index] = true; + } + + for (index = 0; index < invibes.legitimateInterests.length; ++index) { + invibes.legitimateInterests[index] = true; + } return 2; } let purposeConsents = getPurposeConsents(gdprConsent.vendorData); - if (purposeConsents == null) { return 0; } - let properties = Object.keys(purposeConsents); - let purposeConsentsCounter = getPurposeConsentsCounter(gdprConsent.vendorData); - - if (properties.length < purposeConsentsCounter) { + if (purposeConsents == null) { return 0; } + let purposesLength = getPurposeConsentsCounter(gdprConsent.vendorData); - for (let i = 0; i < purposeConsentsCounter; i++) { - if (!purposeConsents[properties[i]] || purposeConsents[properties[i]] === 'false') { return 0; } + if (!tryCopyValueToArray(purposeConsents, invibes.purposes, purposesLength)) { + return 0; } + let legitimateInterests = getLegitimateInterests(gdprConsent.vendorData); + tryCopyValueToArray(legitimateInterests, invibes.legitimateInterests, 10); + + let invibesVendorId = CONSTANTS.INVIBES_VENDOR_ID.toString(10); let vendorConsents = getVendorConsents(gdprConsent.vendorData); - if (vendorConsents == null || vendorConsents[CONSTANTS.INVIBES_VENDOR_ID.toString(10)] == null) { + let vendorHasLegitimateInterest = getVendorLegitimateInterest(gdprConsent.vendorData)[invibesVendorId] === true; + if (vendorConsents == null || vendorConsents[invibesVendorId] == null) { return 4; } - if (vendorConsents[CONSTANTS.INVIBES_VENDOR_ID.toString(10)] === false) { return 0; } + if (vendorConsents[invibesVendorId] === false && vendorHasLegitimateInterest === false) { + return 0; + } return 2; } @@ -398,6 +433,31 @@ function readGdprConsent(gdprConsent) { return 0; } +function tryCopyValueToArray(value, target, length) { + if (value instanceof Array) { + for (let i = 0; i < length && i < value.length; i++) { + target[i] = !((value[i] === false || value[i] === 'false' || value[i] == null)); + } + return true; + } + if (typeof value === 'object' && value !== null) { + let i = 0; + for (let prop in value) { + if (i === length) { + break; + } + + if (value.hasOwnProperty(prop)) { + target[i] = !((value[prop] === false || value[prop] === 'false' || value[prop] == null)); + i++; + } + } + return true; + } + + return false; +} + function getPurposeConsentsCounter(vendorData) { if (vendorData.purpose && vendorData.purpose.consents) { return 10; @@ -418,6 +478,23 @@ function getPurposeConsents(vendorData) { return null; } +function getLegitimateInterests(vendorData) { + if (vendorData.purpose && vendorData.purpose.legitimateInterests) { + return vendorData.purpose.legitimateInterests; + } + + return null; +} + +function getVendorConsentData(vendorData) { + if (vendorData.purpose && vendorData.purpose.consents) { + if (vendorData.tcString != null) { + return vendorData.tcString; + } + } + return vendorData.consentData; +}; + function getVendorConsents(vendorData) { if (vendorData.vendor && vendorData.vendor.consents) { return vendorData.vendor.consents; @@ -430,144 +507,60 @@ function getVendorConsents(vendorData) { return null; } +function getVendorLegitimateInterest(vendorData) { + if (vendorData.vendor && vendorData.vendor.legitimateInterests) { + return vendorData.vendor.legitimateInterests; + } + + return {}; +} + const ivLogger = initLogger(); /// Local domain cookie management ===================== invibes.Uid = { generate: function () { let maxRand = parseInt('zzzzzz', 36) - let mkRand = function () { return Math.floor(Math.random() * maxRand).toString(36); }; + let mkRand = function () { + return Math.floor(Math.random() * maxRand).toString(36); + }; let rand1 = mkRand(); let rand2 = mkRand(); return rand1 + rand2; } }; -let cookieDomain; invibes.getCookie = function (name) { - if (!storage.cookiesAreEnabled()) { return; } - let i, x, y; - let cookies = document.cookie.split(';'); - for (i = 0; i < cookies.length; i++) { - x = cookies[i].substr(0, cookies[i].indexOf('=')); - y = cookies[i].substr(cookies[i].indexOf('=') + 1); - x = x.replace(/^\s+|\s+$/g, ''); - if (x === name) { - return unescape(y); - } + if (!storage.cookiesAreEnabled()) { + return; } -}; - -invibes.setCookie = function (name, value, exdays, domain) { - if (!storage.cookiesAreEnabled()) { return; } - let whiteListed = name == 'ivNoCookie' || name == 'IvbsCampIdsLocal'; - if (invibes.noCookies && !whiteListed && (exdays || 0) >= 0) { return; } - if (exdays > 365) { exdays = 365; } - domain = domain || cookieDomain; - let exdate = new Date(); - let exms = exdays * 24 * 60 * 60 * 1000; - exdate.setTime(exdate.getTime() + exms); - storage.setCookie(name, value, exdate.toUTCString(), undefined, domain); -}; -let detectTopmostCookieDomain = function () { - let testCookie = invibes.Uid.generate(); - let hostParts = location.hostname.split('.'); - if (hostParts.length === 1) { - return location.hostname; - } - for (let i = hostParts.length - 1; i >= 0; i--) { - let domain = '.' + hostParts.slice(i).join('.'); - invibes.setCookie(testCookie, testCookie, 1, domain); - let val = invibes.getCookie(testCookie); - if (val === testCookie) { - invibes.setCookie(testCookie, testCookie, -1, domain); - return domain; - } + if (!invibes.optIn || !invibes.purposes[0]) { + return; } + + return storage.getCookie(name); }; let initDomainId = function (options) { - if (invibes.dom) { return; } - - options = options || {}; - let cookiePersistence = { cname: 'ivbsdid', load: function () { let str = invibes.getCookie(this.cname) || ''; try { return JSON.parse(str); - } catch (e) { } - }, - save: function (obj) { - invibes.setCookie(this.cname, JSON.stringify(obj), 365); - } - }; - - let persistence = options.persistence || cookiePersistence; - let state; - let minHC = 2; - - let validGradTime = function (state) { - if (!state.cr) { return false; } - let min = 151 * 10e9; - if (state.cr < min) { - return false; + } catch (e) { + } } - let now = new Date().getTime(); - let age = now - state.cr; - let minAge = 24 * 60 * 60 * 1000; - return age > minAge; - }; - - state = persistence.load() || { - id: invibes.Uid.generate(), - cr: new Date().getTime(), - hc: 1, }; - if (state.id.match(/\./)) { - state.id = invibes.Uid.generate(); - } + options = options || {}; - let graduate = function () { - if (!state.cr) { return; } - delete state.cr; - delete state.hc; - persistence.save(state); - setId(); - }; + var persistence = options.persistence || cookiePersistence; - let regenerateId = function () { - state.id = invibes.Uid.generate(); - persistence.save(state); - }; + let state = persistence.load(); - let setId = function () { - invibes.dom = { - get id() { - return (!state.cr && invibes.optIn > 0) ? state.id : undefined; - }, - get tempId() { - return (invibes.optIn > 0) ? state.id : undefined; - }, - graduate: graduate, - regen: regenerateId - }; - }; - - if (state.cr && !options.noVisit) { - if (state.hc < minHC) { - state.hc++; - } - if ((state.hc >= minHC && validGradTime(state)) || options.skipGraduation) { - graduate(); - } - } - persistence.save(state); - setId(); - ivLogger.info('Did=' + invibes.dom.id); + return state ? (state.id || state.tempId) : undefined; }; let keywords = (function () { @@ -638,6 +631,7 @@ let keywords = (function () { } return kw; }()); + // ===================== export function resetInvibes() { 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 new file mode 100644 index 00000000000..5b8531d7a85 --- /dev/null +++ b/modules/ironsourceBidAdapter.js @@ -0,0 +1,251 @@ +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 = 'ironsource'; +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.isOrg); + }, + 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.isOrg, + 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: params.sessionId || utils.getBidIdParameter('auctionId', bid), + is_wrapper: !!params.isWrapper, + 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/ironsourceBidAdapter.md b/modules/ironsourceBidAdapter.md new file mode 100644 index 00000000000..86756b08809 --- /dev/null +++ b/modules/ironsourceBidAdapter.md @@ -0,0 +1,51 @@ +#Overview + +Module Name: IronSource Bidder Adapter + +Module Type: Bidder Adapter + +Maintainer: prebid-digital-brands@ironsrc.com + + +# Description + +Module that connects to IronSource's demand sources. + +The IronSource adapter requires setup and approval from the IronSource. Please reach out to prebid-digital-brands@ironsrc.com to create an IronSource account. + +The adapter supports Video(instream). For the integration, IronSource 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 +| ---- | ----- | ---- | ----------- | ------- +| `isOrg` | required | String | IronSource publisher Id provided by your IronSource 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: 'ironsource', + params: { + isOrg: '56f91cd4d3e3660002000033', // Required + floorPrice: 2.00, // Optional + ifa: 'XXX-XXX', // Optional + testMode: false // Optional + } + }] + } + ]; +``` diff --git a/modules/ixBidAdapter.js b/modules/ixBidAdapter.js index 77d4220e59a..64a3237e02a 100644 --- a/modules/ixBidAdapter.js +++ b/modules/ixBidAdapter.js @@ -6,6 +6,8 @@ import isInteger from 'core-js-pure/features/number/is-integer.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; const BIDDER_CODE = 'ix'; +const ALIAS_BIDDER_CODE = 'roundel'; +const GLOBAL_VENDOR_ID = 10; const SECURE_BID_URL = 'https://htlb.casalemedia.com/cygnus'; const SUPPORTED_AD_TYPES = [BANNER, VIDEO]; const BANNER_ENDPOINT_VERSION = 7.2; @@ -14,11 +16,14 @@ const CENT_TO_DOLLAR_FACTOR = 100; const BANNER_TIME_TO_LIVE = 300; const VIDEO_TIME_TO_LIVE = 3600; // 1hr const NET_REVENUE = true; + const PRICE_TO_DOLLAR_FACTOR = { JPY: 1 }; const USER_SYNC_URL = 'https://js-sec.indexww.com/um/ixmatch.html'; +const FLOOR_SOURCE = { PBJS: 'p', IX: 'x' }; + /** * Transform valid bid request config object to banner impression object that will be sent to ad server. * @@ -33,6 +38,8 @@ function bidToBannerImp(bid) { imp.banner.h = bid.params.size[1]; imp.banner.topframe = utils.inIframe() ? 0 : 1; + _applyFloor(bid, imp, BANNER); + return imp; } @@ -44,12 +51,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 videoAdUnitAllowlist = [ + '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 +75,14 @@ function bidToVideoImp(bid) { } } + for (const adUnitProperty in videoAdUnitRef) { + if (videoAdUnitAllowlist.indexOf(adUnitProperty) !== -1 && !imp.video.hasOwnProperty(adUnitProperty)) { + imp.video[adUnitProperty] = videoAdUnitRef[adUnitProperty]; + } + } + + _applyFloor(bid, imp, VIDEO); + return imp; } @@ -78,12 +101,73 @@ function bidToImp(bid) { imp.ext.sid = `${bid.params.size[0]}x${bid.params.size[1]}`; } - if (bid.params.hasOwnProperty('bidFloor') && bid.params.hasOwnProperty('bidFloorCur')) { - imp.bidfloor = bid.params.bidFloor; - imp.bidfloorcur = bid.params.bidFloorCur; + return imp; +} + +/** + * Gets priceFloors floors and IX adapter floors, + * Validates and sets the higher one on the impression + * @param {object} bid bid object + * @param {object} imp impression object + * @param {string} mediaType the impression ad type, one of the SUPPORTED_AD_TYPES + */ +function _applyFloor(bid, imp, mediaType) { + let adapterFloor = null; + let moduleFloor = null; + + if (bid.params.bidFloor && bid.params.bidFloorCur) { + adapterFloor = { floor: bid.params.bidFloor, currency: bid.params.bidFloorCur }; } - return imp; + if (utils.isFn(bid.getFloor)) { + let _mediaType = '*'; + let _size = '*'; + + if (mediaType && utils.contains(SUPPORTED_AD_TYPES, mediaType)) { + const { w: width, h: height } = imp[mediaType]; + _mediaType = mediaType; + _size = [width, height]; + } + try { + moduleFloor = bid.getFloor({ + mediaType: _mediaType, + size: _size + }); + } catch (err) { + // continue with no module floors + utils.logWarn('priceFloors module call getFloor failed, error : ', err); + } + } + + if (adapterFloor && moduleFloor) { + if (adapterFloor.currency !== moduleFloor.currency) { + utils.logWarn('The bid floor currency mismatch between IX params and priceFloors module config'); + return; + } + + if (adapterFloor.floor > moduleFloor.floor) { + imp.bidfloor = adapterFloor.floor; + imp.bidfloorcur = adapterFloor.currency; + imp.ext.fl = FLOOR_SOURCE.IX; + } else { + imp.bidfloor = moduleFloor.floor; + imp.bidfloorcur = moduleFloor.currency; + imp.ext.fl = FLOOR_SOURCE.PBJS; + } + return; + } + + if (moduleFloor) { + imp.bidfloor = moduleFloor.floor; + imp.bidfloorcur = moduleFloor.currency; + imp.ext.fl = FLOOR_SOURCE.PBJS; + } else if (adapterFloor) { + imp.bidfloor = adapterFloor.floor; + imp.bidfloorcur = adapterFloor.currency; + imp.ext.fl = FLOOR_SOURCE.IX; + } else { + utils.logInfo('IX Bid Adapter: No floors available, no floors applied'); + } } /** @@ -198,36 +282,36 @@ 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', + 'uidapi.com': 'UID2' + }; + 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 }; } /** @@ -235,27 +319,18 @@ function addUserEids(userEids, seenIdPartners, id, source, ixlPartnerName, rtiPa * * @param {array} validBidRequests A list of valid bid request config objects. * @param {object} bidderRequest An object containing other info like gdprConsent. - * @param {array} impressions List of impression objects describing the bids. + * @param {object} impressions An object containing a list of impression objects describing the bids for each transactionId * @param {array} version Endpoint version denoting banner or video. - * @return {object} Info describing the request to the server. + * @return {array} List of objects describing the request to the server. * */ 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,27 +339,36 @@ 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); } } } } } + + // If `roundel` alias bidder, only send requests if liveramp ids exist. + if (bidderRequest && bidderRequest.bidderCode === ALIAS_BIDDER_CODE && !eidInfo.seenSources['liveramp.com']) { + return []; + } + const r = {}; // Since bidderRequestId are the same for different bid request, just use the first one. r.id = validBidRequests[0].bidderRequestId; - r.imp = impressions; - r.site = {}; r.ext = {}; 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) { @@ -322,6 +406,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 + } + } } } @@ -358,30 +448,233 @@ function buildRequest(validBidRequests, bidderRequest, impressions, version) { if (typeof otherIxConfig.timeout === 'number') { payload.t = otherIxConfig.timeout; } + + if (typeof otherIxConfig.detectMissingSizes === 'boolean') { + r.ext.ixdiag.dms = otherIxConfig.detectMissingSizes; + } else { + r.ext.ixdiag.dms = true; + } } // Use the siteId in the first bid request as the main siteId. payload.s = validBidRequests[0].params.siteId; payload.v = version; - payload.r = JSON.stringify(r); payload.ac = 'j'; payload.sd = 1; if (version === VIDEO_ENDPOINT_VERSION) { payload.nf = 1; } - return { + const requests = []; + + const request = { method: 'GET', url: baseUrl, data: payload }; + + 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; + const MAX_REQ_LIMIT = 4; + let sn = 0; + let msi = 0; + let msd = 0; + r.ext.ixdiag.msd = 0; + r.ext.ixdiag.msi = 0; + r.imp = []; + let i = 0; + const transactionIds = Object.keys(impressions); + let currMissingImps = []; + + while (i < transactionIds.length && requests.length < MAX_REQ_LIMIT) { + if (impressions[transactionIds[i]].hasOwnProperty('missingCount')) { + msd = impressions[transactionIds[i]].missingCount; + } + + if (BASE_REQ_SIZE < MAX_REQ_SIZE) { + trimImpressions(impressions[transactionIds[i]], MAX_REQ_SIZE - BASE_REQ_SIZE); + } else { + utils.logError('ix bidder: Base request size has exceeded maximum request size.'); + } + + if (impressions[transactionIds[i]].hasOwnProperty('missingImps')) { + msi = impressions[transactionIds[i]].missingImps.length; + } + + let currImpsSize = new Blob([encodeURIComponent(JSON.stringify(impressions[transactionIds[i]]))]).size; + currReqSize += currImpsSize; + if (currReqSize < MAX_REQ_SIZE) { + // pushing ix configured sizes first + r.imp.push(...impressions[transactionIds[i]].ixImps); + // update msd msi + r.ext.ixdiag.msd += msd; + r.ext.ixdiag.msi += msi; + + if (impressions[transactionIds[i]].hasOwnProperty('missingImps')) { + currMissingImps.push(...impressions[transactionIds[i]].missingImps); + } + + i++; + } else { + // pushing missing sizes after configured ones + const clonedPayload = utils.deepClone(payload); + + r.imp.push(...currMissingImps); + r.ext.ixdiag.sn = sn; + clonedPayload.sn = sn; + sn++; + clonedPayload.r = JSON.stringify(r); + + requests.push({ + method: 'GET', + url: baseUrl, + data: clonedPayload + }); + currMissingImps = []; + currReqSize = BASE_REQ_SIZE; + r.imp = []; + msd = 0; + msi = 0; + r.ext.ixdiag.msd = 0; + r.ext.ixdiag.msi = 0; + } + } + + if (currReqSize > BASE_REQ_SIZE && currReqSize < MAX_REQ_SIZE && requests.length < MAX_REQ_LIMIT) { + const clonedPayload = utils.deepClone(payload); + r.imp.push(...currMissingImps); + + if (requests.length > 0) { + r.ext.ixdiag.sn = sn; + clonedPayload.sn = sn; + } + clonedPayload.r = JSON.stringify(r); + + requests.push({ + method: 'GET', + url: baseUrl, + data: clonedPayload + }); + } + + return requests; +} + +/** + * Return an object of user IDs stored by Prebid User ID module + * + * @returns {array} ID providers that are present in userIds + */ +function _getUserIds(bidRequest) { + const userIds = bidRequest.userId || {}; + + const PROVIDERS = [ + 'britepoolid', + 'id5id', + 'lipbid', + 'haloId', + 'criteoId', + 'lotamePanoramaId', + 'merkleId', + 'parrableId', + 'connectid', + 'sharedid', + 'tapadId', + 'quantcastId', + 'pubcid' + ] + + return PROVIDERS.filter(provider => utils.deepAccess(userIds, provider)) +} + +/** + * 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$', + userIds: _getUserIds(validBidRequests[0]) + }; + + // 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 + * + */ +function trimImpressions(impressions, maxSize) { + let currSize = new Blob([encodeURIComponent(JSON.stringify(impressions))]).size; + if (currSize < maxSize) { + return; + } + + while (currSize > maxSize) { + if (impressions.hasOwnProperty('missingImps') && impressions.missingImps.length > 0) { + impressions.missingImps.pop(); + } else if (impressions.hasOwnProperty('ixImps') && impressions.ixImps.length > 0) { + impressions.ixImps.pop(); + } + currSize = new Blob([encodeURIComponent(JSON.stringify(impressions))]).size; + } +} /** * * @param {array} bannerSizeList list of banner sizes * @param {array} bannerSize the size to be removed - * @return {boolean} true if succesfully removed, false if not found + * @return {boolean} true if successfully removed, false if not found */ function removeFromSizes(bannerSizeList, bannerSize) { @@ -416,7 +709,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; @@ -425,23 +719,31 @@ function updateMissingSizes(validBidRequest, missingBannerSizes, imp) { } /** - * + * @param {object} bid ValidBidRequest object, used to adjust floor * @param {object} imp Impression object to be modified * @param {array} newSize The new size to be applied * @return {object} newImp Updated impression object */ -function createMissingBannerImp(imp, newSize) { +function createMissingBannerImp(bid, imp, newSize) { const newImp = utils.deepClone(imp); newImp.ext.sid = `${newSize[0]}x${newSize[1]}`; newImp.banner.w = newSize[0]; newImp.banner.h = newSize[1]; + + _applyFloor(bid, newImp, BANNER); + return newImp; } export const spec = { code: BIDDER_CODE, - gvlid: 10, + gvlid: GLOBAL_VENDOR_ID, + aliases: [{ + code: ALIAS_BIDDER_CODE, + gvlid: GLOBAL_VENDOR_ID, + skipPbsAliasing: false + }], supportedMediaTypes: SUPPORTED_AD_TYPES, /** @@ -451,32 +753,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.'); @@ -496,47 +823,78 @@ export const spec = { */ buildRequests: function (validBidRequests, bidderRequest) { let reqs = []; - let bannerImps = []; - let videoImps = []; + let bannerImps = {}; + let videoImps = {}; let validBidRequest = null; // To capture the missing sizes i.e not configured for ix let missingBannerSizes = {}; + const DEFAULT_IX_CONFIG = { + detectMissingSizes: true, + }; + + const ixConfig = { ...DEFAULT_IX_CONFIG, ...config.getConfig('ix') }; + for (let i = 0; i < validBidRequests.length; i++) { validBidRequest = validBidRequests[i]; if (validBidRequest.mediaType === VIDEO || utils.deepAccess(validBidRequest, 'mediaTypes.video')) { if (validBidRequest.mediaType === VIDEO || includesSize(validBidRequest.mediaTypes.video.playerSize, validBidRequest.params.size)) { - videoImps.push(bidToVideoImp(validBidRequest)); - } else { - utils.logError('Bid size is not included in video playerSize') + if (!videoImps.hasOwnProperty(validBidRequest.transactionId)) { + videoImps[validBidRequest.transactionId] = {}; + } + if (!videoImps[validBidRequest.transactionId].hasOwnProperty('ixImps')) { + videoImps[validBidRequest.transactionId].ixImps = []; + } + videoImps[validBidRequest.transactionId].ixImps.push(bidToVideoImp(validBidRequest)); } } - if (validBidRequest.mediaType === BANNER || utils.deepAccess(validBidRequest, 'mediaTypes.banner') || - (!validBidRequest.mediaType && !validBidRequest.mediaTypes)) { + 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); - bannerImps.push(imp); - updateMissingSizes(validBidRequest, missingBannerSizes, imp); + + if (!bannerImps.hasOwnProperty(validBidRequest.transactionId)) { + bannerImps[validBidRequest.transactionId] = {}; + } + if (!bannerImps[validBidRequest.transactionId].hasOwnProperty('ixImps')) { + bannerImps[validBidRequest.transactionId].ixImps = [] + } + bannerImps[validBidRequest.transactionId].ixImps.push(imp); + if (ixConfig.hasOwnProperty('detectMissingSizes') && ixConfig.detectMissingSizes) { + updateMissingSizes(validBidRequest, missingBannerSizes, imp); + } } } - // Finding the missing banner sizes ,and making impressions for them - for (var transactionID in missingBannerSizes) { - if (missingBannerSizes.hasOwnProperty(transactionID)) { - let missingSizes = missingBannerSizes[transactionID].missingSizes; + + // Finding the missing banner sizes, and making impressions for them + for (var transactionId in missingBannerSizes) { + if (missingBannerSizes.hasOwnProperty(transactionId)) { + let missingSizes = missingBannerSizes[transactionId].missingSizes; + + if (!bannerImps.hasOwnProperty(transactionId)) { + bannerImps[transactionId] = {}; + } + if (!bannerImps[transactionId].hasOwnProperty('missingImps')) { + bannerImps[transactionId].missingImps = []; + bannerImps[transactionId].missingCount = 0; + } + + let origImp = missingBannerSizes[transactionId].impression; for (let i = 0; i < missingSizes.length; i++) { - let origImp = missingBannerSizes[transactionID].impression; - let newImp = createMissingBannerImp(origImp, missingSizes[i]); - bannerImps.push(newImp); + let newImp = createMissingBannerImp(validBidRequest, origImp, missingSizes[i]); + bannerImps[transactionId].missingImps.push(newImp); + bannerImps[transactionId].missingCount++; } } } - if (bannerImps.length > 0) { - reqs.push(buildRequest(validBidRequests, bidderRequest, bannerImps, BANNER_ENDPOINT_VERSION)); + if (Object.keys(bannerImps).length > 0) { + reqs.push(...buildRequest(validBidRequests, bidderRequest, bannerImps, BANNER_ENDPOINT_VERSION)); } - if (videoImps.length > 0) { - reqs.push(buildRequest(validBidRequests, bidderRequest, videoImps, VIDEO_ENDPOINT_VERSION)); + if (Object.keys(videoImps).length > 0) { + reqs.push(...buildRequest(validBidRequests, bidderRequest, videoImps, VIDEO_ENDPOINT_VERSION)); } return reqs; @@ -584,7 +942,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 b5cb0d9d2c1..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. @@ -288,6 +288,26 @@ pbjs.setConfig({ } }); ``` +#### The **detectMissingSizes** feature +By default, the IX bidding adapter bids on all banner sizes available in the ad unit when configured to at least one banner size. If you want the IX bidding adapter to only bid on the banner size it’s configured to, switch off this feature using `detectMissingSizes`. +``` +pbjs.setConfig({ + ix: { + detectMissingSizes: false + } + }); +``` +OR +``` +pbjs.setBidderConfig({ + bidders: ["ix"], + config: { + ix: { + detectMissingSizes: false + } + } + }); +``` ### 2. Include `ixBidAdapter` in your build process diff --git a/modules/jixieBidAdapter.js b/modules/jixieBidAdapter.js new file mode 100644 index 00000000000..7c6e0027482 --- /dev/null +++ b/modules/jixieBidAdapter.js @@ -0,0 +1,254 @@ +import * as utils from '../src/utils.js'; +import { config } from '../src/config.js'; +import { registerBidder } from '../src/adapters/bidderFactory.js'; +import { getStorageManager } from '../src/storageManager.js'; +import { BANNER, VIDEO } from '../src/mediaTypes.js'; +import { ajax } from '../src/ajax.js'; +import { getRefererInfo } from '../src/refererDetection.js'; +import { Renderer } from '../src/Renderer.js'; +export const storage = getStorageManager(); + +const BIDDER_CODE = 'jixie'; +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; + +/** + * Own miscellaneous support functions: + */ + +function setIds_(clientId, sessionId) { + let dd = null; + try { + dd = window.location.hostname.match(/[^.]*\.[^.]{2,3}(?:\.[^.]{2,3})?$/mg); + } catch (err1) {} + try { + let expC = (new Date(new Date().setFullYear(new Date().getFullYear() + 1))).toUTCString(); + let expS = (new Date(new Date().setMinutes(new Date().getMinutes() + sidTTLMins_))).toUTCString(); + + storage.setCookie('_jx', clientId, expC, 'None', null); + storage.setCookie('_jx', clientId, expC, 'None', dd); + + storage.setCookie('_jxs', sessionId, expS, 'None', null); + storage.setCookie('_jxs', sessionId, expS, 'None', dd); + + storage.setDataInLocalStorage('_jx', clientId); + storage.setDataInLocalStorage('_jxs', sessionId); + } catch (error) {} +} + +function fetchIds_() { + let ret = { + client_id_c: '', + client_id_ls: '', + session_id_c: '', + session_id_ls: '' + }; + try { + let tmp = storage.getCookie('_jx'); + if (tmp) ret.client_id_c = tmp; + tmp = storage.getCookie('_jxs'); + if (tmp) ret.session_id_c = tmp; + + tmp = storage.getDataFromLocalStorage('_jx'); + if (tmp) ret.client_id_ls = tmp; + tmp = storage.getDataFromLocalStorage('_jxs'); + if (tmp) ret.session_id_ls = tmp; + } catch (error) {} + return ret; +} + +function getDevice_() { + return ((/(ios|ipod|ipad|iphone|android|blackberry|iemobile|opera mini|webos)/i).test(navigator.userAgent) + ? 'mobile' : 'desktop'); +} + +function pingTracking_(endpointOverride, qpobj) { + internal.ajax((endpointOverride || EVENTS_URL), null, qpobj, { + withCredentials: true, + method: 'GET', + crossOrigin: true + }); +} + +function jxOutstreamRender_(bidAd) { + bidAd.renderer.push(() => { + window.JixieOutstreamVideo.init({ + sizes: [bidAd.width, bidAd.height], + width: bidAd.width, + height: bidAd.height, + targetId: bidAd.adUnitCode, + adResponse: bidAd.adResponse + }); + }); +} + +function createRenderer_(bidAd, scriptUrl, createFcn) { + const renderer = Renderer.install({ + id: bidAd.adUnitCode, + url: scriptUrl, + loaded: false, + config: {'player_height': bidAd.height, 'player_width': bidAd.width}, + adUnitCode: bidAd.adUnitCode + }); + try { + renderer.setRender(createFcn); + } catch (err) { + utils.logWarn('Prebid Error calling setRender on renderer', err); + } + return renderer; +} + +function getMiscDims_() { + let ret = { + pageurl: '', + domain: '', + device: 'unknown' + } + try { + let refererInfo_ = getRefererInfo(); + let url_ = ((refererInfo_ && refererInfo_.referer) ? refererInfo_.referer : window.location.href); + ret.pageurl = url_; + ret.domain = utils.parseUrl(url_).host; + ret.device = getDevice_(); + } catch (error) {} + return ret; +} + +// easier for replacement in the unit test +export const internal = { + getDevice: getDevice_, + getRefererInfo: getRefererInfo, + ajax: ajax, + getMiscDims: getMiscDims_ +}; + +export const spec = { + code: BIDDER_CODE, + EVENTS_URL: EVENTS_URL, + supportedMediaTypes: [BANNER, VIDEO], + isBidRequestValid: function(bid) { + if (bid.bidder !== BIDDER_CODE || typeof bid.params === 'undefined') { + return false; + } + if (typeof bid.params.unit === 'undefined') { + return false; + } + return true; + }, + buildRequests: function(validBidRequests, bidderRequest) { + const currencyObj = config.getConfig('currency'); + const currency = (currencyObj && currencyObj.adServerCurrency) || 'USD'; + + let bids = []; + validBidRequests.forEach(function(one) { + bids.push({ + bidId: one.bidId, + adUnitCode: one.adUnitCode, + mediaTypes: (one.mediaTypes === 'undefined' ? {} : one.mediaTypes), + sizes: (one.sizes === 'undefined' ? [] : one.sizes), + params: one.params, + }); + }); + + let jixieCfgBlob = config.getConfig('jixie'); + if (!jixieCfgBlob) { + jixieCfgBlob = {}; + } + + let ids = fetchIds_(); + let miscDims = internal.getMiscDims(); + let transformedParams = Object.assign({}, { + auctionid: bidderRequest.auctionId, + timeout: bidderRequest.timeout, + currency: currency, + timestamp: (new Date()).getTime(), + device: miscDims.device, + domain: miscDims.domain, + pageurl: miscDims.pageurl, + bids: bids, + cfg: jixieCfgBlob + }, ids); + return Object.assign({}, { + method: 'POST', + url: REQUESTS_URL, + data: JSON.stringify(transformedParams), + currency: currency + }); + }, + + onTimeout: function(timeoutData) { + let jxCfgBlob = config.getConfig('jixie'); + if (jxCfgBlob && jxCfgBlob.onTimeout == 'off') { + return; + } + let url = null;// default + if (jxCfgBlob && jxCfgBlob.onTimeoutUrl && typeof jxCfgBlob.onTimeoutUrl == 'string') { + url = jxCfgBlob.onTimeoutUrl; + } + let miscDims = internal.getMiscDims(); + pingTracking_(url, // no overriding ping URL . just use default + { + action: 'hbtimeout', + device: miscDims.device, + pageurl: encodeURIComponent(miscDims.pageurl), + domain: encodeURIComponent(miscDims.domain), + auctionid: utils.deepAccess(timeoutData, '0.auctionId'), + timeout: utils.deepAccess(timeoutData, '0.timeout'), + count: timeoutData.length + }); + }, + + onBidWon: function(bid) { + if (bid.notrack) { + return; + } + if (bid.trackingUrl) { + pingTracking_(bid.trackingUrl, {}); + } else { + let miscDims = internal.getMiscDims(); + pingTracking_((bid.trackingUrlBase ? bid.trackingUrlBase : null), { + action: 'hbbidwon', + device: miscDims.device, + pageurl: encodeURIComponent(miscDims.pageurl), + domain: encodeURIComponent(miscDims.domain), + cid: bid.cid, + cpid: bid.cpid, + jxbidid: bid.jxBidId, + auctionid: bid.auctionId, + cpm: bid.cpm, + requestid: bid.requestId + }); + } + }, + + interpretResponse: function(response, bidRequest) { + if (response && response.body && utils.isArray(response.body.bids)) { + const bidResponses = []; + response.body.bids.forEach(function(oneBid) { + let bnd = {}; + + Object.assign(bnd, oneBid); + if (oneBid.osplayer) { + bnd.adResponse = { + content: oneBid.vastXml, + parameters: oneBid.osparams, + height: oneBid.height, + width: oneBid.width + }; + let rendererScript = (oneBid.osparams.script ? oneBid.osparams.script : JX_OUTSTREAM_RENDERER_URL); + bnd.renderer = createRenderer_(oneBid, rendererScript, jxOutstreamRender_); + } + bidResponses.push(bnd); + }); + if (response.body.setids) { + setIds_(response.body.setids.client_id, + response.body.setids.session_id); + } + return bidResponses; + } else { return []; } + } +} + +registerBidder(spec); diff --git a/modules/jixieBidAdapter.md b/modules/jixieBidAdapter.md new file mode 100644 index 00000000000..d9c1f19541d --- /dev/null +++ b/modules/jixieBidAdapter.md @@ -0,0 +1,73 @@ +# Overview + +Module Name: Jixie Bidder Adapter +Module Type: Bidder Adapter +Maintainer: contact@jixie.io + +# Description + +Module that connects to Jixie demand source to fetch bids. + +# Test Parameters +``` + var adUnits = [ + { + code: 'demoslot1-div', + mediaTypes: { + banner: { + sizes: [ + [300, 250] + ] + } + }, + bids: [ + { + bidder: 'jixie', + params: { + unit: "prebidsampleunit" + } + } + ] + }, + { + code: 'demoslot2-div', + sizes: [640, 360], + mediaTypes: { + video: { + playerSize: [ + [640, 360] + ], + context: "outstream" + } + }, + bids: [ + { + bidder: 'jixie', + params: { + unit: "prebidsampleunit" + } + } + ] + }, + { + code: 'demoslot3-div', + sizes: [640, 360], + mediaTypes: { + video: { + playerSize: [ + [640, 360] + ], + context: "instream" + } + }, + bids: [ + { + bidder: 'jixie', + params: { + unit: "prebidsampleunit" + } + } + ] + } + ]; +``` \ No newline at end of file diff --git a/modules/jwplayerRtdProvider.js b/modules/jwplayerRtdProvider.js new file mode 100644 index 00000000000..8332e720ae7 --- /dev/null +++ b/modules/jwplayerRtdProvider.js @@ -0,0 +1,282 @@ +/** + * This module adds the jwplayer provider to the Real Time Data module (rtdModule) + * The {@link module:modules/realTimeData} module is required + * The module will allow Ad Bidders to obtain JW Player's Video Ad Targeting information + * The module will fetch segments for the media ids present in the prebid config when the module loads. If any bid + * requests are made while the segments are being fetched, they will be blocked until all requests complete, or the + * timeout expires. + * @module modules/jwplayerRtdProvider + * @requires module:modules/realTimeData + */ + +import { submodule } from '../src/hook.js'; +import { config } from '../src/config.js'; +import { ajaxBuilder } from '../src/ajax.js'; +import { logError } from '../src/utils.js'; +import find from 'core-js-pure/features/array/find.js'; +import { getGlobal } from '../src/prebidGlobal.js'; + +const SUBMODULE_NAME = 'jwplayer'; +const segCache = {}; +const pendingRequests = {}; +let activeRequestCount = 0; +let resumeBidRequest; + +/** @type {RtdSubmodule} */ +export const jwplayerSubmodule = { + /** + * used to link submodule with realTimeData + * @type {string} + */ + name: SUBMODULE_NAME, + /** + * add targeting data to bids and signal completion to realTimeData module + * @function + * @param {Obj} bidReqConfig + * @param {function} onDone + */ + getBidRequestData: enrichBidRequest, + init +}; + +config.getConfig('realTimeData', ({realTimeData}) => { + const providers = realTimeData.dataProviders; + const jwplayerProvider = providers && find(providers, pr => pr.name && pr.name.toLowerCase() === SUBMODULE_NAME); + const params = jwplayerProvider && jwplayerProvider.params; + if (!params) { + return; + } + fetchTargetingInformation(params); +}); + +submodule('realTimeData', jwplayerSubmodule); + +function init(provider, userConsent) { + return true; +} + +export function fetchTargetingInformation(jwTargeting) { + const mediaIDs = jwTargeting.mediaIDs; + if (!mediaIDs) { + return; + } + mediaIDs.forEach(mediaID => { + fetchTargetingForMediaId(mediaID); + }); +} + +export function fetchTargetingForMediaId(mediaId) { + const ajax = ajaxBuilder(); + // TODO: Avoid checking undefined vs null by setting a callback to pendingRequests. + pendingRequests[mediaId] = null; + ajax(`https://cdn.jwplayer.com/v2/media/${mediaId}`, { + success: function (response) { + const segment = parseSegment(response); + cacheSegments(segment, mediaId); + onRequestCompleted(mediaId, !!segment); + }, + error: function () { + logError('failed to retrieve targeting information'); + onRequestCompleted(mediaId, false); + } + }); +} + +function parseSegment(response) { + let segment; + try { + const data = JSON.parse(response); + if (!data) { + throw ('Empty response'); + } + + const playlist = data.playlist; + if (!playlist || !playlist.length) { + throw ('Empty playlist'); + } + + segment = playlist[0].jwpseg; + } catch (err) { + logError(err); + } + return segment; +} + +function cacheSegments(jwpseg, mediaId) { + if (jwpseg && mediaId) { + segCache[mediaId] = jwpseg; + } +} + +function onRequestCompleted(mediaID, success) { + const callback = pendingRequests[mediaID]; + if (callback) { + callback(success ? getVatFromCache(mediaID) : { mediaID }); + activeRequestCount--; + } + delete pendingRequests[mediaID]; + + if (activeRequestCount > 0) { + return; + } + + if (resumeBidRequest) { + resumeBidRequest(); + resumeBidRequest = null; + } +} + +function enrichBidRequest(bidReqConfig, onDone) { + activeRequestCount = 0; + const adUnits = bidReqConfig.adUnits || getGlobal().adUnits; + enrichAdUnits(adUnits); + if (activeRequestCount <= 0) { + onDone(); + } else { + resumeBidRequest = onDone; + } +} + +/** + * get targeting data and write to bids + * @function + * @param {adUnit[]} adUnits + * @param {function} onDone + */ +export function enrichAdUnits(adUnits) { + const fpdFallback = config.getConfig('ortb2.site.ext.data.jwTargeting'); + adUnits.forEach(adUnit => { + const jwTargeting = extractPublisherParams(adUnit, fpdFallback); + if (!jwTargeting || !Object.keys(jwTargeting).length) { + return; + } + + const onVatResponse = function (vat) { + if (!vat) { + return; + } + const targeting = formatTargetingResponse(vat); + addTargetingToBids(adUnit.bids, targeting); + }; + 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.ortb2Imp.ext.data.jwTargeting; + } catch (e) {} + + 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); + return; + } + + const vat = getVatFromCache(mediaID) || getVatFromPlayer(playerID, mediaID) || { mediaID }; + onCompletion(vat); +} + +function loadVatForPendingRequest(playerID, mediaID, callback) { + const vat = getVatFromPlayer(playerID, mediaID); + if (vat) { + callback(vat); + } else { + activeRequestCount++; + pendingRequests[mediaID] = callback; + } +} + +export function getVatFromCache(mediaID) { + const segments = segCache[mediaID]; + + if (!segments) { + return null; + } + + return { + segments, + mediaID + }; +} + +export function getVatFromPlayer(playerID, mediaID) { + const player = getPlayer(playerID); + if (!player) { + return null; + } + + const item = mediaID ? find(player.getPlaylist(), item => item.mediaid === mediaID) : player.getPlaylistItem(); + if (!item) { + return null; + } + + mediaID = mediaID || item.mediaid; + const segments = item.jwpseg; + cacheSegments(segments, mediaID) + + return { + segments, + mediaID + }; +} + +export function formatTargetingResponse(vat) { + const { segments, mediaID } = vat; + const targeting = {}; + if (segments && segments.length) { + targeting.segments = segments; + } + + if (mediaID) { + const id = 'jw_' + mediaID; + targeting.content = { + id + } + } + return targeting; +} + +function addTargetingToBids(bids, targeting) { + if (!bids || !targeting) { + return; + } + + bids.forEach(bid => addTargetingToBid(bid, targeting)); +} + +export function addTargetingToBid(bid, targeting) { + const rtd = bid.rtd || {}; + const jwRtd = {}; + jwRtd[SUBMODULE_NAME] = Object.assign({}, rtd[SUBMODULE_NAME], { targeting }); + bid.rtd = Object.assign({}, rtd, jwRtd); +} + +function getPlayer(playerID) { + const jwplayer = window.jwplayer; + if (!jwplayer) { + logError('jwplayer.js was not found on page'); + return; + } + + const player = jwplayer(playerID); + if (!player || !player.getPlaylist) { + logError('player ID did not match any players'); + return; + } + return player; +} diff --git a/modules/jwplayerRtdProvider.md b/modules/jwplayerRtdProvider.md new file mode 100644 index 00000000000..0723e8cbb6c --- /dev/null +++ b/modules/jwplayerRtdProvider.md @@ -0,0 +1,123 @@ +The purpose of this Real Time Data Provider is to allow publishers to target against their JW Player media without +having to integrate with the Player Bidding product. This prebid module makes JW Player's video ad targeting information accessible +to Bid Adapters. + +#Usage for Publishers: + +Compile the JW Player RTD Provider into your Prebid build: + +`gulp build --modules=jwplayerRtdProvider` + +Publishers must register JW Player as a real time data provider by setting up a Prebid Config conformant to the +following structure: + +```javascript +const jwplayerDataProvider = { + name: "jwplayer" +}; + +pbjs.setConfig({ + ..., + realTimeData: { + dataProviders: [ + jwplayerDataProvider + ] + } +}); +``` +Lastly, include the content's media ID and/or the player's ID in the matching AdUnit's `ortb2Imp.ext.data`: + +```javascript +const adUnit = { + code: '/19968336/prebid_native_example_1', + ... + ortb2Imp: { + ext: { + data: { + jwTargeting: { + // Note: the following Ids are placeholders and should be replaced with your Ids. + playerID: 'abcd', + mediaID: '1234' + } + } + } + } +}; + +pbjs.que.push(function() { + pbjs.addAdUnits([adUnit]); + pbjs.requestBids({ + ... + }); +}); +``` + +**Note**: You may also include `jwTargeting` information in the prebid config's `ortb2.site.ext.data`. Information provided in the adUnit will always supersede, and information in the config will be used as a fallback. + +##Prefetching +In order to prefetch targeting information for certain media, include the media IDs in the `jwplayerDataProvider` var and set `waitForIt` to `true`: + +```javascript +const jwplayerDataProvider = { + name: "jwplayer", + waitForIt: true, + params: { + mediaIDs: ['abc', 'def', 'ghi', 'jkl'] + } +}; +``` + +You must also set a value to `auctionDelay` in the config's `realTimeData` object + +```javascript +realTimeData = { + auctionDelay: 100, + ... +}; +``` + +#Usage for Bid Adapters: + +Implement the `buildRequests` function. When it is called, the `bidRequests` param will be an array of bids. +Each bid for which targeting information was found will conform to the following object structure: + +```javascript +{ + adUnitCode: 'xyz', + bidId: 'abc', + ..., + rtd: { + jwplayer: { + targeting: { + segments: ['123', '456'], + content: { + id: 'jw_abc123' + } + } + } + } +} +``` + +where: +- `segments` is an array of jwpseg targeting segments, of type string. +- `content` is an object containing metadata for the media. It may contain the following information: + - `id` is a unique identifier for the specific media asset. + +**Example:** + +To view an example: + +- in your cli run: + +`gulp serve --modules=jwplayerRtdProvider` + +- in your browser, navigate to: + +`http://localhost:9999/integrationExamples/gpt/jwplayerRtdProvider_example.html` + +**Note:** the mediaIds in the example are placeholder values; replace them with your existing IDs. + +#Maintainer info + +Maintained by JW Player. For any questions, comments or feedback please contact Karim Mourra, karim@jwplayer.com diff --git a/modules/kargoBidAdapter.js b/modules/kargoBidAdapter.js index 31c35f4afe3..610f4558139 100644 --- a/modules/kargoBidAdapter.js +++ b/modules/kargoBidAdapter.js @@ -3,17 +3,19 @@ import {config} from '../src/config.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; import { getStorageManager } from '../src/storageManager.js'; -const storage = getStorageManager(); const BIDDER_CODE = 'kargo'; const HOST = 'https://krk.kargo.com'; -const SYNC = 'https://crb.kargo.com/api/v1/initsyncrnd/{UUID}?seed={SEED}&idx={INDEX}'; +const SYNC = 'https://crb.kargo.com/api/v1/initsyncrnd/{UUID}?seed={SEED}&idx={INDEX}&gdpr={GDPR}&gdpr_consent={GDPR_CONSENT}&us_privacy={US_PRIVACY}'; const SYNC_COUNT = 5; +const GVLID = 972; +const storage = getStorageManager(GVLID, BIDDER_CODE); let sessionId, lastPageUrl, requestCounter; export const spec = { + gvlid: GVLID, code: BIDDER_CODE, isBidRequestValid: function(bid) { if (!bid || !bid.params) { @@ -48,7 +50,7 @@ export const spec = { bidIDs: bidIds, bidSizes: bidSizes, prebidRawBidRequests: validBidRequests - }, spec._getAllMetadata(tdid, bidderRequest.uspConsent)); + }, spec._getAllMetadata(tdid, bidderRequest.uspConsent, bidderRequest.gdprConsent)); const encodedParams = encodeURIComponent(JSON.stringify(transformedParams)); return Object.assign({}, bidderRequest, { method: 'GET', @@ -65,7 +67,8 @@ export const spec = { let meta; if (adUnit.metadata && adUnit.metadata.landingPageDomain) { meta = { - clickUrl: adUnit.metadata.landingPageDomain + clickUrl: adUnit.metadata.landingPageDomain, + advertiserDomains: [adUnit.metadata.landingPageDomain] }; } bidResponses.push({ @@ -84,15 +87,25 @@ export const spec = { } return bidResponses; }, - getUserSyncs: function(syncOptions) { + getUserSyncs: function(syncOptions, responses, gdprConsent, usPrivacy) { const syncs = []; const seed = spec._generateRandomUuid(); const clientId = spec._getClientId(); + var gdpr = (gdprConsent && gdprConsent.gdprApplies) ? 1 : 0; + var gdprConsentString = (gdprConsent && gdprConsent.consentString) ? gdprConsent.consentString : ''; + // don't sync if opted out via usPrivacy + if (typeof usPrivacy == 'string' && usPrivacy.length == 4 && usPrivacy[0] == 1 && usPrivacy[2] == 'Y') { + return syncs; + } if (syncOptions.iframeEnabled && seed && clientId) { for (let i = 0; i < SYNC_COUNT; i++) { syncs.push({ type: 'iframe', - url: SYNC.replace('{UUID}', clientId).replace('{SEED}', seed).replace('{INDEX}', i) + url: SYNC.replace('{UUID}', clientId).replace('{SEED}', seed) + .replace('{INDEX}', i) + .replace('{GDPR}', gdpr) + .replace('{GDPR_CONSENT}', gdprConsentString) + .replace('{US_PRIVACY}', usPrivacy || '') }); } } @@ -182,7 +195,7 @@ export const spec = { } }, - _getUserIds(tdid, usp) { + _getUserIds(tdid, usp, gdpr) { const crb = spec._getCrb(); const userIds = { kargoID: crb.userId, @@ -191,6 +204,16 @@ export const spec = { optOut: crb.optOut, usp: usp }; + + try { + if (gdpr) { + userIds['gdpr'] = { + consent: gdpr.consentString || '', + applies: !!gdpr.gdprApplies, + } + } + } catch (e) { + } if (tdid) { userIds.tdID = tdid; } @@ -202,9 +225,9 @@ export const spec = { return crb.clientId; }, - _getAllMetadata(tdid, usp) { + _getAllMetadata(tdid, usp, gdpr) { return { - userIDs: spec._getUserIds(tdid, usp), + userIDs: spec._getUserIds(tdid, usp, gdpr), krux: spec._getKrux(), pageURL: window.location.href, rawCRB: spec._readCookie('krg_crb'), diff --git a/modules/koblerBidAdapter.js b/modules/koblerBidAdapter.js new file mode 100644 index 00000000000..cc5b374af95 --- /dev/null +++ b/modules/koblerBidAdapter.js @@ -0,0 +1,231 @@ +import * as utils from '../src/utils.js'; +import {config} from '../src/config.js'; +import {registerBidder} from '../src/adapters/bidderFactory.js'; +import {BANNER} from '../src/mediaTypes.js'; +import {getRefererInfo} from '../src/refererDetection.js'; + +const BIDDER_CODE = 'kobler'; +const BIDDER_ENDPOINT = 'https://bid.essrtb.com/bid/prebid_rtb_call'; +const TIMEOUT_NOTIFICATION_ENDPOINT = 'https://bid.essrtb.com/notify/prebid_timeout'; +const SUPPORTED_CURRENCY = 'USD'; +const DEFAULT_TIMEOUT = 1000; +const TIME_TO_LIVE_IN_SECONDS = 10 * 60; + +export const isBidRequestValid = function (bid) { + return !!(bid && bid.bidId && bid.params && bid.params.placementId); +}; + +export const buildRequests = function (validBidRequests, bidderRequest) { + return { + method: 'POST', + url: BIDDER_ENDPOINT, + data: buildOpenRtbBidRequestPayload(validBidRequests, bidderRequest), + options: { + contentType: 'application/json' + } + }; +}; + +export const interpretResponse = function (serverResponse) { + const res = serverResponse.body; + const bids = [] + if (res) { + res.seatbid.forEach(sb => { + sb.bid.forEach(b => { + bids.push({ + requestId: b.impid, + cpm: b.price, + currency: res.cur, + width: b.w, + height: b.h, + creativeId: b.crid, + dealId: b.dealid, + netRevenue: true, + ttl: TIME_TO_LIVE_IN_SECONDS, + ad: b.adm, + nurl: b.nurl, + meta: { + advertiserDomains: b.adomain + } + }) + }) + }); + } + return bids; +}; + +export const onBidWon = function (bid) { + const cpm = bid.cpm || 0; + const adServerPrice = utils.deepAccess(bid, 'adserverTargeting.hb_pb', 0); + if (utils.isStr(bid.nurl) && bid.nurl !== '') { + const winNotificationUrl = utils.replaceAuctionPrice(bid.nurl, cpm) + .replace(/\${AD_SERVER_PRICE}/g, adServerPrice); + utils.triggerPixel(winNotificationUrl); + } +}; + +export const onTimeout = function (timeoutDataArray) { + if (utils.isArray(timeoutDataArray)) { + const refererInfo = getRefererInfo(); + const pageUrl = (refererInfo && refererInfo.referer) + ? refererInfo.referer + : window.location.href; + timeoutDataArray.forEach(timeoutData => { + const query = utils.parseQueryStringParameters({ + ad_unit_code: timeoutData.adUnitCode, + auction_id: timeoutData.auctionId, + bid_id: timeoutData.bidId, + timeout: timeoutData.timeout, + placement_id: utils.deepAccess(timeoutData, 'params.0.placementId'), + page_url: pageUrl, + }); + const timeoutNotificationUrl = `${TIMEOUT_NOTIFICATION_ENDPOINT}?${query}`; + utils.triggerPixel(timeoutNotificationUrl); + }); + } +}; + +function buildOpenRtbBidRequestPayload(validBidRequests, bidderRequest) { + const imps = validBidRequests.map(buildOpenRtbImpObject); + const timeout = bidderRequest.timeout || config.getConfig('bidderTimeout') || DEFAULT_TIMEOUT; + const pageUrl = (bidderRequest.refererInfo && bidderRequest.refererInfo.referer) + ? bidderRequest.refererInfo.referer + : window.location.href; + + const request = { + id: bidderRequest.auctionId, + at: 1, + tmax: timeout, + cur: [SUPPORTED_CURRENCY], + imp: imps, + device: { + devicetype: getDevice(), + geo: getGeo(validBidRequests[0]) + }, + site: { + page: pageUrl, + }, + test: getTest(validBidRequests[0]) + }; + + return JSON.stringify(request); +} + +function buildOpenRtbImpObject(validBidRequest) { + const sizes = getSizes(validBidRequest); + const mainSize = sizes[0]; + const floorInfo = getFloorInfo(validBidRequest, mainSize); + + return { + id: validBidRequest.bidId, + banner: { + format: buildFormatArray(sizes), + w: mainSize[0], + h: mainSize[1], + ext: { + kobler: { + pos: getPosition(validBidRequest) + } + } + }, + tagid: validBidRequest.params.placementId, + bidfloor: floorInfo.floor, + bidfloorcur: floorInfo.currency, + pmp: buildPmpObject(validBidRequest) + }; +} + +function getDevice() { + const ws = utils.getWindowSelf(); + const ua = ws.navigator.userAgent; + + if (/(tablet|ipad|playbook|silk|android 3.0|xoom|sch-i800|kindle)|(android(?!.*mobi))/i + .test(ua.toLowerCase())) { + return 5; // tablet + } + if (/(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|iris|kindle|lge |maemo|midp|mmp|mobile.+firefox|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series([46])0|symbian|treo|up\.(browser|link)|vodafone|wap|windows ce|xda|xiino/i + .test(ua.toLowerCase())) { + return 4; // phone + } + return 2; // personal computers +} + +function getGeo(validBidRequest) { + if (validBidRequest.params.zip) { + return { + zip: validBidRequest.params.zip + }; + } + return {}; +} + +function getTest(validBidRequest) { + return validBidRequest.params.test ? 1 : 0; +} + +function getSizes(validBidRequest) { + const sizes = utils.deepAccess(validBidRequest, 'mediaTypes.banner.sizes', validBidRequest.sizes); + if (utils.isArray(sizes) && sizes.length > 0) { + return sizes; + } + + return [[0, 0]]; +} + +function buildFormatArray(sizes) { + return sizes.map(size => { + return { + w: size[0], + h: size[1] + }; + }); +} + +function getPosition(validBidRequest) { + return parseInt(validBidRequest.params.position) || 0; +} + +function getFloorInfo(validBidRequest, mainSize) { + if (typeof validBidRequest.getFloor === 'function') { + const sizeParam = mainSize[0] === 0 && mainSize[1] === 0 ? '*' : mainSize; + return validBidRequest.getFloor({ + currency: SUPPORTED_CURRENCY, + mediaType: BANNER, + size: sizeParam + }); + } else { + return { + currency: SUPPORTED_CURRENCY, + floor: getFloorPrice(validBidRequest) + }; + } +} + +function getFloorPrice(validBidRequest) { + return parseFloat(validBidRequest.params.floorPrice) || 0.0; +} + +function buildPmpObject(validBidRequest) { + if (validBidRequest.params.dealIds) { + return { + deals: validBidRequest.params.dealIds.map(dealId => { + return { + id: dealId + }; + }) + }; + } + return {}; +} + +export const spec = { + code: BIDDER_CODE, + supportedMediaTypes: [BANNER], + isBidRequestValid, + buildRequests, + interpretResponse, + onBidWon, + onTimeout +}; + +registerBidder(spec); diff --git a/modules/koblerBidAdapter.md b/modules/koblerBidAdapter.md new file mode 100644 index 00000000000..7a7b2388367 --- /dev/null +++ b/modules/koblerBidAdapter.md @@ -0,0 +1,67 @@ +# Overview + +**Module Name**: Kobler Bidder Adapter +**Module Type**: Bidder Adapter +**Maintainer**: bidding-support@kobler.no + +# Description + +Connects to Kobler's demand sources. + +This adapter currently only supports Banner Ads. + +# Parameters + +| Parameter (in `params`) | Scope | Type | Description | Example | +| --- | --- | --- | --- | --- | +| placementId | Required | String | The identifier of the placement, it has to be issued by Kobler. | `'xjer0ch8'` | +| zip | Optional | String | Zip code of the user or the medium. When multiple ad units are submitted together, it is enough to set this parameter on the first one. | `'102 22'` | +| test | Optional | Boolean | Whether the request is for testing only. When multiple ad units are submitted together, it is enough to set this parameter on the first one. Defaults to false. | `true` | +| floorPrice | Optional | Float | Floor price in CPM and USD. Can be used as an alternative to the [Floors module](https://docs.prebid.org/dev-docs/modules/floors.html), which is also supported by this adapter. Defaults to 0. | `5.0` | +| position | Optional | Integer | The position of the ad unit. Can be used to differentiate between ad units if the same placement ID is used across multiple ad units. The first ad unit should have a `position` of 0, the second one should have a `position` of 1 and so on. Defaults to 0. | `1` | +| dealIds | Optional | Array of Strings | Array of deal IDs. | `['abc328745', 'mxw243253']` | + +# Test Parameters +```javascript + const adUnits = [{ + code: 'div-gpt-ad-1460505748561-1', + mediaTypes: { + banner: { + sizes: [[320, 250], [300, 250]], + } + }, + bids: [{ + bidder: 'kobler', + params: { + placementId: 'k5H7et3R0' + } + }] + }]; +``` + +In order to see a sample bid from Kobler (without a proper setup), you have to also do the following: +- Change the [`refererInfo` function](https://github.com/prebid/Prebid.js/blob/master/src/refererDetection.js) to return `'https://www.tv2.no/a/11734615'` as a [`referer`](https://github.com/prebid/Prebid.js/blob/caead3ccccc448e4cd09d074fd9f8833f56fe9b3/src/refererDetection.js#L169). This is necessary because Kobler only bids on recognized articles. +- Change the adapter's [`BIDDER_ENDPOINT`](https://github.com/prebid/Prebid.js/blob/master/modules/koblerBidAdapter.js#L8) to `'https://bid-service.dev.essrtb.com/bid/prebid_rtb_call'`. This endpoint belongs to the development server that is set up to always return a bid for the correct `placementId` and page URL combination. + +# Test Optional Parameters +```javascript + const adUnits = [{ + code: 'div-gpt-ad-1460505748561-1', + mediaTypes: { + banner: { + sizes: [[320, 250], [300, 250]], + } + }, + bids: [{ + bidder: 'kobler', + params: { + placementId: 'k5H7et3R0', + zip: '102 22', + test: true, + floorPrice: 5.0, + position: 1, + dealIds: ['abc328745', 'mxw243253'] + } + }] + }]; +``` diff --git a/modules/krushmediaBidAdapter.js b/modules/krushmediaBidAdapter.js new file mode 100644 index 00000000000..de1cce503e3 --- /dev/null +++ b/modules/krushmediaBidAdapter.js @@ -0,0 +1,123 @@ +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 = 'krushmedia'; +const AD_URL = 'https://ads4.krushmedia.com/?c=rtb&m=hb'; +const SYNC_URL = 'https://cs.krushmedia.com/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.key))); + }, + + 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 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 = { + key: bid.params.key, + bidId: bid.bidId, + traffic: bid.params.traffic || BANNER, + schain: bid.schain || {}, + }; + + if (bid.mediaTypes && bid.mediaTypes[BANNER] && bid.mediaTypes[BANNER].sizes) { + placement.sizes = bid.mediaTypes[BANNER].sizes; + } else if (bid.mediaTypes && bid.mediaTypes[VIDEO] && bid.mediaTypes[VIDEO].playerSize) { + placement.wPlayer = bid.mediaTypes[VIDEO].playerSize[0]; + placement.hPlayer = bid.mediaTypes[VIDEO].playerSize[1]; + } else if (bid.mediaTypes && bid.mediaTypes[NATIVE]) { + placement.native = bid.mediaTypes[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/krushmediaBidAdapter.md b/modules/krushmediaBidAdapter.md new file mode 100644 index 00000000000..7bf7c4fe491 --- /dev/null +++ b/modules/krushmediaBidAdapter.md @@ -0,0 +1,80 @@ +# Overview + +``` +Module Name: krushmedia Bidder Adapter +Module Type: krushmedia Bidder Adapter +Maintainer: adapter@krushmedia.com +``` + +# Description + +Module that connects to krushmedia demand sources + +# Test Parameters +``` + var adUnits = [ + // Will return static test banner + { + code: 'adunit1', + mediaTypes: { + banner: { + sizes: [[300, 250]], + } + }, + bids: [ + { + bidder: 'krushmedia', + params: { + key: 0, + traffic: 'banner' + } + } + ] + }, + // Will return test vast xml. All video params are stored under placement in publishers UI + { + code: 'addunit2', + mediaTypes: { + video: { + playerSize: [640, 480], + context: 'instream' + } + }, + bids: [ + { + bidder: 'krushmedia', + params: { + key: 0, + traffic: 'video' + } + } + ] + }, + { + code: 'addunit3', + mediaTypes: { + native: { + title: { + required: true + }, + body: { + required: true + }, + icon: { + required: true, + size: [64, 64] + } + } + }, + bids: [ + { + bidder: 'krushmedia', + params: { + key: 0, + traffic: 'native' + } + } + ] + } + ]; +``` 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 1ad660e5916..c7440743d2c 100644 --- a/modules/lemmaBidAdapter.js +++ b/modules/lemmaBidAdapter.js @@ -5,10 +5,13 @@ import { BANNER, VIDEO } from '../src/mediaTypes.js'; var BIDDER_CODE = 'lemma'; var LOG_WARN_PREFIX = 'LEMMA: '; var ENDPOINT = 'https://ads.lemmatechnologies.com/lemma/servad'; +var USER_SYNC = 'https://sync.lemmatechnologies.com/js/usersync.html?'; var DEFAULT_CURRENCY = 'USD'; var AUCTION_TYPE = 2; var DEFAULT_TMAX = 300; var DEFAULT_NET_REVENUE = false; +var pubId = 0; +var adunitId = 0; export var spec = { @@ -57,6 +60,17 @@ export var spec = { interpretResponse: (response, request) => { return parseRTBResponse(request, response.body); }, + getUserSyncs: (syncOptions, responses, gdprConsent, uspConsent) => { + let syncurl = USER_SYNC + 'pid=' + pubId; + if (syncOptions.iframeEnabled) { + return [{ + type: 'iframe', + url: syncurl + }]; + } else { + utils.logWarn(LOG_WARN_PREFIX + 'Please enable iframe based user sync.'); + } + }, }; function _initConf(refererInfo) { @@ -70,6 +84,30 @@ function _initConf(refererInfo) { return conf; } +function _setFloor(impObj, bid) { + let bidFloor = -1; + // get lowest floor from floorModule + if (typeof bid.getFloor === 'function') { + [BANNER, VIDEO].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 from impObj.bidfllor and floor from floor module + // as we are using Math.max, it is ok if we have not got any floor from floorModule, then value of bidFloor will be -1 + if (impObj.bidfloor) { + bidFloor = Math.max(bidFloor, impObj.bidfloor) + } + + // assign value only if bidFloor is > 0 + impObj.bidfloor = ((!isNaN(bidFloor) && bidFloor > 0) ? bidFloor : undefined); +} + function parseRTBResponse(request, response) { var bidResponses = []; try { @@ -96,9 +134,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; @@ -167,8 +205,8 @@ function _getImpressionArray(request) { function endPointURL(request) { var params = request && request[0].params ? request[0].params : null; if (params) { - var pubId = params.pubId ? params.pubId : 0; - var adunitId = params.adunitId ? params.adunitId : 0; + pubId = params.pubId ? params.pubId : 0; + adunitId = params.adunitId ? params.adunitId : 0; return ENDPOINT + '?pid=' + pubId + '&aid=' + adunitId; } return null; @@ -183,7 +221,7 @@ function _getDomain(url) { function _getSiteObject(request, conf) { var params = request && request.params ? request.params : null; if (params) { - var pubId = params.pubId ? params.pubId : '0'; + pubId = params.pubId ? params.pubId : '0'; var siteId = params.siteId ? params.siteId : '0'; var appParams = params.app; if (!appParams) { @@ -204,7 +242,7 @@ function _getSiteObject(request, conf) { function _getAppObject(request) { var params = request && request.params ? request.params : null; if (params) { - var pubId = params.pubId ? params.pubId : 0; + pubId = params.pubId ? params.pubId : 0; var appParams = params.app; if (appParams) { return { @@ -251,10 +289,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; @@ -342,11 +376,9 @@ function _getImpressionObject(bid) { secure: window.location.protocol === 'https:' ? 1 : 0, bidfloorcur: params.currency ? params.currency : DEFAULT_CURRENCY }; - if (params.bidFloor) { impression.bidfloor = params.bidFloor; } - if (bid.hasOwnProperty('mediaTypes')) { for (mediaTypes in bid.mediaTypes) { switch (mediaTypes) { @@ -382,7 +414,7 @@ function _getImpressionObject(bid) { } impression.banner = bObj; } - + _setFloor(impression, bid); return impression.hasOwnProperty(BANNER) || impression.hasOwnProperty(VIDEO) ? impression : undefined; } @@ -398,4 +430,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 bd2638e5936..5a955eefa92 100644 --- a/modules/liveIntentIdSystem.js +++ b/modules/liveIntentIdSystem.js @@ -5,14 +5,33 @@ * @requires module:modules/userId */ import * as utils from '../src/utils.js'; -import { ajax } from '../src/ajax.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 { uspDataHandler } from '../src/adapterManager.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); +const calls = { + ajaxGet: (url, onSuccess, onError, timeout) => { + ajaxBuilder(timeout)( + url, + { + success: onSuccess, + error: onError + }, + undefined, + { + method: 'GET', + withCredentials: true + } + ) + }, + pixelGet: (url, onload) => triggerPixel(url, onload) +} let eventFired = false; let liveConnect = null; @@ -24,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; } @@ -64,6 +75,9 @@ function initializeLiveConnect(configParams) { if (configParams.partner) { identityResolutionConfig.source = configParams.partner } + if (configParams.ajaxTimeout) { + identityResolutionConfig.ajaxTimeout = configParams.ajaxTimeout; + } const liveConnectConfig = parseLiveIntentCollectorConfig(configParams.liCollectConfig); liveConnectConfig.wrapperName = 'prebid'; @@ -73,9 +87,18 @@ function initializeLiveConnect(configParams) { if (usPrivacyString) { liveConnectConfig.usPrivacyString = usPrivacyString; } + const gdprConsent = gdprDataHandler.getConsentData() + if (gdprConsent) { + liveConnectConfig.gdprApplies = gdprConsent.gdprApplies; + liveConnectConfig.gdprConsent = gdprConsent.consentString; + } - // The second param is the storage object, which means that all LS & Cookie manipulation will go through PBJS utils. - liveConnect = LiveConnect(liveConnectConfig, storage); + // 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 = liveIntentIdSubmodule.getInitializer()(liveConnectConfig, storage, calls); + if (configParams.emailHash) { + liveConnect.push({ hash: configParams.emailHash }) + } return liveConnect; } @@ -88,24 +111,33 @@ 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 * `publisherId` params. * @function * @param {{unifiedId:string}} value - * @param {SubmoduleParams|undefined} [configParams] + * @param {SubmoduleConfig|undefined} config * @returns {{lipb:Object}} */ - decode(value, configParams) { + 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 } }; } @@ -121,38 +153,29 @@ export const liveIntentIdSubmodule = { /** * performs action to obtain id and return a value in the callback's response argument * @function - * @param {SubmoduleParams} [configParams] + * @param {SubmoduleConfig} [config] * @returns {IdResponse|undefined} */ - getId(configParams) { + getId(config) { + const configParams = (config && config.params) || {}; const liveConnect = initializeLiveConnect(configParams); if (!liveConnect) { return; } tryFireEvent(); - // Don't do the internal ajax call, but use the composed url and fire it via PBJS ajax module - const url = liveConnect.resolutionCallUrl(); - const result = function (callback) { - const callbacks = { - success: response => { - let responseObj = {}; - if (response) { - try { - responseObj = JSON.parse(response); - } catch (error) { - utils.logError(error); - } - } - callback(responseObj); + const result = function(callback) { + liveConnect.resolve( + response => { + callback(response); }, - error: error => { + error => { utils.logError(`${MODULE_NAME}: ID fetch encountered an error: `, error); callback(); } - }; - ajax(url, callbacks, undefined, { method: 'GET', withCredentials: true }); - }; - return {callback: result}; + ) + } + + return { callback: result }; } }; diff --git a/modules/livewrappedAnalyticsAdapter.js b/modules/livewrappedAnalyticsAdapter.js index b331448161e..a872a709ec9 100644 --- a/modules/livewrappedAnalyticsAdapter.js +++ b/modules/livewrappedAnalyticsAdapter.js @@ -34,6 +34,18 @@ let livewrappedAnalyticsAdapter = Object.assign(adapter({EMPTYURL, ANALYTICSTYPE cache.auctions[args.auctionId].timeStamp = args.start; args.bids.forEach(function(bidRequest) { + cache.auctions[args.auctionId].gdprApplies = args.gdprConsent ? args.gdprConsent.gdprApplies : undefined; + cache.auctions[args.auctionId].gdprConsent = args.gdprConsent ? args.gdprConsent.consentString : undefined; + let lwFloor; + + if (bidRequest.lwflr) { + lwFloor = bidRequest.lwflr.flr; + + let buyerFloor = bidRequest.lwflr.bflrs ? bidRequest.lwflr.bflrs[bidRequest.bidder] : undefined; + + lwFloor = buyerFloor || lwFloor; + } + cache.auctions[args.auctionId].bids[bidRequest.bidId] = { bidder: bidRequest.bidder, adUnit: bidRequest.adUnitCode, @@ -42,7 +54,11 @@ let livewrappedAnalyticsAdapter = Object.assign(adapter({EMPTYURL, ANALYTICSTYPE timeout: false, sendStatus: 0, readyToSend: 0, - start: args.start + start: args.start, + lwFloor: lwFloor, + floorData: bidRequest.floorData, + auc: bidRequest.auc, + buc: bidRequest.buc } utils.logInfo(bidRequest); @@ -59,7 +75,7 @@ let livewrappedAnalyticsAdapter = Object.assign(adapter({EMPTYURL, ANALYTICSTYPE bidResponse.cpm = args.cpm; bidResponse.ttr = args.timeToRespond; bidResponse.readyToSend = 1; - bidResponse.mediaType = args.mediaType == 'native' ? 2 : 1; + bidResponse.mediaType = args.mediaType == 'native' ? 2 : (args.mediaType == 'video' ? 4 : 1); if (!bidResponse.ttr) { bidResponse.ttr = time - bidResponse.start; } @@ -116,12 +132,15 @@ livewrappedAnalyticsAdapter.enableAnalytics = function (config) { }; livewrappedAnalyticsAdapter.sendEvents = function() { + var sentRequests = getSentRequests(); var events = { publisherId: initOptions.publisherId, - requests: getSentRequests(), - responses: getResponses(), - wins: getWins(), - timeouts: getTimeouts(), + gdpr: sentRequests.gdpr, + auctionIds: sentRequests.auctionIds, + requests: sentRequests.sentRequests, + responses: getResponses(sentRequests.gdpr, sentRequests.auctionIds), + wins: getWins(sentRequests.gdpr, sentRequests.auctionIds), + timeouts: getTimeouts(sentRequests.auctionIds), bidAdUnits: getbidAdUnits(), rcv: getAdblockerRecovered() }; @@ -144,10 +163,15 @@ function getAdblockerRecovered() { function getSentRequests() { var sentRequests = []; + var gdpr = []; + var auctionIds = []; Object.keys(cache.auctions).forEach(auctionId => { + let auction = cache.auctions[auctionId]; + let gdprPos = getGdprPos(gdpr, auction); + let auctionIdPos = getAuctionIdPos(auctionIds, auctionId); + Object.keys(cache.auctions[auctionId].bids).forEach(bidId => { - let auction = cache.auctions[auctionId]; let bid = auction.bids[bidId]; if (!(bid.sendStatus & REQUESTSENT)) { bid.sendStatus |= REQUESTSENT; @@ -155,21 +179,28 @@ function getSentRequests() { sentRequests.push({ timeStamp: auction.timeStamp, adUnit: bid.adUnit, - bidder: bid.bidder + bidder: bid.bidder, + gdpr: gdprPos, + floor: bid.lwFloor, + auctionId: auctionIdPos, + auc: bid.auc, + buc: bid.buc }); } }); }); - return sentRequests; + return {gdpr: gdpr, auctionIds: auctionIds, sentRequests: sentRequests}; } -function getResponses() { +function getResponses(gdpr, auctionIds) { var responses = []; Object.keys(cache.auctions).forEach(auctionId => { Object.keys(cache.auctions[auctionId].bids).forEach(bidId => { let auction = cache.auctions[auctionId]; + let gdprPos = getGdprPos(gdpr, auction); + let auctionIdPos = getAuctionIdPos(auctionIds, auctionId) let bid = auction.bids[bidId]; if (bid.readyToSend && !(bid.sendStatus & RESPONSESENT) && !bid.timeout) { bid.sendStatus |= RESPONSESENT; @@ -183,7 +214,13 @@ function getResponses() { cpm: bid.cpm, ttr: bid.ttr, IsBid: bid.isBid, - mediaType: bid.mediaType + mediaType: bid.mediaType, + gdpr: gdprPos, + floor: bid.floorData ? bid.floorData.floorValue : bid.lwFloor, + floorCur: bid.floorData ? bid.floorData.floorCurrency : undefined, + auctionId: auctionIdPos, + auc: bid.auc, + buc: bid.buc }); } }); @@ -192,13 +229,16 @@ function getResponses() { return responses; } -function getWins() { +function getWins(gdpr, auctionIds) { var wins = []; Object.keys(cache.auctions).forEach(auctionId => { Object.keys(cache.auctions[auctionId].bids).forEach(bidId => { let auction = cache.auctions[auctionId]; + let gdprPos = getGdprPos(gdpr, auction); + let auctionIdPos = getAuctionIdPos(auctionIds, auctionId); let bid = auction.bids[bidId]; + if (!(bid.sendStatus & WINSENT) && bid.won) { bid.sendStatus |= WINSENT; @@ -209,7 +249,13 @@ function getWins() { width: bid.width, height: bid.height, cpm: bid.cpm, - mediaType: bid.mediaType + mediaType: bid.mediaType, + gdpr: gdprPos, + floor: bid.floorData ? bid.floorData.floorValue : bid.lwFloor, + floorCur: bid.floorData ? bid.floorData.floorCurrency : undefined, + auctionId: auctionIdPos, + auc: bid.auc, + buc: bid.buc }); } }); @@ -218,10 +264,42 @@ function getWins() { return wins; } -function getTimeouts() { +function getGdprPos(gdpr, auction) { + var gdprPos = 0; + for (gdprPos = 0; gdprPos < gdpr.length; gdprPos++) { + if (gdpr[gdprPos].gdprApplies == auction.gdprApplies && + gdpr[gdprPos].gdprConsent == auction.gdprConsent) { + break; + } + } + + if (gdprPos == gdpr.length) { + gdpr[gdprPos] = {gdprApplies: auction.gdprApplies, gdprConsent: auction.gdprConsent}; + } + + return gdprPos; +} + +function getAuctionIdPos(auctionIds, auctionId) { + var auctionIdPos = 0; + for (auctionIdPos = 0; auctionIdPos < auctionIds.length; auctionIdPos++) { + if (auctionIds[auctionIdPos] == auctionId) { + break; + } + } + + if (auctionIdPos == auctionIds.length) { + auctionIds[auctionIdPos] = auctionId; + } + + return auctionIdPos; +} + +function getTimeouts(auctionIds) { var timeouts = []; Object.keys(cache.auctions).forEach(auctionId => { + let auctionIdPos = getAuctionIdPos(auctionIds, auctionId); Object.keys(cache.auctions[auctionId].bids).forEach(bidId => { let auction = cache.auctions[auctionId]; let bid = auction.bids[bidId]; @@ -231,7 +309,10 @@ function getTimeouts() { timeouts.push({ bidder: bid.bidder, adUnit: bid.adUnit, - timeStamp: auction.timeStamp + timeStamp: auction.timeStamp, + auctionId: auctionIdPos, + auc: bid.auc, + buc: bid.buc }); } }); diff --git a/modules/livewrappedAnalyticsAdapter.md b/modules/livewrappedAnalyticsAdapter.md new file mode 100644 index 00000000000..de4f352aa19 --- /dev/null +++ b/modules/livewrappedAnalyticsAdapter.md @@ -0,0 +1,22 @@ +# Overview +Module Name: Livewrapped Analytics Adapter + +Module Type: Analytics Adapter + +Maintainer: info@livewrapped.com + +# Description + +Analytics adapter for Livewrapped AB. In order to use the adapter, please contact Livewrapped AB. + +# Test Parameters + +``` +{ + provider: 'livewrapped', + options : { + publisherId: "64c01620-fa98-4936-9794-6001d8ebfdb0" + } +} + +``` diff --git a/modules/livewrappedBidAdapter.js b/modules/livewrappedBidAdapter.js index 78b29ab5016..93552638007 100644 --- a/modules/livewrappedBidAdapter.js +++ b/modules/livewrappedBidAdapter.js @@ -2,18 +2,19 @@ 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'; +import { BANNER, NATIVE, VIDEO } from '../src/mediaTypes.js'; import { getStorageManager } from '../src/storageManager.js'; export const storage = getStorageManager(); const BIDDER_CODE = 'livewrapped'; export const URL = 'https://lwadm.com/ad'; -const VERSION = '1.3'; +const VERSION = '1.4'; export const spec = { code: BIDDER_CODE, - supportedMediaTypes: [BANNER, NATIVE], + supportedMediaTypes: [BANNER, NATIVE, VIDEO], + gvlid: 919, /** * Determines whether or not the given bid request is valid. @@ -80,6 +81,8 @@ export const spec = { version: VERSION, gdprApplies: bidderRequest.gdprConsent ? bidderRequest.gdprConsent.gdprApplies : undefined, gdprConsent: bidderRequest.gdprConsent ? bidderRequest.gdprConsent.consentString : undefined, + coppa: getCoppa(), + usPrivacy: bidderRequest.uspConsent, cookieSupport: !utils.isSafariBrowser() && storage.cookiesAreEnabled(), rcv: getAdblockerRecovered(), adRequests: [...adRequests], @@ -129,7 +132,12 @@ export const spec = { if (ad.native) { bidResponse.native = ad.native; - bidResponse.mediaType = NATIVE + bidResponse.mediaType = NATIVE; + } + + if (ad.video) { + bidResponse.mediaType = VIDEO; + bidResponse.vastXml = ad.tag; } bidResponses.push(bidResponse); @@ -216,9 +224,15 @@ function bidToAdRequest(bid) { options: bid.params.options }; + if (bid.auc !== undefined) { + adRequest.auc = bid.auc; + } + adRequest.native = utils.deepAccess(bid, 'mediaTypes.native'); - if (adRequest.native && utils.deepAccess(bid, 'mediaTypes.banner')) { + adRequest.video = utils.deepAccess(bid, 'mediaTypes.video'); + + if ((adRequest.native || adRequest.video) && utils.deepAccess(bid, 'mediaTypes.banner')) { adRequest.banner = true; } @@ -247,33 +261,10 @@ function getAdblockerRecovered() { } catch (e) {} } -function AddExternalUserId(eids, value, source, atype, rtiPartner) { - if (utils.isStr(value)) { - var eid = { - source, - uids: [{ - id: value, - atype - }] - }; - - if (rtiPartner) { - eid.uids[0] = {ext: {rtiPartner}}; - } - - eids.push(eid); - } -} - function handleEids(bidRequests) { - let eids = []; const bidRequest = bidRequests[0]; - if (bidRequest && bidRequest.userId) { - AddExternalUserId(eids, utils.deepAccess(bidRequest, `userId.pubcid`), 'pubcommon', 1); // Also add this to eids - AddExternalUserId(eids, utils.deepAccess(bidRequest, `userId.id5id`), 'id5-sync.com', 1); - } - if (eids.length > 0) { - return {user: {ext: {eids}}}; + if (bidRequest && bidRequest.userIdAsEids) { + return {user: {ext: {eids: bidRequest.userIdAsEids}}}; } return undefined; @@ -320,4 +311,9 @@ function getDeviceHeight() { return window.innerHeight; } +function getCoppa() { + if (typeof config.getConfig('coppa') === 'boolean') { + return config.getConfig('coppa'); + } +} registerBidder(spec); diff --git a/modules/livewrappedBidAdapter.md b/modules/livewrappedBidAdapter.md index 15e3e8d533a..10fc2a4778a 100644 --- a/modules/livewrappedBidAdapter.md +++ b/modules/livewrappedBidAdapter.md @@ -8,7 +8,7 @@ Connects to Livewrapped Header Bidding wrapper for bids. -Livewrapped supports banner. +Livewrapped supports banner, native and video. # Test Parameters @@ -20,7 +20,7 @@ var adUnits = [ bids: [{ bidder: 'livewrapped', params: { - adUnitId: '6A32352E-BC17-4B94-B2A7-5BF1724417D7' + adUnitId: 'D801852A-681F-11E8-86A7-0A44794250D4' } }] } diff --git a/modules/lkqdBidAdapter.js b/modules/lkqdBidAdapter.js index 51d5c48e1fc..0f5782649ad 100644 --- a/modules/lkqdBidAdapter.js +++ b/modules/lkqdBidAdapter.js @@ -1,6 +1,7 @@ import * as utils from '../src/utils.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; import { VIDEO } from '../src/mediaTypes.js'; +import { config } from '../src/config.js'; const BIDDER_CODE = 'lkqd'; const BID_TTL_DEFAULT = 300; @@ -148,6 +149,9 @@ function buildRequests(validBidRequests, bidderRequest) { if (bidRequest.params.hasOwnProperty('dnt') && bidRequest.params.dnt != null) { sspData.dnt = bidRequest.params.dnt; } + if (config.getConfig('coppa') === true) { + sspData.coppa = 1; + } if (bidRequest.params.hasOwnProperty('pageurl') && bidRequest.params.pageurl != null) { sspData.pageurl = bidRequest.params.pageurl; } else if (bidderRequest && bidderRequest.refererInfo) { @@ -177,6 +181,12 @@ function buildRequests(validBidRequests, bidderRequest) { sspData.bidWidth = playerWidth; sspData.bidHeight = playerHeight; + for (let k = 1; k <= 40; k++) { + if (bidRequest.params.hasOwnProperty(`c${k}`) && bidRequest.params[`c${k}`]) { + sspData[`c${k}`] = bidRequest.params[`c${k}`]; + } + } + bidRequests.push({ method: 'GET', url: sspUrl, 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/logicadBidAdapter.js b/modules/logicadBidAdapter.js index 74c00ee39d6..2c919f9c157 100644 --- a/modules/logicadBidAdapter.js +++ b/modules/logicadBidAdapter.js @@ -1,12 +1,12 @@ import {registerBidder} from '../src/adapters/bidderFactory.js'; -import {BANNER} from '../src/mediaTypes.js'; +import {BANNER, NATIVE} from '../src/mediaTypes.js'; const BIDDER_CODE = 'logicad'; const ENDPOINT_URL = 'https://pb.ladsp.com/adrequest/prebid'; export const spec = { code: BIDDER_CODE, - supportedMediaTypes: [BANNER], + supportedMediaTypes: [BANNER, NATIVE], isBidRequestValid: function (bid) { return !!(bid.params && bid.params.tid); }, @@ -62,6 +62,7 @@ function newBidRequest(bid, bidderRequest) { prebidJsVersion: '$prebid.version$', referrer: bidderRequest.refererInfo.referer, auctionStartTime: bidderRequest.auctionStart, + eids: bid.userIdAsEids, }; } diff --git a/modules/logicadBidAdapter.md b/modules/logicadBidAdapter.md index 32d40a7d3cd..de439f3f8c2 100644 --- a/modules/logicadBidAdapter.md +++ b/modules/logicadBidAdapter.md @@ -11,15 +11,48 @@ Currently module supports only banner mediaType. # Test Parameters ``` - var adUnits = [{ - code: 'test-code', - sizes: [[300, 250],[300, 600]], - bids: [{ - bidder: 'logicad', - params: { - tid: 'test', - page: 'url', - } - }] - }]; +var adUnits = [ + // Banner adUnit + { + code: 'test-code', + sizes: [[300, 250], [300, 600]], + mediaTypes: { + banner: { + sizes: [[300, 250], [300, 600]] + } + }, + bids: [{ + bidder: 'logicad', + params: { + tid: 'lfp-test-banner', + page: 'url' + } + }] + }, + // Native adUnit + { + code: 'test-native-code', + sizes: [[1, 1]], + mediaTypes: { + native: { + title: { + required: true + }, + image: { + required: true + }, + sponsoredBy: { + required: true + } + } + }, + bids: [{ + bidder: 'logicad', + params: { + tid: 'lfp-test-native', + page: 'url' + } + }] + } +]; ``` diff --git a/modules/lotamePanoramaIdSystem.js b/modules/lotamePanoramaIdSystem.js index cdf9131dd68..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,39 +158,46 @@ 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 * @param {(Object|string)} value + * @param {SubmoduleConfig|undefined} config * @returns {(Object|undefined)} */ - decode(value, configParams) { - return utils.isStr(value) ? { 'lotamePanoramaId': value } : undefined; + decode(value, config) { + return utils.isStr(value) ? { lotamePanoramaId: value } : undefined; }, /** * Retrieve the Lotame Panorama Id * @function - * @param {SubmoduleParams} [configParams] + * @param {SubmoduleConfig} [config] * @param {ConsentData} [consentData] * @param {(Object|undefined)} cacheIdObj * @returns {IdResponse|undefined} */ - getId(configParams, consentData, cacheIdObj) { + 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, }; } @@ -182,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', @@ -204,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( @@ -220,7 +262,9 @@ export const lotamePanoramaIdSubmodule = { clearLotameCache(KEY_ID); } } else { - clearLotameCache(KEY_PROFILE); + if (shouldUpdateProfileId) { + clearLotameCache(KEY_PROFILE); + } clearLotameCache(KEY_ID); } } catch (error) { @@ -232,7 +276,7 @@ export const lotamePanoramaIdSubmodule = { undefined, { method: 'GET', - withCredentials: true + withCredentials: true, } ); }; diff --git a/modules/lunamediahbBidAdapter.js b/modules/lunamediahbBidAdapter.js new file mode 100644 index 00000000000..1376d0c1714 --- /dev/null +++ b/modules/lunamediahbBidAdapter.js @@ -0,0 +1,107 @@ +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 = 'lunamediahb'; +const AD_URL = 'https://balancer.lmgssp.com/?c=o&m=multi'; + +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) => { + let winTop = window; + let location; + try { + location = new URL(bidderRequest.refererInfo.referer) + winTop = window.top; + } catch (e) { + location = winTop.location; + utils.logMessage(e); + }; + + 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; + }, +}; + +registerBidder(spec); diff --git a/modules/lunamediahbBidAdapter.md b/modules/lunamediahbBidAdapter.md new file mode 100644 index 00000000000..184dd846a9d --- /dev/null +++ b/modules/lunamediahbBidAdapter.md @@ -0,0 +1,72 @@ +# Overview + +``` +Module Name: lunamedia Bidder Adapter +Module Type: lunamedia Bidder Adapter +Maintainer: support@lunamedia.io +``` + +# Description + +Module that connects to lunamedia demand sources + +# Test Parameters +``` + var adUnits = [ + { + code:'1', + mediaTypes:{ + banner: { + sizes: [[300, 250]], + } + }, + bids:[ + { + bidder: 'lunamediahb', + params: { + placementId: 0 + } + } + ] + }, + { + code:'1', + mediaTypes:{ + video: { + playerSize: [640, 480], + context: 'instream' + } + }, + bids:[ + { + bidder: 'lunamediahb', + params: { + placementId: 0 + } + } + ] + }, + { + code:'1', + mediaTypes:{ + native: { + title: { + required: true + }, + icon: { + required: true, + size: [64, 64] + } + } + }, + bids:[ + { + bidder: 'lunamediahb', + params: { + placementId: 0 + } + } + ] + } + ]; +``` diff --git a/modules/luponmediaBidAdapter.js b/modules/luponmediaBidAdapter.js index 0a2eed0ad13..29b54f77fbb 100644 --- a/modules/luponmediaBidAdapter.js +++ b/modules/luponmediaBidAdapter.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 {BANNER} from '../src/mediaTypes.js'; +import { ajax } from '../src/ajax.js'; const BIDDER_CODE = 'luponmedia'; const ENDPOINT_URL = 'https://rtb.adxpremium.services/openrtb2/auction'; @@ -113,6 +114,19 @@ export const spec = { hasSynced = true; return allUserSyncs; }, + onBidWon: bid => { + const bidString = JSON.stringify(bid); + spec.sendWinningsToServer(bidString); + }, + sendWinningsToServer: data => { + let mutation = `mutation {createWin(input: {win: {eventData: "${window.btoa(data)}"}}) {win {createTime } } }`; + let dataToSend = JSON.stringify({ query: mutation }); + + ajax('https://analytics.adxpremium.services/graphql', null, dataToSend, { + contentType: 'application/json', + method: 'POST' + }); + } }; function newOrtbBidRequest(bidRequest, bidderRequest, currentImps) { @@ -265,8 +279,9 @@ function newOrtbBidRequest(bidRequest, bidderRequest, currentImps) { 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')); + const fpd = config.getLegacyFpd(config.getConfig('ortb2')) || {}; + const siteData = Object.assign({}, bidRequest.params.inventory, fpd.context); + const userData = Object.assign({}, bidRequest.params.visitor, fpd.user); if (!utils.isEmpty(siteData) || !utils.isEmpty(userData)) { const bidderData = { @@ -287,7 +302,7 @@ function newOrtbBidRequest(bidRequest, bidderRequest, currentImps) { utils.deepSetValue(data, 'ext.prebid.bidderconfig.0', bidderData); } - const pbAdSlot = utils.deepAccess(bidRequest, 'fpd.context.pbAdSlot'); + const pbAdSlot = utils.deepAccess(bidRequest, 'ortb2Imp.ext.data.pbadslot'); if (typeof pbAdSlot === 'string' && pbAdSlot) { utils.deepSetValue(data.imp[0].ext, 'context.data.adslot', pbAdSlot); } diff --git a/modules/malltvBidAdapter.js b/modules/malltvBidAdapter.js new file mode 100644 index 00000000000..7deffe6c07a --- /dev/null +++ b/modules/malltvBidAdapter.js @@ -0,0 +1,116 @@ +import { registerBidder } from '../src/adapters/bidderFactory.js'; +import { BANNER, VIDEO } from '../src/mediaTypes.js'; + +const BIDDER_CODE = 'malltv'; +const ENDPOINT_URL = 'https://central.mall.tv/bid'; +const DIMENSION_SEPARATOR = 'x'; +const SIZE_SEPARATOR = ';'; + +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: function (bid) { + return !!(bid.params.propertyId && bid.params.placementId); + }, + /** + * Make a server request from the list of BidRequests. + * + * @param {validBidRequests[]} - an array of bids + * @return ServerRequest Info describing the request to the server. + */ + buildRequests: function (validBidRequests, bidderRequest) { + let propertyId = ''; + let pageViewGuid = ''; + let storageId = ''; + let bidderRequestId = ''; + let url = ''; + let contents = []; + let data = {}; + + let placements = validBidRequests.map(bidRequest => { + if (!propertyId) { propertyId = bidRequest.params.propertyId; } + if (!pageViewGuid && bidRequest.params) { pageViewGuid = bidRequest.params.pageViewGuid || ''; } + 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 (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; + let sizes = generateSizeParam(bidRequest.sizes); + + return { + sizes: sizes, + adUnitId: adUnitId, + placementId: placementId, + bidid: bidRequest.bidId, + count: bidRequest.params.count, + skipTime: bidRequest.params.skipTime + }; + }); + + let body = { + propertyId: propertyId, + pageViewGuid: pageViewGuid, + storageId: storageId, + url: url, + requestid: bidderRequestId, + placements: placements, + contents: contents, + data: data + } + + return [{ + method: 'POST', + url: ENDPOINT_URL, + data: body + }]; + }, + /** + * Unpack the response from the server into a list of bids. + * + * @param {ServerResponse} serverResponse A successful response from the server. + * @return {Bid[]} An array of bids which were nested inside the server. + */ + interpretResponse: function (serverResponse) { + const responses = serverResponse.body; + const bidResponses = []; + for (var i = 0; i < responses.length; i++) { + const bidResponse = { + requestId: responses[i].BidId, + cpm: responses[i].CPM, + width: responses[i].Width, + height: responses[i].Height, + creativeId: responses[i].CreativeId, + currency: responses[i].Currency, + netRevenue: responses[i].NetRevenue, + ttl: responses[i].TTL, + referrer: responses[i].Referrer, + ad: responses[i].Ad, + vastUrl: responses[i].VastUrl, + mediaType: responses[i].MediaType + }; + bidResponses.push(bidResponse); + } + return bidResponses; + } +} + +/** +* Generate size param for bid request using sizes array +* +* @param {Array} sizes Possible sizes for the ad unit. +* @return {string} Processed sizes param to be used for the bid request. +*/ +function generateSizeParam(sizes) { + return sizes.map(size => size.join(DIMENSION_SEPARATOR)).join(SIZE_SEPARATOR); +} + +registerBidder(spec); diff --git a/modules/malltvBidAdapter.md b/modules/malltvBidAdapter.md new file mode 100644 index 00000000000..e32eb54f90f --- /dev/null +++ b/modules/malltvBidAdapter.md @@ -0,0 +1,68 @@ +# Overview +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] + ] + } + }, + bids: [{ + bidder: 'malltv', + params: { + propertyId: '105134', //Required + placementId: '846832', //Required + data: { //Optional + catalogs: [{ + catalogId: 9, + items: ["193", "4", "1"] + }], + inventory: { + category: ["tech"], + query: ["iphone 12"] + } + } + } + }] + }, + { + code: 'test-div', + mediaTypes: { + video: { + context: 'instream' + } + }, + 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/marsmediaBidAdapter.js b/modules/marsmediaBidAdapter.js index 1ce2558b8de..bb1763ebb2e 100644 --- a/modules/marsmediaBidAdapter.js +++ b/modules/marsmediaBidAdapter.js @@ -3,6 +3,7 @@ import * as utils from '../src/utils.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; import { BANNER, VIDEO } from '../src/mediaTypes.js'; +import {config} from '../src/config.js'; function MarsmediaAdapter() { this.code = 'marsmedia'; @@ -16,7 +17,7 @@ function MarsmediaAdapter() { let SUPPORTED_VIDEO_API = [1, 2, 5]; let slotsToBids = {}; let that = this; - let version = '2.3'; + let version = '2.4'; this.isBidRequestValid = function (bid) { return !!(bid.params && bid.params.zoneId); @@ -53,6 +54,7 @@ function MarsmediaAdapter() { if (!(impObj.banner || impObj.video)) { continue; } + impObj.bidfloor = _getFloor(BRs[i]); impObj.ext = frameExt(BRs[i]); impList.push(impObj); } @@ -153,9 +155,31 @@ function MarsmediaAdapter() { } function frameExt(bid) { - return { - bidder: { - zoneId: bid.params['zoneId'] + if ((bid.mediaTypes && bid.mediaTypes.banner && bid.mediaTypes.banner.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)})); + + const element = document.getElementById(bid.adUnitCode); + const minSize = _getMinSize(processedSizes); + const viewabilityAmount = _isViewabilityMeasurable(element) + ? _getViewability(element, utils.getWindowTop(), minSize) + : 'na'; + const viewabilityAmountRounded = isNaN(viewabilityAmount) ? viewabilityAmount : Math.round(viewabilityAmount); + + return { + bidder: { + zoneId: bid.params['zoneId'] + }, + viewability: viewabilityAmountRounded + } + } else { + return { + bidder: { + zoneId: bid.params['zoneId'] + }, + viewability: 'na' } } } @@ -180,12 +204,15 @@ function MarsmediaAdapter() { } }; if (BRs[0].schain) { - bid.source = { - 'ext': { - 'schain': BRs[0].schain - } - } + utils.deepSetValue(bid, 'source.ext.schain', BRs[0].schain); + } + if (bidderRequest.uspConsent) { + utils.deepSetValue(bid, 'regs.ext.us_privacy', bidderRequest.uspConsent) } + if (config.getConfig('coppa') === true) { + utils.deepSetValue(bid, 'regs.coppa', config.getConfig('coppa') & 1) + } + return bid; } @@ -241,12 +268,6 @@ function MarsmediaAdapter() { sendbeacon(bid, 20) }; - function sendbeacon(bid, type) { - const bidString = JSON.stringify(bid); - const encodedBuf = window.btoa(bidString); - utils.triggerPixel('https://ping-hqx-1.go2speed.media/notification/rtb/beacon/?bt=' + type + '&bid=3mhdom&hb_j=' + encodedBuf, null); - } - this.interpretResponse = function (serverResponse) { let responses = serverResponse.body || []; let bids = []; @@ -295,6 +316,126 @@ function MarsmediaAdapter() { return bids; }; + + function sendbeacon(bid, type) { + const bidString = JSON.stringify(bid); + const encodedBuf = window.btoa(bidString); + utils.triggerPixel('https://ping-hqx-1.go2speed.media/notification/rtb/beacon/?bt=' + type + '&bid=3mhdom&hb_j=' + encodedBuf, null); + } + + /** + * Gets bidfloor + * @param {Object} bid + * @returns {Number} floor + */ + function _getFloor (bid) { + const curMediaType = bid.mediaTypes.video ? 'video' : 'banner'; + let floor = 0; + + if (typeof bid.getFloor === 'function') { + const floorInfo = bid.getFloor({ + currency: 'USD', + mediaType: curMediaType, + size: '*' + }); + + if (typeof floorInfo === 'object' && + floorInfo.currency === 'USD' && + !isNaN(parseFloat(floorInfo.floor))) { + floor = floorInfo.floor; + } + } + + return floor; + } + + function _getMinSize(sizes) { + return sizes.reduce((min, size) => size.h * size.w < min.h * min.w ? size : min); + } + + function _isViewabilityMeasurable(element) { + return !_isIframe() && element !== null; + } + + function _isIframe() { + try { + return utils.getWindowSelf() !== utils.getWindowTop(); + } catch (e) { + return true; + } + } + + function _getViewability(element, topWin, { w, h } = {}) { + return topWin.document.visibilityState === 'visible' + ? _getPercentInView(element, topWin, { w, h }) + : 0; + } + + function _getPercentInView(element, topWin, { w, h } = {}) { + const elementBoundingBox = _getBoundingBox(element, { w, h }); + + const elementInViewBoundingBox = _getIntersectionOfRects([ { + left: 0, + top: 0, + right: topWin.innerWidth, + bottom: topWin.innerHeight + }, elementBoundingBox ]); + + let elementInViewArea, elementTotalArea; + + if (elementInViewBoundingBox !== null) { + // Some or all of the element is in view + elementInViewArea = elementInViewBoundingBox.width * elementInViewBoundingBox.height; + elementTotalArea = elementBoundingBox.width * elementBoundingBox.height; + + return ((elementInViewArea / elementTotalArea) * 100); + } + + return 0; + } + + function _getBoundingBox(element, { w, h } = {}) { + let { width, height, left, top, right, bottom } = element.getBoundingClientRect(); + + if ((width === 0 || height === 0) && w && h) { + width = w; + height = h; + right = left + w; + bottom = top + h; + } + + return { width, height, left, top, right, bottom }; + } + + function _getIntersectionOfRects(rects) { + const bbox = { + left: rects[0].left, + right: rects[0].right, + top: rects[0].top, + bottom: rects[0].bottom + }; + + for (let i = 1; i < rects.length; ++i) { + bbox.left = Math.max(bbox.left, rects[i].left); + bbox.right = Math.min(bbox.right, rects[i].right); + + if (bbox.left >= bbox.right) { + return null; + } + + bbox.top = Math.max(bbox.top, rects[i].top); + bbox.bottom = Math.min(bbox.bottom, rects[i].bottom); + + if (bbox.top >= bbox.bottom) { + return null; + } + } + + bbox.width = bbox.right - bbox.left; + bbox.height = bbox.bottom - bbox.top; + + return bbox; + } } export const spec = new MarsmediaAdapter(); diff --git a/modules/mass.js b/modules/mass.js new file mode 100644 index 00000000000..01135e7ddff --- /dev/null +++ b/modules/mass.js @@ -0,0 +1,184 @@ +/** + * This module adds MASS support to Prebid.js. + */ + +import { config } from '../src/config.js'; +import { getHook } from '../src/hook.js'; +import find from 'core-js-pure/features/array/find.js'; + +const defaultCfg = { + dealIdPattern: /^MASS/i +}; +let cfg; + +export let listenerAdded = false; +export let isEnabled = false; + +const matchedBids = {}; +let renderers; + +init(); +config.getConfig('mass', config => init(config.mass)); + +/** + * Module init. + */ +export function init(userCfg) { + cfg = Object.assign({}, defaultCfg, window.massConfig && window.massConfig.mass, userCfg); + + if (cfg.enabled === false) { + if (isEnabled) { + getHook('addBidResponse').getHooks({hook: addBidResponseHook}).remove(); + isEnabled = false; + } + } else { + if (!isEnabled) { + getHook('addBidResponse').before(addBidResponseHook); + isEnabled = true; + } + } + + if (isEnabled) { + updateRenderers(); + } +} + +/** + * Update the list of renderers based on current config. + */ +export function updateRenderers() { + renderers = []; + + // official MASS renderer: + if (cfg.dealIdPattern && cfg.renderUrl) { + renderers.push({ + match: isMassBid, + render: useDefaultRender(cfg.renderUrl, 'mass') + }); + } + + // add any custom renderer defined in the config: + (cfg.custom || []).forEach(renderer => { + if (!renderer.match && renderer.dealIdPattern) { + renderer.match = useDefaultMatch(renderer.dealIdPattern); + } + + if (!renderer.render && renderer.renderUrl && renderer.namespace) { + renderer.render = useDefaultRender(renderer.renderUrl, renderer.namespace); + } + + if (renderer.match && renderer.render) { + renderers.push(renderer); + } + }); + + return renderers; +} + +/** + * Before hook for 'addBidResponse'. + */ +export function addBidResponseHook(next, adUnitCode, bid) { + let renderer; + for (let i = 0; i < renderers.length; i++) { + if (renderers[i].match(bid)) { + renderer = renderers[i]; + break; + } + } + + if (renderer) { + const bidRequest = find(this.bidderRequest.bids, bidRequest => + bidRequest.bidId === bid.requestId + ); + + matchedBids[bid.requestId] = { + renderer, + payload: { + bidRequest, + bid, + adm: bid.ad + } + }; + + bid.ad = '`; + 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/outbrainBidAdapter.md b/modules/outbrainBidAdapter.md new file mode 100644 index 00000000000..32df22ddad8 --- /dev/null +++ b/modules/outbrainBidAdapter.md @@ -0,0 +1,111 @@ +# Overview + +``` +Module Name: Outbrain Adapter +Module Type: Bidder Adapter +Maintainer: prog-ops-team@outbrain.com +``` + +# Description + +Module that connects to Outbrain 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 Outbrain adapter does not work without setting the correct bidder and usersync URLs. +You will receive the URLs when contacting us. + +``` +pbjs.setConfig({ + outbrain: { + 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: 'outbrain', + params: { + publisher: { + id: '2706', // required + name: 'Publishers Name', + domain: 'publisher.com' + }, + tagid: 'tag-id', + bcat: ['IAB1-1'], + badv: ['example.com'] + } + }] + ]; + + pbjs.setConfig({ + outbrain: { + 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: 'outbrain', + params: { + publisher: { + id: '2706', // required + name: 'Publishers Name', + domain: 'publisher.com' + }, + tagid: 'tag-id', + bcat: ['IAB1-1'], + badv: ['example.com'] + }, + }] + ]; + + pbjs.setConfig({ + outbrain: { + bidderUrl: 'https://prebidtest.zemanta.com/api/bidder/prebidtest/bid/' + } + }); +``` diff --git a/modules/ozoneBidAdapter.js b/modules/ozoneBidAdapter.js index 451ab654c53..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,16 +601,25 @@ 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', 'id5id', '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'); @@ -560,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 @@ -638,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`), '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; }, @@ -721,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; @@ -743,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); @@ -849,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++) { @@ -875,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 @@ -928,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]; } @@ -953,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', @@ -971,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']; @@ -1040,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]}); @@ -1075,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; @@ -1095,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); @@ -1117,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 75f89fffc14..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) { @@ -113,7 +124,58 @@ function migrateLegacyCookies(parrableId) { } } -function fetchId(configParams) { +function shouldFilterImpression(configParams, parrableId) { + const config = configParams.timezoneFilter; + + if (!config) { + return false; + } + + if (parrableId) { + return false; + } + + 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 (isZoneListed(config.allowedZones, zone)) { + return true; + } + if (utils.contains(config.allowedOffsets, offset)) { + return true; + } + return false; + } + + function isBlocked() { + if (utils.isEmpty(config.blockedZones) && + utils.isEmpty(config.blockedOffsets)) { + return false; + } + if (isZoneListed(config.blockedZones, zone)) { + return true; + } + if (utils.contains(config.blockedOffsets, offset)) { + return true; + } + return false; + } + + return isBlocked() || !isAllowed(); +} + +function fetchId(configParams, gdprConsentData) { if (!isValidConfig(configParams)) return; let parrableId = readCookie(); @@ -122,18 +184,31 @@ function fetchId(configParams) { migrateLegacyCookies(parrableId); } + if (shouldFilterImpression(configParams, parrableId)) { + return null; + } + 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() }; @@ -141,6 +216,10 @@ function fetchId(configParams) { searchParams.us_privacy = uspString; } + if (gdprApplies) { + searchParams.gdpr_consent = gdprConsentString; + } + const options = { method: 'GET', withCredentials: true @@ -187,7 +266,7 @@ function fetchId(configParams) { callback, id: parrableId }; -}; +} /** @type {Submodule} */ export const parrableIdSubmodule = { @@ -196,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 @@ -212,12 +297,13 @@ export const parrableIdSubmodule = { /** * performs action to obtain id and return a value in the callback's response argument * @function - * @param {SubmoduleParams} [configParams] + * @param {SubmoduleConfig} [config] * @param {ConsentData} [consentData] * @returns {function(callback:function), id:ParrableId} */ - getId(configParams, gdprConsentData, currentStoredId) { - return fetchId(configParams); + getId(config, gdprConsentData, currentStoredId) { + const configParams = (config && config.params) || {}; + return fetchId(configParams, gdprConsentData); } }; diff --git a/modules/permutiveRtdProvider.js b/modules/permutiveRtdProvider.js new file mode 100644 index 00000000000..91e88d3e4e1 --- /dev/null +++ b/modules/permutiveRtdProvider.js @@ -0,0 +1,189 @@ +/** + * 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, logError } from '../src/utils.js' +import includes from 'core-js-pure/features/array/includes.js' +const MODULE_NAME = 'permutive' + +export const storage = getStorageManager(null, MODULE_NAME) + +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) + } + }) + }) +} + +function makeSafe (fn) { + try { + fn() + } catch (e) { + logError(e) + } +} + +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 + }, + trustx: function (bid, data, acEnabled) { + if (acEnabled && data.ac && data.ac.length) { + deepSetValue(bid, 'params.keywords.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: MODULE_NAME, + getBidRequestData: function (reqBidsConfigObj, callback, customConfig) { + makeSafe(function () { + initSegments(reqBidsConfigObj, callback, customConfig) + }) + }, + init: init +} + +submodule('realTimeData', permutiveSubmodule) diff --git a/modules/permutiveRtdProvider.md b/modules/permutiveRtdProvider.md new file mode 100644 index 00000000000..3738c6e8be7 --- /dev/null +++ b/modules/permutiveRtdProvider.md @@ -0,0 +1,103 @@ +# 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=rtdModule,permutiveRtdProvider +``` + +> Note that the global RTD module, `rtdModule`, is a prerequisite of the Permutive RTD module. + +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 | +| TrustX | `trustx` | 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/config.js b/modules/prebidServerBidAdapter/config.js index 56e4fdfbfef..92a8f5deaa5 100644 --- a/modules/prebidServerBidAdapter/config.js +++ b/modules/prebidServerBidAdapter/config.js @@ -3,15 +3,40 @@ export const S2S_VENDORS = { 'appnexus': { adapter: 'prebidServer', enabled: true, - endpoint: 'https://prebid.adnxs.com/pbs/v1/openrtb2/auction', - syncEndpoint: 'https://prebid.adnxs.com/pbs/v1/cookie_sync', + endpoint: { + p1Consent: 'https://prebid.adnxs.com/pbs/v1/openrtb2/auction', + noP1Consent: 'https://prebid.adnxs-simple.com/pbs/v1/openrtb2/auction' + }, + syncEndpoint: { + p1Consent: 'https://prebid.adnxs.com/pbs/v1/cookie_sync', + noP1Consent: 'https://prebid.adnxs-simple.com/pbs/v1/cookie_sync' + }, timeout: 1000 }, 'rubicon': { adapter: 'prebidServer', enabled: true, - endpoint: 'https://prebid-server.rubiconproject.com/openrtb2/auction', - syncEndpoint: 'https://prebid-server.rubiconproject.com/cookie_sync', + endpoint: { + p1Consent: 'https://prebid-server.rubiconproject.com/openrtb2/auction', + noP1Consent: 'https://prebid-server.rubiconproject.com/openrtb2/auction', + }, + syncEndpoint: { + p1Consent: 'https://prebid-server.rubiconproject.com/cookie_sync', + noP1Consent: 'https://prebid-server.rubiconproject.com/cookie_sync', + }, timeout: 500 + }, + 'openx': { + adapter: 'prebidServer', + enabled: true, + endpoint: { + p1Consent: 'https://prebid.openx.net/openrtb2/auction', + noP1Consent: 'https://prebid.openx.net/openrtb2/auction' + }, + syncEndpoint: { + p1Consent: 'https://prebid.openx.net/cookie_sync', + noP1Consent: 'https://prebid.openx.net/cookie_sync' + }, + timeout: 1000 } } diff --git a/modules/prebidServerBidAdapter/index.js b/modules/prebidServerBidAdapter/index.js index b3d559d956f..ec5d05f0fe0 100644 --- a/modules/prebidServerBidAdapter/index.js +++ b/modules/prebidServerBidAdapter/index.js @@ -1,7 +1,7 @@ import Adapter from '../../src/adapter.js'; import { createBid } from '../../src/bidfactory.js'; import * as utils from '../../src/utils.js'; -import { STATUS, S2S, EVENTS } from '../../src/constants.json'; +import CONSTANTS from '../../src/constants.json'; import adapterManager from '../../src/adapterManager.js'; import { config } from '../../src/config.js'; import { VIDEO, NATIVE } from '../../src/mediaTypes.js'; @@ -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; +const TYPE = CONSTANTS.S2S.SRC; +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,18 +48,30 @@ 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 */ /** * @type {S2SDefaultConfig} */ const s2sDefaultConfig = { - enabled: false, timeout: 1000, maxBids: 1, adapter: 'prebidServer', @@ -64,30 +84,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 +104,16 @@ function setS2sConfig(options) { return false; } } + // this is how we can know if user / defaultVendor has set it, or if we should default to false + return option.enabled = typeof option.enabled === 'boolean' ? option.enabled : 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 +121,56 @@ function setS2sConfig(options) { } return false; }).length > 0) { + return false; + } +} + +// temporary change to modify the s2sConfig for new format used for endpoint URLs; +// could be removed later as part of a major release, if we decide to not support the old format +function formatUrlParams(option) { + ['endpoint', 'syncEndpoint'].forEach((prop) => { + if (utils.isStr(option[prop])) { + let temp = option[prop]; + option[prop] = { p1Consent: temp, noP1Consent: temp }; + } + }); +} + +/** + * @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) => { + formatUrlParams(options); + 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; + } + } + utils.logWarn('prebidServer: s2s config is disabled'); + return false; + }); - _s2sConfig = options; + if (optionsValid) { + return _s2sConfigs = normalizedOptions; + } } getConfig('s2sConfig', ({s2sConfig}) => setS2sConfig(s2sConfig)); @@ -116,51 +178,52 @@ 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; } if (gdprConsent) { - // only populate gdpr field if we know CMP returned consent information (ie didn't timeout or have an error) - if (typeof gdprConsent.consentString !== 'undefined') { - payload.gdpr = (gdprConsent.gdprApplies) ? 1 : 0; - } + payload.gdpr = (gdprConsent.gdprApplies) ? 1 : 0; // attempt to populate gdpr_consent if we know gdprApplies or it may apply if (gdprConsent.gdprApplies !== false) { payload.gdpr_consent = gdprConsent.consentString; } } - // US Privace (CCPA) support + // US Privacy (CCPA) support if (uspConsent) { payload.us_privacy = uspConsent; } + if (typeof s2sConfig.coopSync === 'boolean') { + payload.coopSync = s2sConfig.coopSync; + } + const jsonPayload = JSON.stringify(payload); - ajax(_s2sConfig.syncEndpoint, + ajax(getMatchingConsentUrl(s2sConfig.syncEndpoint, gdprConsent), (response) => { try { response = JSON.parse(response); - doAllSyncs(response.bidder_status); + doAllSyncs(response.bidder_status, s2sConfig); } catch (e) { utils.logError(e); } @@ -172,16 +235,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 +259,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) @@ -231,22 +299,31 @@ function doBidderSync(type, url, bidder, done) { * * @param {Array} bidders a list of bidder names */ -function doClientSideSyncs(bidders) { +function doClientSideSyncs(bidders, gdprConsent, uspConsent) { bidders.forEach(bidder => { let clientAdapter = adapterManager.getBidAdapter(bidder); if (clientAdapter && clientAdapter.registerSyncs) { - clientAdapter.registerSyncs([]); + config.runWithBidder( + bidder, + utils.bind.call( + clientAdapter.registerSyncs, + clientAdapter, + [], + gdprConsent, + uspConsent + ) + ); } }); } -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 +331,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) { @@ -279,18 +356,18 @@ function addBidderFirstPartyDataToRequest(request) { const bidderConfig = config.getBidderConfig(); const fpdConfigs = Object.keys(bidderConfig).reduce((acc, bidder) => { const currBidderConfig = bidderConfig[bidder]; - if (currBidderConfig.fpd) { - const fpd = {}; - if (currBidderConfig.fpd.context) { - fpd.site = currBidderConfig.fpd.context; + if (currBidderConfig.ortb2) { + const ortb2 = {}; + if (currBidderConfig.ortb2.site) { + ortb2.site = currBidderConfig.ortb2.site; } - if (currBidderConfig.fpd.user) { - fpd.user = currBidderConfig.fpd.user; + if (currBidderConfig.ortb2.user) { + ortb2.user = currBidderConfig.ortb2.user; } acc.push({ bidders: [ bidder ], - config: { fpd } + config: { ortb2 } }); } return acc; @@ -369,6 +446,18 @@ function addWurl(auctionId, adId, wurl) { } } +function getPbsResponseData(bidderRequests, response, pbsName, pbjsName) { + const bidderValues = utils.deepAccess(response, `ext.${pbsName}`); + if (bidderValues) { + Object.keys(bidderValues).forEach(bidder => { + let biddersReq = find(bidderRequests, bidderReq => bidderReq.bidderCode === bidder); + if (biddersReq) { + biddersReq[pbjsName] = bidderValues[bidder]; + } + }); + } +} + /** * @param {string} auctionId * @param {string} adId generated value set to bidObject.adId by bidderFactory Bid() @@ -397,7 +486,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]; @@ -480,7 +569,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]; + } } }); @@ -504,7 +598,20 @@ const OPEN_RTB_PROTOCOL = { // Don't push oustream w/o renderer to request object. utils.logError('Outstream bid without renderer cannot be sent to Prebid Server.'); } else { - mediaTypes['video'] = videoParams; + if (videoParams.context === 'instream' && !videoParams.hasOwnProperty('placement')) { + videoParams.placement = 1; + } + + mediaTypes['video'] = Object.keys(videoParams).filter(param => param !== 'context') + .reduce((result, param) => { + if (param === 'playerSize') { + result.w = utils.deepAccess(videoParams, `${param}.0.0`); + result.h = utils.deepAccess(videoParams, `${param}.0.1`); + } else { + result[param] = videoParams[param]; + } + return result; + }, {}); } } @@ -530,34 +637,46 @@ const OPEN_RTB_PROTOCOL = { } // get bidder params in form { : {...params} } + // initialize reduce function with the user defined `ext` properties on the ad unit const ext = adUnit.bids.reduce((acc, bid) => { const adapter = adapterManager.bidderRegistry[bid.bidder]; 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 }; - - /** - * Prebid AdSlot - * @type {(string|undefined)} - */ - const pbAdSlot = utils.deepAccess(adUnit, 'fpd.context.pbAdSlot'); - if (typeof pbAdSlot === 'string' && pbAdSlot) { - utils.deepSetValue(imp, 'ext.context.data.pbadslot', pbAdSlot); - } - - /** - * GAM Ad Unit - * @type {(string|undefined)} - */ - const gamAdUnit = utils.deepAccess(adUnit, 'fpd.context.adServer.adSlot'); - if (typeof gamAdUnit === 'string' && gamAdUnit) { - utils.deepSetValue(imp, 'ext.context.data.adslot', gamAdUnit); - } + }, {...utils.deepAccess(adUnit, 'ortb2Imp.ext')}); + + const imp = { id: adUnit.code, ext, secure: s2sConfig.secure }; + + const ortb2 = {...utils.deepAccess(adUnit, 'ortb2Imp.ext.data')}; + Object.keys(ortb2).forEach(prop => { + /** + * Prebid AdSlot + * @type {(string|undefined)} + */ + if (prop === 'pbadslot') { + if (typeof ortb2[prop] === 'string' && ortb2[prop]) { + utils.deepSetValue(imp, 'ext.data.pbadslot', ortb2[prop]); + } else { + // remove pbadslot property if it doesn't meet the spec + delete imp.ext.data.pbadslot; + } + } else if (prop === 'adserver') { + /** + * Copy GAM AdUnit and Name to imp + */ + ['name', 'adslot'].forEach(name => { + /** @type {(string|undefined)} */ + const value = utils.deepAccess(ortb2, `adserver.${name}`); + if (typeof value === 'string' && value) { + utils.deepSetValue(imp, `ext.data.adserver.${name.toLowerCase()}`, value); + } + }); + } else { + utils.deepSetValue(imp, `ext.data.${prop}`, ortb2[prop]); + } + }); Object.assign(imp, mediaTypes); @@ -567,6 +686,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); } @@ -579,7 +715,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: { @@ -596,9 +732,12 @@ const OPEN_RTB_PROTOCOL = { } }; + // Sets pbjs version, can be overwritten below if channel exists in s2sConfig.extPrebid + request.ext.prebid = Object.assign(request.ext.prebid, {channel: {name: 'pbjs', version: $$PREBID_GLOBAL$$.version}}) + // 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); } /** @@ -613,7 +752,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'); @@ -624,7 +763,7 @@ const OPEN_RTB_PROTOCOL = { } if (!utils.isEmpty(aliases)) { - request.ext.prebid.aliases = aliases; + request.ext.prebid.aliases = {...request.ext.prebid.aliases, ...aliases}; } const bidUserIdAsEids = utils.deepAccess(bidRequests, '0.bids.0.userIdAsEids'); @@ -632,6 +771,32 @@ 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); + } + + const multibid = config.getConfig('multibid'); + if (multibid) { + utils.deepSetValue(request, 'ext.prebid.multibid', multibid.reduce((result, i) => { + let obj = {}; + + Object.keys(i).forEach(key => { + obj[key.toLowerCase()] = i[key]; + }); + + result.push(obj); + + return result; + }, [])); + } + if (bidRequests) { if (firstBidRequest.gdprConsent) { // note - gdprApplies & consentString may be undefined in certain use-cases for consentManagement module @@ -656,21 +821,24 @@ const OPEN_RTB_PROTOCOL = { utils.deepSetValue(request, 'regs.coppa', 1); } - const commonFpd = getConfig('fpd') || {}; - if (commonFpd.context) { - utils.deepSetValue(request, 'site.ext.data', commonFpd.context); + const commonFpd = getConfig('ortb2') || {}; + if (commonFpd.site) { + utils.mergeDeep(request, {site: commonFpd.site}); } if (commonFpd.user) { - utils.deepSetValue(request, 'user.ext.data', commonFpd.user); + utils.mergeDeep(request, {user: commonFpd.user}); } addBidderFirstPartyDataToRequest(request); return request; }, - interpretResponse(response, bidderRequests) { + interpretResponse(response, bidderRequests, s2sConfig) { const bids = []; + [['errors', 'serverErrors'], ['responsetimemillis', 'serverResponseTimeMs']] + .forEach(info => getPbsResponseData(bidderRequests, response, info[0], info[1])) + if (response.seatbid) { // a seatbid object contains a `bid` array and a `seat` string response.seatbid.forEach(seatbid => { @@ -685,7 +853,7 @@ const OPEN_RTB_PROTOCOL = { } const cpm = bid.price; - const status = cpm !== 0 ? STATUS.GOOD : STATUS.NO_BID; + const status = cpm !== 0 ? CONSTANTS.STATUS.GOOD : CONSTANTS.STATUS.NO_BID; let bidObject = createBid(status, bidRequest || { bidder: seatbid.seat, src: TYPE @@ -693,6 +861,8 @@ const OPEN_RTB_PROTOCOL = { bidObject.cpm = cpm; + // temporarily leaving attaching it to each bidResponse so no breaking change + // BUT: this is a flat map, so it should be only attached to bidderRequest, a the change above does let serverResponseTimeMs = utils.deepAccess(response, ['ext', 'responsetimemillis', seatbid.seat].join('.')); if (bidRequest && serverResponseTimeMs) { bidRequest.serverResponseTimeMs = serverResponseTimeMs; @@ -728,8 +898,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 @@ -818,11 +988,12 @@ const OPEN_RTB_PROTOCOL = { if (bid.burl) { bidObject.burl = bid.burl; } bidObject.currency = (response.cur) ? response.cur : DEFAULT_S2S_CURRENCY; bidObject.meta = bidObject.meta || {}; + if (bid.ext && bid.ext.dchain) { bidObject.meta.dchain = utils.deepClone(bid.ext.dchain); } if (bid.adomain) { bidObject.meta.advertiserDomains = bid.adomain; } - // TODO: Remove when prebid-server returns ttl and netRevenue - const configTtl = _s2sConfig.defaultTtl || DEFAULT_S2S_TTL; - bidObject.ttl = (bid.ttl) ? bid.ttl : configTtl; + // 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; bids.push({ adUnit: bid.impid, bid: bidObject }); @@ -849,6 +1020,29 @@ function bidWonHandler(bid) { } } +function hasPurpose1Consent(gdprConsent) { + let result = true; + if (gdprConsent) { + if (gdprConsent.gdprApplies && gdprConsent.apiVersion === 2) { + result = !!(utils.deepAccess(gdprConsent, 'vendorData.purpose.consents.1') === true); + } + } + return result; +} + +function getMatchingConsentUrl(urlProp, gdprConsent) { + return hasPurpose1Consent(gdprConsent) ? urlProp.p1Consent : urlProp.noP1Consent; +} + +function getConsentData(bidRequests) { + let gdprConsent, uspConsent; + if (Array.isArray(bidRequests) && bidRequests.length > 0) { + gdprConsent = bidRequests[0].gdprConsent; + uspConsent = bidRequests[0].uspConsent; + } + return { gdprConsent, uspConsent }; +} + /** * Bidder adapter for Prebid Server */ @@ -858,6 +1052,7 @@ export function PrebidServer() { /* Prebid executes this function when the page asks to send out bid requests */ baseAdapter.callBids = function(s2sBidRequest, bidRequests, addBidResponse, done, ajax) { const adUnits = utils.deepClone(s2sBidRequest.ad_units); + let { gdprConsent, uspConsent } = getConsentData(bidRequests); // at this point ad units should have a size array either directly or mapped so filter for that const validAdUnits = adUnits.filter(unit => @@ -870,39 +1065,37 @@ 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 && getMatchingConsentUrl(s2sBidRequest.s2sConfig.syncEndpoint, gdprConsent)) { + let syncBidders = s2sBidRequest.s2sConfig.bidders + .map(bidder => adapterManager.aliasRegistry[bidder] || bidder) + .filter((bidder, index, array) => (array.indexOf(bidder) === index)); - let syncBidders = _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( + getMatchingConsentUrl(s2sBidRequest.s2sConfig.endpoint, gdprConsent), + { + 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 = []; + let { gdprConsent, uspConsent } = getConsentData(bidderRequests); try { result = JSON.parse(response); @@ -910,7 +1103,7 @@ export function PrebidServer() { bids = OPEN_RTB_PROTOCOL.interpretResponse( result, bidderRequests, - requestedBidders + s2sConfig ); bids.forEach(({adUnit, bid}) => { @@ -919,7 +1112,7 @@ export function PrebidServer() { } }); - bidderRequests.forEach(bidderRequest => events.emit(EVENTS.BIDDER_DONE, bidderRequest)); + bidderRequests.forEach(bidderRequest => events.emit(CONSTANTS.EVENTS.BIDDER_DONE, bidderRequest)); } catch (error) { utils.logError(error); } @@ -929,11 +1122,11 @@ export function PrebidServer() { } done(); - doClientSideSyncs(requestedBidders); + doClientSideSyncs(requestedBidders, gdprConsent, uspConsent); } // Listen for bid won to call wurl - events.on(EVENTS.BID_WON, bidWonHandler); + events.on(CONSTANTS.EVENTS.BID_WON, bidWonHandler); return Object.assign(this, { callBids: baseAdapter.callBids, @@ -942,4 +1135,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/prebidmanagerAnalyticsAdapter.js b/modules/prebidmanagerAnalyticsAdapter.js index b98ca864cd5..b9a7d79f991 100644 --- a/modules/prebidmanagerAnalyticsAdapter.js +++ b/modules/prebidmanagerAnalyticsAdapter.js @@ -1,10 +1,12 @@ import {ajaxBuilder} from '../src/ajax.js'; import adapter from '../src/AnalyticsAdapter.js'; import adapterManager from '../src/adapterManager.js'; +import { getStorageManager } from '../src/storageManager.js'; /** * prebidmanagerAnalyticsAdapter.js - analytics adapter for prebidmanager */ +export const storage = getStorageManager(undefined, 'prebidmanager'); const DEFAULT_EVENT_URL = 'https://endpoint.prebidmanager.com/endpoint' const analyticsType = 'endpoint'; const analyticsName = 'Prebid Manager Analytics: '; @@ -82,14 +84,14 @@ function collectUtmTagData() { }); if (newUtm === false) { utmTags.forEach(function (utmKey) { - let itemValue = localStorage.getItem(`pm_${utmKey}`); - if (itemValue.length !== 0) { + let itemValue = storage.getDataFromLocalStorage(`pm_${utmKey}`); + if (itemValue && itemValue.length !== 0) { pmUtmTags[utmKey] = itemValue; } }); } else { utmTags.forEach(function (utmKey) { - localStorage.setItem(`pm_${utmKey}`, pmUtmTags[utmKey]); + storage.setDataInLocalStorage(`pm_${utmKey}`, pmUtmTags[utmKey]); }); } } catch (e) { @@ -99,6 +101,16 @@ function collectUtmTagData() { return pmUtmTags; } +function collectPageInfo() { + const pageInfo = { + domain: window.location.hostname, + } + if (document.referrer) { + pageInfo.referrerDomain = utils.parseUrl(document.referrer).hostname; + } + return pageInfo; +} + function flush() { if (!pmAnalyticsEnabled) { return; @@ -111,6 +123,7 @@ function flush() { bundleId: initOptions.bundleId, events: _eventQueue, utmTags: collectUtmTagData(), + pageInfo: collectPageInfo(), }; ajax( diff --git a/modules/priceFloors.js b/modules/priceFloors.js index eb1f3aed84c..3555fedbf3a 100644 --- a/modules/priceFloors.js +++ b/modules/priceFloors.js @@ -55,7 +55,7 @@ export let _floorDataForAuction = {}; * @summary Simple function to round up to a certain decimal degree */ function roundUp(number, precision) { - return Math.ceil(parseFloat(number) * Math.pow(10, precision)) / Math.pow(10, precision); + return Math.ceil((parseFloat(number) * Math.pow(10, precision)).toFixed(1)) / Math.pow(10, precision); } let referrerHostname; @@ -98,21 +98,23 @@ export function getFirstMatchingFloor(floorData, bidObject, responseObject = {}) let fieldValues = enumeratePossibleFieldValues(utils.deepAccess(floorData, 'schema.fields') || [], bidObject, responseObject); if (!fieldValues.length) return { matchingFloor: floorData.default }; - // look to see iof a request for this context was made already + // look to see if a request for this context was made already let matchingInput = fieldValues.map(field => field[0]).join('-'); // if we already have gotten the matching rule from this matching input then use it! No need to look again let previousMatch = utils.deepAccess(floorData, `matchingInputs.${matchingInput}`); if (previousMatch) { - return previousMatch; + return {...previousMatch}; } let allPossibleMatches = generatePossibleEnumerations(fieldValues, utils.deepAccess(floorData, 'schema.delimiter') || '|'); let matchingRule = find(allPossibleMatches, hashValue => floorData.values.hasOwnProperty(hashValue)); let matchingData = { - matchingFloor: floorData.values[matchingRule] || floorData.default, + floorMin: floorData.floorMin || 0, + floorRuleValue: floorData.values[matchingRule] || floorData.default, matchingData: allPossibleMatches[0], // the first possible match is an "exact" so contains all data relevant for anlaytics adapters matchingRule }; + matchingData.matchingFloor = Math.max(matchingData.floorMin, matchingData.floorRuleValue); // save for later lookup if needed utils.deepSetValue(floorData, `matchingInputs.${matchingInput}`, {...matchingData}); return matchingData; @@ -138,10 +140,10 @@ 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) { - const adjustmentFunction = utils.deepAccess(getGlobal(), `bidderSettings.${bidderName}.bidCpmAdjustment`); +export function getBiddersCpmAdjustment(bidderName, inputCpm, bid = {}) { + const adjustmentFunction = utils.deepAccess(getGlobal(), `bidderSettings.${bidderName}.bidCpmAdjustment`) || utils.deepAccess(getGlobal(), 'bidderSettings.standard.bidCpmAdjustment'); if (adjustmentFunction) { - return parseFloat(adjustmentFunction(inputCpm)); + return parseFloat(adjustmentFunction(inputCpm, {...bid, cpm: inputCpm})); } return parseFloat(inputCpm); } @@ -287,11 +289,14 @@ export function updateAdUnitsForAuction(adUnits, floorData, auctionId) { bid.floorData = { skipped: floorData.skipped, 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 - } + }; }); }); } @@ -336,6 +341,8 @@ export function createFloorsDataForAuction(adUnits, auctionId) { const isSkipped = Math.random() * 100 < parseFloat(auctionSkipRate); resolvedFloorsData.skipped = isSkipped; } + // copy FloorMin to floorData.data + if (resolvedFloorsData.hasOwnProperty('floorMin')) resolvedFloorsData.data.floorMin = resolvedFloorsData.floorMin; // add floorData to bids updateAdUnitsForAuction(adUnits, resolvedFloorsData, auctionId); return resolvedFloorsData; @@ -568,6 +575,7 @@ function addFieldOverrides(overrides) { */ export function handleSetFloorsConfig(config) { _floorsConfig = utils.pick(config, [ + 'floorMin', 'enabled', enabled => enabled !== false, // defaults to true 'auctionDelay', auctionDelay => auctionDelay || 0, 'floorProvider', floorProvider => utils.deepAccess(config, 'data.floorProvider', floorProvider), @@ -623,6 +631,7 @@ function addFloorDataToBid(floorData, floorInfo, bid, adjustedCpm) { bid.floorData = { floorValue: floorInfo.matchingFloor, floorRule: floorInfo.matchingRule, + floorRuleValue: floorInfo.floorRuleValue, floorCurrency: floorData.data.currency, cpmAfterAdjustments: adjustedCpm, enforcements: {...floorData.enforcement}, @@ -682,7 +691,7 @@ export function addBidResponseHook(fn, adUnitCode, bid) { } // ok we got the bid response cpm in our desired currency. Now we need to run the bidders CPMAdjustment function if it exists - adjustedCpm = getBiddersCpmAdjustment(bid.bidderCode, adjustedCpm); + adjustedCpm = getBiddersCpmAdjustment(bid.bidderCode, adjustedCpm, bid); // add necessary data information for analytics adapters / floor providers would possibly need addFloorDataToBid(floorData, floorInfo, bid, adjustedCpm); diff --git a/modules/proxistoreBidAdapter.js b/modules/proxistoreBidAdapter.js index c546337c47f..ff07ee9ede9 100644 --- a/modules/proxistoreBidAdapter.js +++ b/modules/proxistoreBidAdapter.js @@ -1,62 +1,79 @@ - import { registerBidder } from '../src/adapters/bidderFactory.js'; -import { getStorageManager } from '../src/storageManager.js'; const BIDDER_CODE = 'proxistore'; -const storage = getStorageManager(); const PROXISTORE_VENDOR_ID = 418; function _createServerRequest(bidRequests, bidderRequest) { - const sizeIds = []; - + var sizeIds = []; bidRequests.forEach(function (bid) { - const sizeId = { + var sizeId = { id: bid.bidId, sizes: bid.sizes.map(function (size) { return { width: size[0], - height: size[1] + height: size[1], }; - }) + }), + floor: _assignFloor(bid), + segments: _assignSegments(bid), }; sizeIds.push(sizeId); }); - const payload = { + var payload = { auctionId: bidRequests[0].auctionId, transactionId: bidRequests[0].auctionId, bids: sizeIds, website: bidRequests[0].params.website, language: bidRequests[0].params.language, gdpr: { - applies: false - } - }; - const options = { - contentType: 'application/json', - withCredentials: true + applies: false, + consentGiven: false + }, }; if (bidderRequest && bidderRequest.gdprConsent) { - if (typeof bidderRequest.gdprConsent.gdprApplies === 'boolean' && bidderRequest.gdprConsent.gdprApplies) { + const { gdprConsent } = bidderRequest; + if (typeof gdprConsent.gdprApplies === 'boolean' && gdprConsent.gdprApplies) { payload.gdpr.applies = true; } - if (typeof bidderRequest.gdprConsent.consentString === 'string' && bidderRequest.gdprConsent.consentString) { + if (typeof gdprConsent.consentString === 'string' && gdprConsent.consentString) { payload.gdpr.consentString = bidderRequest.gdprConsent.consentString; } - - if (bidderRequest.gdprConsent.vendorData && bidderRequest.gdprConsent.vendorData.vendorConsents && typeof bidderRequest.gdprConsent.vendorData.vendorConsents[PROXISTORE_VENDOR_ID.toString(10)] !== 'undefined') { - payload.gdpr.consentGiven = !!bidderRequest.gdprConsent.vendorData.vendorConsents[PROXISTORE_VENDOR_ID.toString(10)]; + if (gdprConsent.vendorData) { + const {vendorData} = gdprConsent; + const {apiVersion} = gdprConsent; + if (apiVersion === 2 && vendorData.vendor && vendorData.vendor.consents && typeof vendorData.vendor.consents[PROXISTORE_VENDOR_ID.toString(10)] !== 'undefined') { + payload.gdpr.consentGiven = !!vendorData.vendor.consents[PROXISTORE_VENDOR_ID.toString(10)]; + } else if (apiVersion === 1 && vendorData.vendorConsents && typeof vendorData.vendorConsents[PROXISTORE_VENDOR_ID.toString(10)] !== 'undefined') { + payload.gdpr.consentGiven = !!vendorData.vendorConsents[PROXISTORE_VENDOR_ID.toString(10)]; + } } } + const options = { + contentType: 'application/json', + withCredentials: payload.gdpr.consentGiven, + }; + + const endPointUri = payload.gdpr.consentGiven || !payload.gdpr.applies + ? `https://abs.proxistore.com/${payload.language}/v3/rtb/prebid/multi` + : `https://abs.cookieless-proxistore.com/${payload.language}/v3/rtb/prebid/multi`; + return { method: 'POST', - url: bidRequests[0].params.url || 'https://abs.proxistore.com/' + payload.language + '/v3/rtb/prebid/multi', + url: endPointUri, data: JSON.stringify(payload), - options: options + options: options, }; } +function _assignSegments(bid) { + if (bid.ortb2 && bid.ortb2.user && bid.ortb2.user.ext && bid.ortb2.user.ext.data) { + return bid.ortb2.user.ext.data || {segments: [], contextual_categories: {}}; + } + return {segments: [], contextual_categories: {}}; +} + function _createBidResponse(response) { return { requestId: response.requestId, @@ -70,7 +87,7 @@ function _createBidResponse(response) { netRevenue: response.netRevenue, vastUrl: response.vastUrl, vastXml: response.vastXml, - dealId: response.dealId + dealId: response.dealId, }; } /** @@ -81,23 +98,8 @@ function _createBidResponse(response) { */ function isBidRequestValid(bid) { - const hasNoAd = function() { - if (!storage.hasLocalStorage()) { - return false; - } - const pxNoAds = storage.getDataFromLocalStorage(`PX_NoAds_${bid.params.website}`); - if (!pxNoAds) { - return false; - } else { - const storedDate = new Date(pxNoAds); - const now = new Date(); - const diff = Math.abs(storedDate.getTime() - now.getTime()) / 60000; - return diff <= 5; - } - } - return !!(bid.params.website && bid.params.language) && !hasNoAd(); + return !!(bid.params.website && bid.params.language); } - /** * Make a server request from the list of BidRequests. * @@ -107,7 +109,7 @@ function isBidRequestValid(bid) { */ function buildRequests(bidRequests, bidderRequest) { - const request = _createServerRequest(bidRequests, bidderRequest); + var request = _createServerRequest(bidRequests, bidderRequest); return request; } /** @@ -119,34 +121,23 @@ function buildRequests(bidRequests, bidderRequest) { */ function interpretResponse(serverResponse, bidRequest) { - const itemName = `PX_NoAds_${websiteFromBidRequest(bidRequest)}`; - if (serverResponse.body.length > 0) { - storage.removeDataFromLocalStorage(itemName, true); - return serverResponse.body.map(_createBidResponse); - } else { - storage.setDataInLocalStorage(itemName, new Date()); - return []; - } + return serverResponse.body.map(_createBidResponse); } -const websiteFromBidRequest = function(bidR) { - if (bidR.data) { - return JSON.parse(bidR.data).website - } else if (bidR.params.website) { - return bidR.params.website; - } -} +function _assignFloor(bid) { + if (typeof bid.getFloor === 'function') { + var floorInfo = bid.getFloor({ + currency: 'EUR', + mediaType: 'banner', + size: '*', + }); -/** - * Register the user sync pixels which should be dropped after the auction. - * - * @param syncOptions Which user syncs are allowed? - * @param serverResponses List of server's responses. - * @return The user syncs which should be dropped. - */ + if (floorInfo.currency === 'EUR') { + return floorInfo.floor; + } + } -function getUserSyncs(syncOptions, serverResponses) { - return []; + return null; } export const spec = { @@ -154,7 +145,7 @@ export const spec = { isBidRequestValid: isBidRequestValid, buildRequests: buildRequests, interpretResponse: interpretResponse, - getUserSyncs: getUserSyncs + }; registerBidder(spec); diff --git a/modules/pubCommonId.js b/modules/pubCommonId.js index 174fa6ffe6e..427f775c44b 100644 --- a/modules/pubCommonId.js +++ b/modules/pubCommonId.js @@ -212,9 +212,11 @@ export function requestBidHook(next, config) { // into bid requests later. if (adUnits && pubcid) { adUnits.forEach((unit) => { - unit.bids.forEach((bid) => { - Object.assign(bid, {crumbs: {pubcid}}); - }); + if (unit.bids && utils.isArray(unit.bids)) { + unit.bids.forEach((bid) => { + Object.assign(bid, {crumbs: {pubcid}}); + }); + } }); } diff --git a/modules/pubCommonIdSystem.js b/modules/pubCommonIdSystem.js index 9516934de42..95e539a4d6a 100644 --- a/modules/pubCommonIdSystem.js +++ b/modules/pubCommonIdSystem.js @@ -8,12 +8,171 @@ import * as utils from '../src/utils.js'; import {submodule} from '../src/hook.js'; import {getStorageManager} from '../src/storageManager.js'; +import {ajax} from '../src/ajax.js'; +import { uspDataHandler, coppaDataHandler } from '../src/adapterManager.js'; const PUB_COMMON_ID = 'PublisherCommonId'; - const MODULE_NAME = 'pubCommonId'; -const storage = getStorageManager(null, 'pubCommonId'); +const COOKIE = 'cookie'; +const LOCAL_STORAGE = 'html5'; +const SHAREDID_OPT_OUT_VALUE = '00000000000000000000000000'; +const SHAREDID_URL = 'https://id.sharedid.org/id'; +const SHAREDID_SUFFIX = '_sharedid'; +const EXPIRED_COOKIE_DATE = 'Thu, 01 Jan 1970 00:00:01 GMT'; +const SHAREDID_DEFAULT_STATE = false; +const GVLID = 887; + +const storage = getStorageManager(GVLID, 'pubCommonId'); + +/** + * Store sharedid in either cookie or local storage + * @param {Object} config Need config.storage object to derive key, expiry time, and storage type. + * @param {string} value Shareid value to store + */ + +function storeData(config, value) { + try { + if (value) { + const key = config.storage.name + SHAREDID_SUFFIX; + const expiresStr = (new Date(Date.now() + (storage.expires * (60 * 60 * 24 * 1000)))).toUTCString(); + + if (config.storage.type === COOKIE) { + if (storage.cookiesAreEnabled()) { + storage.setCookie(key, value, expiresStr, 'LAX', pubCommonIdSubmodule.domainOverride()); + } + } else if (config.storage.type === LOCAL_STORAGE) { + if (storage.hasLocalStorage()) { + storage.setDataInLocalStorage(`${key}_exp`, expiresStr); + storage.setDataInLocalStorage(key, value); + } + } + } + } catch (error) { + utils.logError(error); + } +} + +/** + * Read sharedid from cookie or local storage + * @param config Need config.storage to derive key and storage type + * @return {string} + */ +function readData(config) { + try { + const key = config.storage.name + SHAREDID_SUFFIX; + if (config.storage.type === COOKIE) { + if (storage.cookiesAreEnabled()) { + return storage.getCookie(key); + } + } else if (config.storage.type === LOCAL_STORAGE) { + if (storage.hasLocalStorage()) { + const expValue = storage.getDataFromLocalStorage(`${key}_exp`); + if (!expValue) { + return storage.getDataFromLocalStorage(key); + } else if ((new Date(expValue)).getTime() - Date.now() > 0) { + return storage.getDataFromLocalStorage(key) + } + } + } + } catch (error) { + utils.logError(error); + } +} + +/** + * Delete sharedid from cookie or local storage + * @param config Need config.storage to derive key and storage type + */ +function delData(config) { + try { + const key = config.storage.name + SHAREDID_SUFFIX; + if (config.storage.type === COOKIE) { + if (storage.cookiesAreEnabled()) { + storage.setCookie(key, '', EXPIRED_COOKIE_DATE); + } + } else if (config.storage.type === LOCAL_STORAGE) { + storage.removeDataFromLocalStorage(`${key}_exp`); + storage.removeDataFromLocalStorage(key); + } + } catch (error) { + utils.logError(error); + } +} + +/** + * setup success and error handler for sharedid callback thru ajax + * @param {string} pubcid Current pubcommon id + * @param {function} callback userId module callback. + * @param {Object} config Need config.storage to derive sharedid storage params + * @return {{success: success, error: error}} + */ + +function handleResponse(pubcid, callback, config) { + return { + success: function (responseBody) { + if (responseBody) { + try { + let responseObj = JSON.parse(responseBody); + utils.logInfo('PubCommonId: Generated SharedId: ' + responseObj.sharedId); + if (responseObj.sharedId) { + if (responseObj.sharedId !== SHAREDID_OPT_OUT_VALUE) { + // Store sharedId locally + storeData(config, responseObj.sharedId); + } else { + // Delete local copy if the user has opted out + delData(config); + } + } + // Pass pubcid even though there is no change in order to trigger decode + callback(pubcid); + } catch (error) { + utils.logError(error); + } + } + }, + error: function (statusText, responseBody) { + utils.logInfo('PubCommonId: failed to get sharedid'); + } + } +} + +/** + * Builds and returns the shared Id URL with attached consent data if applicable + * @param {Object} consentData + * @return {string} + */ +function sharedIdUrl(consentData) { + const usPrivacyString = uspDataHandler.getConsentData(); + let sharedIdUrl = SHAREDID_URL; + if (usPrivacyString && typeof usPrivacyString === 'string') { + sharedIdUrl = `${SHAREDID_URL}?us_privacy=${usPrivacyString}`; + } + if (!consentData || typeof consentData.gdprApplies !== 'boolean' || !consentData.gdprApplies) return sharedIdUrl; + if (usPrivacyString) { + sharedIdUrl = `${sharedIdUrl}&gdpr=1&gdpr_consent=${consentData.consentString}` + return sharedIdUrl; + } + sharedIdUrl = `${SHAREDID_URL}?gdpr=1&gdpr_consent=${consentData.consentString}`; + return sharedIdUrl +} + +/** + * Wraps pixelCallback in order to call sharedid sync + * @param {string} pubcid Pubcommon id value + * @param {function|undefined} pixelCallback fires a pixel to first party server + * @param {Object} config Need config.storage to derive sharedid storage params. + * @return {function(...[*]=)} + */ + +function getIdCallback(pubcid, pixelCallback, config, consentData) { + return function (callback) { + if (typeof pixelCallback === 'function') { + pixelCallback(); + } + ajax(sharedIdUrl(consentData), handleResponse(pubcid, callback, config), undefined, {method: 'GET', withCredentials: true}); + } +} /** @type {Submodule} */ export const pubCommonIdSubmodule = { @@ -22,6 +181,11 @@ export const pubCommonIdSubmodule = { * @type {string} */ name: MODULE_NAME, + /** + * Vendor id of prebid + * @type {Number} + */ + gvlid: GVLID, /** * Return a callback function that calls the pixelUrl with id as a query parameter * @param pixelUrl @@ -46,52 +210,104 @@ export const pubCommonIdSubmodule = { * decode the stored id value for passing to bid requests * @function * @param {string} value + * @param {SubmoduleConfig} config * @returns {{pubcid:string}} */ - decode(value) { - return { 'pubcid': value } + decode(value, config) { + const idObj = {'pubcid': value}; + const {params: {enableSharedId = SHAREDID_DEFAULT_STATE} = {}} = config; + + if (enableSharedId) { + const sharedId = readData(config); + if (sharedId) idObj['sharedid'] = {id: sharedId}; + } + + return idObj; }, /** * performs action to obtain id * @function - * @param {SubmoduleParams} [configParams] + * @param {SubmoduleConfig} [config] Config object with params and storage properties + * @param {Object} consentData + * @param {string} storedId Existing pubcommon id * @returns {IdResponse} */ - getId: function ({create = true, pixelUrl} = {}) { - try { - if (typeof window[PUB_COMMON_ID] === 'object') { - // If the page includes its own pubcid module, then save a copy of id. - return {id: window[PUB_COMMON_ID].getId()}; - } - } catch (e) { + getId: function (config = {}, consentData, storedId) { + const coppa = coppaDataHandler.getCoppa(); + if (coppa) { + utils.logInfo('PubCommonId: IDs not provided for coppa requests, exiting PubCommonId'); + return; } + const {params: {create = true, pixelUrl, enableSharedId = SHAREDID_DEFAULT_STATE} = {}} = config; + let newId = storedId; + if (!newId) { + try { + if (typeof window[PUB_COMMON_ID] === 'object') { + // If the page includes its own pubcid module, then save a copy of id. + newId = window[PUB_COMMON_ID].getId(); + } + } catch (e) { + } - const newId = (create && utils.hasDeviceAccess()) ? utils.generateUUID() : undefined; - return { - id: newId, - callback: this.makeCallback(pixelUrl, newId) + if (!newId) newId = (create && utils.hasDeviceAccess()) ? utils.generateUUID() : undefined; } + + const pixelCallback = this.makeCallback(pixelUrl, newId); + const combinedCallback = enableSharedId ? getIdCallback(newId, pixelCallback, config, consentData) : pixelCallback; + + return {id: newId, callback: combinedCallback}; }, /** - * performs action to extend an id + * performs action to extend an id. There are generally two ways to extend the expiration time + * of stored id: using pixelUrl or return the id and let main user id module write it again with + * the new expiration time. + * + * PixelUrl, if defined, should point back to a first party domain endpoint. On the server + * side, there is either a plugin, or customized logic to read and write back the pubcid cookie. + * The extendId function itself should return only the callback, and not the id itself to avoid + * having the script-side overwriting server-side. This applies to both pubcid and sharedid. + * + * On the other hand, if there is no pixelUrl, then the extendId should return storedId so that + * its expiration time is updated. Sharedid, however, will have to be updated by this submodule + * separately. + * * @function - * @param {SubmoduleParams} [configParams] + * @param {SubmoduleParams} [config] + * @param {ConsentData|undefined} consentData * @param {Object} storedId existing id * @returns {IdResponse|undefined} */ - extendId: function({extend = false, pixelUrl} = {}, storedId) { - try { - if (typeof window[PUB_COMMON_ID] === 'object') { - // If the page includes its onw pubcid module, then there is nothing to do. - return; - } - } catch (e) { + extendId: function(config = {}, consentData, storedId) { + const coppa = coppaDataHandler.getCoppa(); + if (coppa) { + utils.logInfo('PubCommonId: IDs not provided for coppa requests, exiting PubCommonId'); + return; } + const {params: {extend = false, pixelUrl, enableSharedId = SHAREDID_DEFAULT_STATE} = {}} = config; if (extend) { - // When extending, only one of response fields is needed - const callback = this.makeCallback(pixelUrl, storedId); - return callback ? {callback: callback} : {id: storedId}; + try { + if (typeof window[PUB_COMMON_ID] === 'object') { + if (enableSharedId) { + // If the page includes its own pubcid module, then there is nothing to do + // except to update sharedid's expiration time + storeData(config, readData(config)); + } + return; + } + } catch (e) { + } + + if (pixelUrl) { + const callback = this.makeCallback(pixelUrl, storedId); + return {callback: callback}; + } else { + if (enableSharedId) { + // Update with the same value to extend expiration time + storeData(config, readData(config)); + } + return {id: storedId}; + } } }, @@ -103,16 +319,19 @@ export const pubCommonIdSubmodule = { domainOverride: function () { const domainElements = document.domain.split('.'); const cookieName = `_gd${Date.now()}`; - for (let i = 0, topDomain; i < domainElements.length; i++) { + for (let i = 0, topDomain, testCookie; i < domainElements.length; i++) { const nextDomain = domainElements.slice(i).join('.'); // write test cookie storage.setCookie(cookieName, '1', undefined, undefined, nextDomain); // read test cookie to verify domain was valid - if (storage.getCookie(cookieName) === '1') { - // delete test cookie - storage.setCookie(cookieName, '', 'Thu, 01 Jan 1970 00:00:01 GMT', undefined, nextDomain); + testCookie = storage.getCookie(cookieName); + + // delete test cookie + storage.setCookie(cookieName, '', 'Thu, 01 Jan 1970 00:00:01 GMT', undefined, nextDomain); + + if (testCookie === '1') { // cookie was written successfully using test domain so the topDomain is updated topDomain = nextDomain; } else { diff --git a/modules/pubProvidedIdSystem.js b/modules/pubProvidedIdSystem.js new file mode 100644 index 00000000000..0b2175f57cb --- /dev/null +++ b/modules/pubProvidedIdSystem.js @@ -0,0 +1,54 @@ +/** + * This module adds Publisher Provided ids support to the User ID module + * The {@link module:modules/userId} module is required. + * @module modules/pubProvidedSystem + * @requires module:modules/userId + */ + +import {submodule} from '../src/hook.js'; +import * as utils from '../src/utils.js'; + +const MODULE_NAME = 'pubProvidedId'; + +/** @type {Submodule} */ +export const pubProvidedIdSubmodule = { + + /** + * used to link submodule with config + * @type {string} + */ + name: MODULE_NAME, + + /** + * decode the stored id value for passing to bid request + * @function + * @param {string} value + * @returns {{pubProvidedId: array}} or undefined if value doesn't exists + */ + decode(value) { + const res = value ? {pubProvidedId: value} : undefined; + utils.logInfo('PubProvidedId: Decoded value ' + JSON.stringify(res)); + return res; + }, + + /** + * performs action to obtain id and return a value. + * @function + * @param {SubmoduleConfig} [config] + * @returns {{id: array}} + */ + getId(config) { + const configParams = (config && config.params) || {}; + let res = []; + if (utils.isArray(configParams.eids)) { + res = res.concat(configParams.eids); + } + if (typeof configParams.eidsFunction === 'function') { + res = res.concat(configParams.eidsFunction()); + } + return {id: res}; + } +}; + +// Register submodule for userId +submodule('userId', pubProvidedIdSubmodule); diff --git a/modules/pubgeniusBidAdapter.js b/modules/pubgeniusBidAdapter.js index 05f18f99a9a..89dea545434 100644 --- a/modules/pubgeniusBidAdapter.js +++ b/modules/pubgeniusBidAdapter.js @@ -1,26 +1,27 @@ import { registerBidder } from '../src/adapters/bidderFactory.js'; import { ajax } from '../src/ajax.js'; import { config } from '../src/config.js'; -import { BANNER } from '../src/mediaTypes.js'; +import { BANNER, VIDEO } from '../src/mediaTypes.js'; import { deepAccess, deepSetValue, inIframe, isArrayOfNums, + isFn, isInteger, - isNumber, isStr, logError, parseQueryStringParameters, + pick, } from '../src/utils.js'; -const BIDDER_VERSION = '1.0.0'; +const BIDDER_VERSION = '1.1.0'; const BASE_URL = 'https://ortb.adpearl.io'; export const spec = { code: 'pubgenius', - supportedMediaTypes: [ BANNER ], + supportedMediaTypes: [ BANNER, VIDEO ], isBidRequestValid(bid) { const adUnitId = bid.params.adUnitId; @@ -29,15 +30,20 @@ export const spec = { return false; } - const sizes = deepAccess(bid, 'mediaTypes.banner.sizes'); - return !!(sizes && sizes.length) && sizes.every(size => isArrayOfNums(size, 2)); + const { mediaTypes } = bid; + + if (mediaTypes.banner) { + return isValidBanner(mediaTypes.banner); + } + + return isValidVideo(mediaTypes.video, bid.params.video); }, buildRequests: function (bidRequests, bidderRequest) { const data = { id: bidderRequest.auctionId, imp: bidRequests.map(buildImp), - tmax: config.getConfig('bidderTimeout'), + tmax: bidderRequest.timeout, ext: { pbadapter: { version: BIDDER_VERSION, @@ -141,19 +147,69 @@ export const spec = { }, }; +function buildVideoParams(videoMediaType, videoParams) { + videoMediaType = videoMediaType || {}; + const params = pick(videoMediaType, [ + 'mimes', + 'minduration', + 'maxduration', + 'protocols', + 'startdelay', + 'placement', + 'skip', + 'skipafter', + 'minbitrate', + 'maxbitrate', + 'delivery', + 'playbackmethod', + 'api', + 'linearity', + ]); + + switch (videoMediaType.context) { + case 'instream': + params.placement = 1; + break; + case 'outstream': + params.placement = 2; + break; + default: + break; + } + + if (videoMediaType.playerSize) { + params.w = videoMediaType.playerSize[0][0]; + params.h = videoMediaType.playerSize[0][1]; + } + + return Object.assign(params, videoParams); +} + function buildImp(bid) { const imp = { id: bid.bidId, - banner: { - format: deepAccess(bid, 'mediaTypes.banner.sizes').map(size => ({ w: size[0], h: size[1] })), - topframe: numericBoolean(!inIframe()), - }, tagid: String(bid.params.adUnitId), }; - const bidFloor = bid.params.bidFloor; - if (isNumber(bidFloor)) { - imp.bidfloor = bidFloor; + if (bid.mediaTypes.banner) { + imp.banner = { + format: bid.mediaTypes.banner.sizes.map(size => ({ w: size[0], h: size[1] })), + topframe: numericBoolean(!inIframe()), + }; + } else { + imp.video = buildVideoParams(bid.mediaTypes.video, bid.params.video); + } + + if (isFn(bid.getFloor)) { + const { floor } = bid.getFloor({ + mediaType: bid.mediaTypes.banner ? 'banner' : 'video', + size: '*', + currency: 'USD', + }); + + if (floor) { + imp.bidfloor = floor; + } } const pos = bid.params.position; @@ -197,7 +253,6 @@ function interpretBid(bid) { cpm: bid.price, width: bid.w, height: bid.h, - ad: bid.adm, ttl: bid.exp, creativeId: bid.crid, netRevenue: true, @@ -209,6 +264,24 @@ function interpretBid(bid) { }; } + const pbadapter = deepAccess(bid, 'ext.pbadapter') || {}; + switch (pbadapter.mediaType) { + case 'video': + if (bid.nurl) { + bidResponse.vastUrl = bid.nurl; + } + + if (bid.adm) { + bidResponse.vastXml = bid.adm; + } + + bidResponse.mediaType = VIDEO; + break; + default: // banner by default + bidResponse.ad = bid.adm; + break; + } + return bidResponse; } @@ -221,4 +294,22 @@ function getBaseUrl() { return (pubg && pubg.endpoint) || BASE_URL; } +function isValidSize(size) { + return isArrayOfNums(size, 2) && size[0] > 0 && size[1] > 0; +} + +function isValidBanner(banner) { + const sizes = banner.sizes; + return !!(sizes && sizes.length) && sizes.every(isValidSize); +} + +function isValidVideo(videoMediaType, videoParams) { + const params = buildVideoParams(videoMediaType, videoParams); + + return !!(params.placement && + isValidSize([params.w, params.h]) && + params.mimes && params.mimes.length && + isArrayOfNums(params.protocols) && params.protocols.length); +} + registerBidder(spec); diff --git a/modules/pubgeniusBidAdapter.md b/modules/pubgeniusBidAdapter.md index 66851af9c3f..2a10b421de2 100644 --- a/modules/pubgeniusBidAdapter.md +++ b/modules/pubgeniusBidAdapter.md @@ -12,8 +12,6 @@ Module that connects to pubGENIUS's demand sources # Test Parameters -Test bids have $0.01 CPM by default. Use `bidFloor` in bidder params to control CPM for testing purposes. - ``` var adUnits = [ { @@ -45,12 +43,43 @@ var adUnits = [ bidder: 'pubgenius', params: { adUnitId: '1000', - bidFloor: 0.5, test: true } } ] - } + }, + { + code: 'test-video', + mediaTypes: { + video: { + context: 'instream', + playerSize: [640, 360], + mimes: ['video/mp4'], + protocols: [3], + } + }, + bids: [ + { + bidder: 'pubgenius', + params: { + adUnitId: '1001', + test: true, + + // Other video parameters can be put here as in OpenRTB v2.5 spec. + // This overrides the same parameters in mediaTypes.video. + video: { + placement: 1, + + // w and h overrides mediaTypes.video.playerSize. + w: 640, + h: 360, + + skip: 1 + } + } + } + ] + }, ]; ``` diff --git a/modules/pubmaticAnalyticsAdapter.js b/modules/pubmaticAnalyticsAdapter.js index d94c098db5d..c7eeaf87fdc 100755 --- a/modules/pubmaticAnalyticsAdapter.js +++ b/modules/pubmaticAnalyticsAdapter.js @@ -152,6 +152,7 @@ function parseBidResponse(bid) { 'mediaType', 'params', 'mi', + 'regexPattern', () => bid.regexPattern || undefined, 'partnerImpId', // partner impression ID 'dimensions', () => utils.pick(bid, [ 'width', @@ -166,6 +167,35 @@ function getDomainFromUrl(url) { return a.hostname; } +function getDevicePlatform() { + var deviceType = 3; + try { + var ua = navigator.userAgent; + if (ua && utils.isStr(ua) && ua.trim() != '') { + ua = ua.toLowerCase().trim(); + var isMobileRegExp = new RegExp('(mobi|tablet|ios).*'); + if (ua.match(isMobileRegExp)) { + deviceType = 2; + } else { + deviceType = 1; + } + } + } catch (ex) {} + return deviceType; +} + +function getValueForKgpv(bid, adUnitId) { + if (bid.params.regexPattern) { + return bid.params.regexPattern; + } else if (bid.bidResponse && bid.bidResponse.regexPattern) { + return bid.bidResponse.regexPattern; + } else if (bid.params.kgpv) { + return bid.params.kgpv; + } else { + return adUnitId; + } +} + function gatherPartnerBidsForAdUnitForLogger(adUnit, adUnitId, highestBid) { highestBid = (highestBid && highestBid.length > 0) ? highestBid[0] : null; return Object.keys(adUnit.bids).reduce(function(partnerBids, bidId) { @@ -174,7 +204,7 @@ function gatherPartnerBidsForAdUnitForLogger(adUnit, adUnitId, highestBid) { 'pn': bid.bidder, 'bidid': bid.bidId, 'db': bid.bidResponse ? 0 : 1, - 'kgpv': bid.params.kgpv ? bid.params.kgpv : adUnitId, + 'kgpv': getValueForKgpv(bid, adUnitId), 'kgpsv': bid.params.kgpv ? bid.params.kgpv : adUnitId, 'psz': bid.bidResponse ? (bid.bidResponse.dimensions.width + 'x' + bid.bidResponse.dimensions.height) : '0x0', 'eg': bid.bidResponse ? bid.bidResponse.bidGrossCpmUSD : 0, @@ -220,6 +250,7 @@ function executeBidsLoggerCall(e, highestCpmBids) { outputObj['tst'] = Math.round((new window.Date()).getTime() / 1000); outputObj['pid'] = '' + profileId; outputObj['pdvid'] = '' + profileVersionId; + outputObj['dvc'] = {'plt': getDevicePlatform()}; outputObj['tgid'] = (function() { var testGroupId = parseInt(config.getConfig('testGroupId') || 0); if (testGroupId <= 15 && testGroupId >= 0) { @@ -228,13 +259,6 @@ function executeBidsLoggerCall(e, highestCpmBids) { return 0; })(); - // GDPR support - if (auctionCache.gdprConsent) { - outputObj['cns'] = auctionCache.gdprConsent.consentString || ''; - outputObj['gdpr'] = auctionCache.gdprConsent.gdprApplies === true ? 1 : 0; - pixelURL += '&gdEn=1'; - } - outputObj.s = Object.keys(auctionCache.adUnitCodes).reduce(function(slotsArray, adUnitId) { let adUnit = auctionCache.adUnitCodes[adUnitId]; let slotObject = { @@ -275,7 +299,7 @@ function executeBidWonLoggerCall(auctionId, adUnitId) { pixelURL += '&pn=' + enc(winningBid.bidder); pixelURL += '&en=' + enc(winningBid.bidResponse.bidPriceUSD); pixelURL += '&eg=' + enc(winningBid.bidResponse.bidGrossCpmUSD); - pixelURL += '&kgpv=' + enc(winningBid.params.kgpv || adUnitId); + pixelURL += '&kgpv=' + enc(getValueForKgpv(winningBid, adUnitId)); pixelURL += '&piid=' + enc(winningBid.bidResponse.partnerImpId || EMPTY_STRING); ajax( pixelURL, @@ -307,7 +331,6 @@ function auctionInitHandler(args) { } function bidRequestedHandler(args) { - cache.auctions[args.auctionId].gdprConsent = args.gdprConsent || undefined; args.bids.forEach(function(bid) { if (!cache.auctions[args.auctionId].adUnitCodes.hasOwnProperty(bid.adUnitCode)) { cache.auctions[args.auctionId].adUnitCodes[bid.adUnitCode] = { diff --git a/modules/pubmaticBidAdapter.js b/modules/pubmaticBidAdapter.js index d21854a57c4..ff934204b43 100644 --- a/modules/pubmaticBidAdapter.js +++ b/modules/pubmaticBidAdapter.js @@ -101,12 +101,17 @@ const NATIVE_MINIMUM_REQUIRED_IMAGE_ASSETS = [ } ] -const NET_REVENUE = false; +const NET_REVENUE = true; const dealChannelValues = { 1: 'PMP', 5: 'PREF', 6: 'PMPG' }; + +const FLOC_FORMAT = { + 'EID': 1, + 'SEGMENT': 2 +} // BB stands for Blue BillyWig const BB_RENDERER = { bootstrapPlayer: function(bid) { @@ -204,6 +209,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 ''; } @@ -642,6 +650,8 @@ function _createImpressionObject(bid, conf) { impObj.banner = bannerObj; } + _addImpressionFPD(impObj, bid); + _addFloorFromFloorModule(impObj, bid); return impObj.hasOwnProperty(BANNER) || @@ -649,6 +659,36 @@ function _createImpressionObject(bid, conf) { impObj.hasOwnProperty(VIDEO) ? impObj : UNDEFINED; } +function _addImpressionFPD(imp, bid) { + const ortb2 = {...utils.deepAccess(bid, 'ortb2Imp.ext.data')}; + Object.keys(ortb2).forEach(prop => { + /** + * Prebid AdSlot + * @type {(string|undefined)} + */ + if (prop === 'pbadslot') { + if (typeof ortb2[prop] === 'string' && ortb2[prop]) utils.deepSetValue(imp, 'ext.data.pbadslot', ortb2[prop]); + } else if (prop === 'adserver') { + /** + * Copy GAM AdUnit and Name to imp + */ + ['name', 'adslot'].forEach(name => { + /** @type {(string|undefined)} */ + const value = utils.deepAccess(ortb2, `adserver.${name}`); + if (typeof value === 'string' && value) { + utils.deepSetValue(imp, `ext.data.adserver.${name.toLowerCase()}`, value); + // copy GAM ad unit id as imp[].ext.dfp_ad_unit_code + if (name === 'adslot') { + utils.deepSetValue(imp, `ext.dfp_ad_unit_code`, value); + } + } + }); + } else { + utils.deepSetValue(imp, `ext.data.${prop}`, ortb2[prop]); + } + }); +} + function _addFloorFromFloorModule(impObj, bid) { let bidFloor = -1; // get lowest floor from floorModule @@ -673,8 +713,67 @@ function _addFloorFromFloorModule(impObj, bid) { impObj.bidfloor = ((!isNaN(bidFloor) && bidFloor > 0) ? bidFloor : UNDEFINED); } +function _getFlocId(validBidRequests, flocFormat) { + var flocIdObject = null; + var flocId = utils.deepAccess(validBidRequests, '0.userId.flocId'); + if (flocId && flocId.id) { + switch (flocFormat) { + case FLOC_FORMAT.SEGMENT: + flocIdObject = { + id: 'FLOC', + name: 'FLOC', + ext: { + ver: flocId.version + }, + segment: [{ + id: flocId.id, + name: 'chrome.com', + value: flocId.id.toString() + }] + } + break; + case FLOC_FORMAT.EID: + default: + flocIdObject = { + source: 'chrome.com', + uids: [ + { + atype: 1, + id: flocId.id, + ext: { + ver: flocId.version + } + }, + ] + } + break; + } + } + return flocIdObject; +} + +function _handleFlocId(payload, validBidRequests) { + var flocObject = _getFlocId(validBidRequests, FLOC_FORMAT.SEGMENT); + if (flocObject) { + if (!payload.user) { + payload.user = {}; + } + if (!payload.user.data) { + payload.user.data = []; + } + payload.user.data.push(flocObject); + } +} + function _handleEids(payload, validBidRequests) { - const bidUserIdAsEids = utils.deepAccess(validBidRequests, '0.userIdAsEids'); + let bidUserIdAsEids = utils.deepAccess(validBidRequests, '0.userIdAsEids'); + let flocObject = _getFlocId(validBidRequests, FLOC_FORMAT.EID); + if (flocObject) { + if (!bidUserIdAsEids) { + bidUserIdAsEids = []; + } + bidUserIdAsEids.push(flocObject); + } if (utils.isArray(bidUserIdAsEids) && bidUserIdAsEids.length > 0) { utils.deepSetValue(payload, 'user.eids', bidUserIdAsEids); } @@ -847,7 +946,7 @@ export const spec = { isBidRequestValid: bid => { if (bid && bid.params) { if (!utils.isStr(bid.params.publisherId)) { - utils.logWarn(LOG_WARN_PREFIX + 'Error: publisherId is mandatory and cannot be numeric. Call to OpenBid will not be sent for ad unit: ' + JSON.stringify(bid)); + utils.logWarn(LOG_WARN_PREFIX + 'Error: publisherId is mandatory and cannot be numeric (wrap it in quotes in your config). Call to OpenBid will not be sent for ad unit: ' + JSON.stringify(bid)); return false; } // video ad validation @@ -861,8 +960,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 { @@ -938,7 +1037,7 @@ export const spec = { payload.ext.wrapper = {}; payload.ext.wrapper.profile = parseInt(conf.profId) || UNDEFINED; payload.ext.wrapper.version = parseInt(conf.verId) || UNDEFINED; - payload.ext.wrapper.wiid = conf.wiid || UNDEFINED; + payload.ext.wrapper.wiid = conf.wiid || bidderRequest.auctionId; // eslint-disable-next-line no-undef payload.ext.wrapper.wv = $$REPO_AND_VERSION$$; payload.ext.wrapper.transactionId = conf.transactionId; @@ -994,6 +1093,15 @@ export const spec = { _handleDealCustomTargetings(payload, dctrArr, validBidRequests); _handleEids(payload, validBidRequests); _blockedIabCategoriesValidation(payload, blockedIabCategories); + _handleFlocId(payload, validBidRequests); + // First Party Data + const commonFpd = config.getConfig('ortb2') || {}; + if (commonFpd.site) { + utils.mergeDeep(payload, {site: commonFpd.site}); + } + if (commonFpd.user) { + utils.mergeDeep(payload, {user: commonFpd.user}); + } // Note: Do not move this block up // if site object is set in Prebid config then we need to copy required fields from site into app and unset the site object diff --git a/modules/pubmaticBidAdapter.md b/modules/pubmaticBidAdapter.md index cd9398477f4..e9d93d79758 100644 --- a/modules/pubmaticBidAdapter.md +++ b/modules/pubmaticBidAdapter.md @@ -24,9 +24,9 @@ var adUnits = [ bids: [{ bidder: 'pubmatic', params: { - publisherId: '156209', // required - 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/pubwiseAnalyticsAdapter.js b/modules/pubwiseAnalyticsAdapter.js index 915aeb58f99..fe217454b88 100644 --- a/modules/pubwiseAnalyticsAdapter.js +++ b/modules/pubwiseAnalyticsAdapter.js @@ -4,7 +4,6 @@ import adapterManager from '../src/adapterManager.js'; import CONSTANTS from '../src/constants.json'; import { getStorageManager } from '../src/storageManager.js'; const utils = require('../src/utils.js'); - const storage = getStorageManager(); /**** @@ -17,30 +16,54 @@ const storage = getStorageManager(); pbjs.enableAnalytics({ provider: 'pubwise', options: { - site: 'test-test-test-test', - endpoint: 'https://api.pubwise.io/api/v4/event/add/', + site: 'b1ccf317-a6fc-428d-ba69-0c9c208aa61c' } }); - */ + +Changes in 4.0 Version +4.0.1 - Initial Version for Prebid 4.x, adds activationId, adds additiona testing, removes prebid global in favor of a prebid.version const +4.0.2 - Updates to include dedicated default site to keep everything from getting rate limited + +*/ const analyticsType = 'endpoint'; -const analyticsName = 'PubWise Analytics: '; -let defaultUrl = 'https://api.pubwise.io/api/v4/event/default/'; -let pubwiseVersion = '3.0'; -let pubwiseSchema = 'AVOCET'; -let configOptions = {site: '', endpoint: 'https://api.pubwise.io/api/v4/event/default/', debug: ''}; +const analyticsName = 'PubWise:'; +const prebidVersion = '$prebid.version$'; +let pubwiseVersion = '4.0.1'; +let configOptions = {site: '', endpoint: 'https://api.pubwise.io/api/v5/event/add/', debug: null}; let pwAnalyticsEnabled = false; let utmKeys = {utm_source: '', utm_medium: '', utm_campaign: '', utm_term: '', utm_content: ''}; +let sessionData = {sessionId: '', activationId: ''}; +let pwNamespace = 'pubwise'; +let pwEvents = []; +let metaData = {}; +let auctionEnded = false; +let sessTimeout = 60 * 30 * 1000; // 30 minutes, G Analytics default session length +let sessName = 'sess_id'; +let sessTimeoutName = 'sess_timeout'; -function markEnabled() { - utils.logInfo(`${analyticsName}Enabled`, configOptions); - pwAnalyticsEnabled = true; +function enrichWithSessionInfo(dataBag) { + try { + // eslint-disable-next-line + // console.log(sessionData); + dataBag['session_id'] = sessionData.sessionId; + dataBag['activation_id'] = sessionData.activationId; + } catch (e) { + dataBag['error_sess'] = 1; + } + + return dataBag; } function enrichWithMetrics(dataBag) { try { + if (window.PREBID_TIMEOUT) { + dataBag['target_timeout'] = window.PREBID_TIMEOUT; + } else { + dataBag['target_timeout'] = 'NA'; + } dataBag['pw_version'] = pubwiseVersion; - dataBag['pbjs_version'] = $$PREBID_GLOBAL$$.version; + dataBag['pbjs_version'] = prebidVersion; dataBag['debug'] = configOptions.debug; } catch (e) { dataBag['error_metric'] = 1; @@ -54,7 +77,7 @@ function enrichWithUTM(dataBag) { try { for (let prop in utmKeys) { utmKeys[prop] = utils.getParameterByName(prop); - if (utmKeys[prop] != '') { + if (utmKeys[prop]) { newUtm = true; dataBag[prop] = utmKeys[prop]; } @@ -62,70 +85,253 @@ function enrichWithUTM(dataBag) { if (newUtm === false) { for (let prop in utmKeys) { - let itemValue = storage.getDataFromLocalStorage(`pw-${prop}`); - if (itemValue.length !== 0) { + let itemValue = storage.getDataFromLocalStorage(setNamespace(prop)); + if (itemValue !== null && typeof itemValue !== 'undefined' && itemValue.length !== 0) { dataBag[prop] = itemValue; } } } else { for (let prop in utmKeys) { - storage.setDataInLocalStorage(`pw-${prop}`, utmKeys[prop]); + storage.setDataInLocalStorage(setNamespace(prop), utmKeys[prop]); } } } catch (e) { - utils.logInfo(`${analyticsName}Error`, e); + pwInfo(`Error`, e); dataBag['error_utm'] = 1; } return dataBag; } -function sendEvent(eventType, data) { - utils.logInfo(`${analyticsName}Event ${eventType} ${pwAnalyticsEnabled}`, data); +function expireUtmData() { + pwInfo(`Session Expiring UTM Data`); + for (let prop in utmKeys) { + storage.removeDataFromLocalStorage(setNamespace(prop)); + } +} + +function enrichWithCustomSegments(dataBag) { + // c_script_type: '', c_slot1: '', c_slot2: '', c_slot3: '', c_slot4: '' + if (configOptions.custom) { + if (configOptions.custom.c_script_type) { + dataBag['c_script_type'] = configOptions.custom.c_script_type; + } + + if (configOptions.custom.c_host) { + dataBag['c_host'] = configOptions.custom.c_host; + } + + if (configOptions.custom.c_slot1) { + dataBag['c_slot1'] = configOptions.custom.c_slot1; + } + + if (configOptions.custom.c_slot2) { + dataBag['c_slot2'] = configOptions.custom.c_slot2; + } + + if (configOptions.custom.c_slot3) { + dataBag['c_slot3'] = configOptions.custom.c_slot3; + } + + if (configOptions.custom.c_slot4) { + dataBag['c_slot4'] = configOptions.custom.c_slot4; + } + } + + return dataBag; +} + +function setNamespace(itemText) { + return pwNamespace.concat('_' + itemText); +} + +function localStorageSessTimeoutName() { + return setNamespace(sessTimeoutName); +} + +function localStorageSessName() { + return setNamespace(sessName); +} + +function extendUserSessionTimeout() { + storage.setDataInLocalStorage(localStorageSessTimeoutName(), Date.now().toString()); +} + +function userSessionID() { + return storage.getDataFromLocalStorage(localStorageSessName()) ? localStorage.getItem(localStorageSessName()) : ''; +} + +function sessionExpired() { + let sessLastTime = storage.getDataFromLocalStorage(localStorageSessTimeoutName()); + return (Date.now() - parseInt(sessLastTime)) > sessTimeout; +} + +function flushEvents() { + if (pwEvents.length > 0) { + let dataBag = {metaData: metaData, eventList: pwEvents.splice(0)}; // put all the events together with the metadata and send + ajax(configOptions.endpoint, (result) => pwInfo(`Result`, result), JSON.stringify(dataBag)); + } +} + +function isIngestedEvent(eventType) { + const ingested = [ + CONSTANTS.EVENTS.AUCTION_INIT, + CONSTANTS.EVENTS.BID_REQUESTED, + CONSTANTS.EVENTS.BID_RESPONSE, + CONSTANTS.EVENTS.BID_WON, + CONSTANTS.EVENTS.BID_TIMEOUT, + CONSTANTS.EVENTS.AD_RENDER_FAILED, + CONSTANTS.EVENTS.TCF2_ENFORCEMENT + ]; + return ingested.indexOf(eventType) !== -1; +} - // put the typical items in the data bag - let dataBag = { - eventType: eventType, - args: data, - target_site: configOptions.site, - pubwiseSchema: pubwiseSchema, - debug: configOptions.debug ? 1 : 0, - }; +function markEnabled() { + pwInfo(`Enabled`, configOptions); + pwAnalyticsEnabled = true; + setInterval(flushEvents, 100); +} + +function pwInfo(info, context) { + utils.logInfo(`${analyticsName} ` + info, context); +} - dataBag = enrichWithMetrics(dataBag); - // for certain events, track additional info - if (eventType == CONSTANTS.EVENTS.AUCTION_INIT) { - dataBag = enrichWithUTM(dataBag); +function filterBidResponse(data) { + let modified = Object.assign({}, data); + // clean up some properties we don't track in public version + if (typeof modified.ad !== 'undefined') { + modified.ad = ''; } + if (typeof modified.adUrl !== 'undefined') { + modified.adUrl = ''; + } + if (typeof modified.adserverTargeting !== 'undefined') { + modified.adserverTargeting = ''; + } + if (typeof modified.ts !== 'undefined') { + modified.ts = ''; + } + // clean up a property to make simpler + if (typeof modified.statusMessage !== 'undefined' && modified.statusMessage === 'Bid returned empty or error response') { + modified.statusMessage = 'eoe'; + } + modified.auctionEnded = auctionEnded; + return modified; +} - ajax(configOptions.endpoint, (result) => utils.logInfo(`${analyticsName}Result`, result), JSON.stringify(dataBag)); +function filterAuctionInit(data) { + let modified = Object.assign({}, data); + + modified.refererInfo = {}; + // handle clean referrer, we only need one + if (typeof modified.bidderRequests !== 'undefined' && typeof modified.bidderRequests[0] !== 'undefined' && typeof modified.bidderRequests[0].refererInfo !== 'undefined') { + modified.refererInfo = modified.bidderRequests[0].refererInfo; + } + + if (typeof modified.adUnitCodes !== 'undefined') { + delete modified.adUnitCodes; + } + if (typeof modified.adUnits !== 'undefined') { + delete modified.adUnits; + } + if (typeof modified.bidderRequests !== 'undefined') { + delete modified.bidderRequests; + } + if (typeof modified.bidsReceived !== 'undefined') { + delete modified.bidsReceived; + } + if (typeof modified.config !== 'undefined') { + delete modified.config; + } + if (typeof modified.noBids !== 'undefined') { + delete modified.noBids; + } + if (typeof modified.winningBids !== 'undefined') { + delete modified.winningBids; + } + + return modified; } -let pubwiseAnalytics = Object.assign(adapter( - { - defaultUrl, - analyticsType - }), -{ +let pubwiseAnalytics = Object.assign(adapter({analyticsType}), { // Override AnalyticsAdapter functions by supplying custom methods track({eventType, args}) { - sendEvent(eventType, args); + this.handleEvent(eventType, args); } }); +pubwiseAnalytics.handleEvent = function(eventType, data) { + // we log most events, but some are information + if (isIngestedEvent(eventType)) { + pwInfo(`Emitting Event ${eventType} ${pwAnalyticsEnabled}`, data); + + // record metadata + metaData = { + target_site: configOptions.site, + debug: configOptions.debug ? 1 : 0, + }; + metaData = enrichWithSessionInfo(metaData); + metaData = enrichWithMetrics(metaData); + metaData = enrichWithUTM(metaData); + metaData = enrichWithCustomSegments(metaData); + + // add data on init to the metadata container + if (eventType === CONSTANTS.EVENTS.AUCTION_INIT) { + data = filterAuctionInit(data); + } else if (eventType === CONSTANTS.EVENTS.BID_RESPONSE) { + data = filterBidResponse(data); + } + + // add all ingested events + pwEvents.push({ + eventType: eventType, + args: data + }); + } else { + pwInfo(`Skipping Event ${eventType} ${pwAnalyticsEnabled}`, data); + } + + // once the auction ends, or the event is a bid won send events + if (eventType === CONSTANTS.EVENTS.AUCTION_END || eventType === CONSTANTS.EVENTS.BID_WON) { + flushEvents(); + } +} + +pubwiseAnalytics.storeSessionID = function (userSessID) { + storage.setDataInLocalStorage(localStorageSessName(), userSessID); + pwInfo(`New Session Generated`, userSessID); +}; + +// ensure a session exists, if not make one, always store it +pubwiseAnalytics.ensureSession = function () { + if (sessionExpired() === true || userSessionID() === null || userSessionID() === '') { + let generatedId = utils.generateUUID(); + expireUtmData(); + this.storeSessionID(generatedId); + sessionData.sessionId = generatedId; + } + // eslint-disable-next-line + // console.log('ensured session'); + extendUserSessionTimeout(); +}; + pubwiseAnalytics.adapterEnableAnalytics = pubwiseAnalytics.enableAnalytics; pubwiseAnalytics.enableAnalytics = function (config) { - if (config.options.debug === undefined) { - config.options.debug = utils.debugTurnedOn(); + configOptions = Object.assign(configOptions, config.options); + // take the PBJS debug for our debug setting if no PW debug is defined + if (configOptions.debug === null) { + configOptions.debug = utils.debugTurnedOn(); } - configOptions = config.options; markEnabled(); + sessionData.activationId = utils.generateUUID(); + this.ensureSession(); pubwiseAnalytics.adapterEnableAnalytics(config); }; adapterManager.registerAnalyticsAdapter({ adapter: pubwiseAnalytics, - code: 'pubwise' + code: 'pubwise', + gvlid: 842 }); export default pubwiseAnalytics; 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 new file mode 100644 index 00000000000..35d75e96f95 --- /dev/null +++ b/modules/pubxBidAdapter.js @@ -0,0 +1,94 @@ +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) { + if (!(bid.params.sid)) { + return false; + } else { return true } + }, + buildRequests: function(validBidRequests) { + return validBidRequests.map(bidRequest => { + const bidId = bidRequest.bidId; + const params = bidRequest.params; + const sid = params.sid; + const payload = { + sid: sid + }; + return { + id: bidId, + method: 'GET', + url: BID_ENDPOINT, + data: payload, + } + }); + }, + interpretResponse: function(serverResponse, bidRequest) { + const body = serverResponse.body; + const bidResponses = []; + if (body.cid) { + const bidResponse = { + requestId: bidRequest.id, + cpm: body.cpm, + currency: body.currency, + width: body.width, + height: body.height, + creativeId: body.cid, + netRevenue: true, + ttl: body.TTL, + ad: body.adm + }; + 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/pubxBidAdapter.md b/modules/pubxBidAdapter.md new file mode 100644 index 00000000000..da7d960c831 --- /dev/null +++ b/modules/pubxBidAdapter.md @@ -0,0 +1,32 @@ +# Overview + +Module Name: pubx Bid Adapter + +Maintainer: x@pub-x.io + +# Description + +Module that connects to Pub-X's demand sources +Supported MediaTypes: banner only + +# Test Parameters +```javascript + var adUnits = [ + { + code: 'test', + mediaTypes: { + banner: { + sizes: [300,250] + } + }, + bids: [ + { + bidder: 'pubx', + params: { + sid: 'eDMR' //ID should be provided by Pub-X + } + } + ] + } + ]; +``` \ No newline at end of file diff --git a/modules/pubxaiAnalyticsAdapter.js b/modules/pubxaiAnalyticsAdapter.js new file mode 100644 index 00000000000..136b328d381 --- /dev/null +++ b/modules/pubxaiAnalyticsAdapter.js @@ -0,0 +1,182 @@ +import { ajax } from '../src/ajax.js'; +import adapter from '../src/AnalyticsAdapter.js'; +import adapterManager from '../src/adapterManager.js'; +import CONSTANTS from '../src/constants.json'; +import * as utils from '../src/utils.js'; + +const emptyUrl = ''; +const analyticsType = 'endpoint'; +const pubxaiAnalyticsVersion = 'v1.1.0'; +const defaultHost = 'api.pbxai.com'; +const auctionPath = '/analytics/auction'; +const winningBidPath = '/analytics/bidwon'; + +let initOptions; +let auctionTimestamp; +let auctionCache = []; +let events = { + bids: [], + floorDetail: {}, + pageDetail: {}, + deviceDetail: {} +}; + +var pubxaiAnalyticsAdapter = Object.assign(adapter( + { + emptyUrl, + analyticsType + }), { + track({ eventType, args }) { + if (typeof args !== 'undefined') { + if (eventType === CONSTANTS.EVENTS.BID_TIMEOUT) { + args.forEach(item => { mapBidResponse(item, 'timeout'); }); + } else if (eventType === CONSTANTS.EVENTS.AUCTION_INIT) { + events.auctionInit = args; + events.floorDetail = {}; + events.bids = []; + const floorData = utils.deepAccess(args, 'bidderRequests.0.bids.0.floorData'); + if (typeof floorData !== 'undefined') { + Object.assign(events.floorDetail, floorData); + } + auctionTimestamp = args.timestamp; + } else if (eventType === CONSTANTS.EVENTS.BID_RESPONSE) { + mapBidResponse(args, 'response'); + } else if (eventType === CONSTANTS.EVENTS.BID_WON) { + send({ + winningBid: mapBidResponse(args, 'bidwon') + }, 'bidwon'); + } + } + if (eventType === CONSTANTS.EVENTS.AUCTION_END) { + send(events, 'auctionEnd'); + } + } +}); + +function mapBidResponse(bidResponse, status) { + if (typeof bidResponse !== 'undefined') { + let bid = { + adUnitCode: bidResponse.adUnitCode, + auctionId: bidResponse.auctionId, + bidderCode: bidResponse.bidder, + cpm: bidResponse.cpm, + creativeId: bidResponse.creativeId, + currency: bidResponse.currency, + floorData: bidResponse.floorData, + mediaType: bidResponse.mediaType, + netRevenue: bidResponse.netRevenue, + requestTimestamp: bidResponse.requestTimestamp, + responseTimestamp: bidResponse.responseTimestamp, + status: bidResponse.status, + statusMessage: bidResponse.statusMessage, + timeToRespond: bidResponse.timeToRespond, + transactionId: bidResponse.transactionId + }; + if (status !== 'bidwon') { + Object.assign(bid, { + bidId: status === 'timeout' ? bidResponse.bidId : bidResponse.requestId, + renderStatus: status === 'timeout' ? 3 : 2, + sizes: utils.parseSizesInput(bidResponse.size).toString(), + }); + events.bids.push(bid); + } else { + Object.assign(bid, { + bidId: bidResponse.requestId, + floorProvider: events.floorDetail ? events.floorDetail.floorProvider : null, + isWinningBid: true, + placementId: bidResponse.params ? utils.deepAccess(bidResponse, 'params.0.placementId') : null, + renderedSize: bidResponse.size, + renderStatus: 4 + }); + return bid; + } + } +} + +export function getDeviceType() { + if ((/ipad|android 3.0|xoom|sch-i800|playbook|tablet|kindle/i.test(navigator.userAgent.toLowerCase()))) { + return 'tablet'; + } + if ((/iphone|ipod|android|blackberry|opera|mini|windows\sce|palm|smartphone|iemobile/i.test(navigator.userAgent.toLowerCase()))) { + return 'mobile'; + } + return 'desktop'; +} + +export function getBrowser() { + if (/Chrome/.test(navigator.userAgent) && /Google Inc/.test(navigator.vendor)) return 'Chrome'; + else if (navigator.userAgent.match('CriOS')) return 'Chrome'; + else if (/Firefox/.test(navigator.userAgent)) return 'Firefox'; + else if (/Edg/.test(navigator.userAgent)) return 'Microsoft Edge'; + else if (/Safari/.test(navigator.userAgent) && /Apple Computer/.test(navigator.vendor)) return 'Safari'; + else if (/Trident/.test(navigator.userAgent) || /MSIE/.test(navigator.userAgent)) return 'Internet Explorer'; + else return 'Others'; +} + +export function getOS() { + if (navigator.userAgent.indexOf('Android') != -1) return 'Android'; + if (navigator.userAgent.indexOf('like Mac') != -1) return 'iOS'; + if (navigator.userAgent.indexOf('Win') != -1) return 'Windows'; + if (navigator.userAgent.indexOf('Mac') != -1) return 'Macintosh'; + if (navigator.userAgent.indexOf('Linux') != -1) return 'Linux'; + if (navigator.appVersion.indexOf('X11') != -1) return 'Unix'; + return 'Others'; +} + +// add sampling rate +pubxaiAnalyticsAdapter.shouldFireEventRequest = function (samplingRate = 1) { + return (Math.floor((Math.random() * samplingRate + 1)) === parseInt(samplingRate)); +} + +function send(data, status) { + if (pubxaiAnalyticsAdapter.shouldFireEventRequest(initOptions.samplingRate)) { + let location = utils.getWindowLocation(); + data.initOptions = initOptions; + if (typeof data !== 'undefined' && typeof data.auctionInit !== 'undefined') { + Object.assign(data.pageDetail, { + host: location.host, + path: location.pathname, + search: location.search, + adUnitCount: data.auctionInit.adUnitCodes ? data.auctionInit.adUnitCodes.length : null + }); + data.initOptions.auctionId = data.auctionInit.auctionId; + delete data.auctionInit; + } + data.deviceDetail = {}; + Object.assign(data.deviceDetail, { + platform: navigator.platform, + deviceType: getDeviceType(), + deviceOS: getOS(), + browser: getBrowser() + }); + let pubxaiAnalyticsRequestUrl = utils.buildUrl({ + protocol: 'https', + hostname: (initOptions && initOptions.hostName) || defaultHost, + pathname: status == 'bidwon' ? winningBidPath : auctionPath, + search: { + auctionTimestamp: auctionTimestamp, + pubxaiAnalyticsVersion: pubxaiAnalyticsVersion, + prebidVersion: $$PREBID_GLOBAL$$.version + } + }); + if (status == 'bidwon') { + ajax(pubxaiAnalyticsRequestUrl, undefined, JSON.stringify(data), { method: 'POST', contentType: 'text/json' }); + } else if (status == 'auctionEnd' && auctionCache.indexOf(data.initOptions.auctionId) === -1) { + ajax(pubxaiAnalyticsRequestUrl, undefined, JSON.stringify(data), { method: 'POST', contentType: 'text/json' }); + auctionCache.push(data.initOptions.auctionId); + } + } +} + +pubxaiAnalyticsAdapter.originEnableAnalytics = pubxaiAnalyticsAdapter.enableAnalytics; +pubxaiAnalyticsAdapter.enableAnalytics = function (config) { + initOptions = config.options; + pubxaiAnalyticsAdapter.originEnableAnalytics(config); +}; + +adapterManager.registerAnalyticsAdapter({ + adapter: pubxaiAnalyticsAdapter, + code: 'pubxai' +}); + +export default pubxaiAnalyticsAdapter; diff --git a/modules/pubxaiAnalyticsAdapter.md b/modules/pubxaiAnalyticsAdapter.md new file mode 100644 index 00000000000..112329fc171 --- /dev/null +++ b/modules/pubxaiAnalyticsAdapter.md @@ -0,0 +1,27 @@ +# Overview +Module Name: PubX.io Analytics Adapter +Module Type: Analytics Adapter +Maintainer: phaneendra@pubx.ai + +# Description + +Analytics adapter for prebid provided by Pubx.ai. Contact alex@pubx.ai for information. + +# Test Parameters + +``` +{ + provider: 'pubxai', + options : { + pubxId: 'xxx', + hostName: 'example.com', + samplingRate: 1 + } +} +``` +Property | Data Type | Is required? | Description |Example +:-----:|:-----:|:-----:|:-----:|:-----: +pubxId|string|Yes | A unique identifier provided by PubX.ai to indetify publishers. |`"a9d48e2f-24ec-4ec1-b3e2-04e32c3aeb03"` +hostName|string|No|hostName is provided by Pubx.ai. |`"https://example.com"` +samplingRate |number |No|How often the sampling must be taken. |`2` - (sample one in two cases) \ `3` - (sample one in three cases) + | | | | \ No newline at end of file diff --git a/modules/pulsepointBidAdapter.js b/modules/pulsepointBidAdapter.js index 33fdaa44100..f74d79a3dc5 100644 --- a/modules/pulsepointBidAdapter.js +++ b/modules/pulsepointBidAdapter.js @@ -121,10 +121,7 @@ function bidResponseAvailable(request, response) { netRevenue: DEFAULT_NET_REVENUE, currency: bidResponse.cur || DEFAULT_CURRENCY }; - if (idToImpMap[id]['native']) { - bid['native'] = nativeResponse(idToImpMap[id], idToBidMap[id]); - bid.mediaType = 'native'; - } else if (idToImpMap[id].video) { + if (idToImpMap[id].video) { // for outstream, a renderer is specified if (idToSlotConfig[id] && utils.deepAccess(idToSlotConfig[id], 'mediaTypes.video.context') === 'outstream') { bid.renderer = outstreamRenderer(utils.deepAccess(idToSlotConfig[id], 'renderer.options'), utils.deepAccess(idToBidMap[id], 'ext.outstream')); @@ -133,10 +130,13 @@ function bidResponseAvailable(request, response) { bid.mediaType = 'video'; bid.width = idToBidMap[id].w; bid.height = idToBidMap[id].h; - } else { + } else if (idToImpMap[id].banner) { bid.ad = idToBidMap[id].adm; bid.width = idToBidMap[id].w || idToImpMap[id].banner.w; bid.height = idToBidMap[id].h || idToImpMap[id].banner.h; + } else if (idToImpMap[id]['native']) { + bid['native'] = nativeResponse(idToImpMap[id], idToBidMap[id]); + bid.mediaType = 'native'; } bids.push(bid); } @@ -165,12 +165,12 @@ function impression(slot) { function banner(slot) { const sizes = parseSizes(slot); const size = adSize(slot, sizes); - return (slot.nativeParams || slot.params.video) ? null : { + return (slot.mediaTypes && slot.mediaTypes.banner) ? { w: size[0], h: size[1], battr: slot.params.battr, format: sizes - }; + } : null; } /** @@ -420,7 +420,7 @@ function user(bidRequest, bidderRequest) { addExternalUserId(ext.eids, bidRequest.userId.britepoolid, 'britepool.com'); addExternalUserId(ext.eids, bidRequest.userId.criteoId, 'criteo'); addExternalUserId(ext.eids, bidRequest.userId.idl_env, 'identityLink'); - addExternalUserId(ext.eids, bidRequest.userId.id5id, 'id5-sync.com'); + addExternalUserId(ext.eids, utils.deepAccess(bidRequest, 'userId.id5id.uid'), 'id5-sync.com', utils.deepAccess(bidRequest, 'userId.id5id.ext')); addExternalUserId(ext.eids, utils.deepAccess(bidRequest, 'userId.parrableId.eid'), 'parrable.com'); // liveintent if (bidRequest.userId.lipb && bidRequest.userId.lipb.lipbid) { diff --git a/modules/quantcastBidAdapter.js b/modules/quantcastBidAdapter.js index 91022d70df9..e9541edb534 100644 --- a/modules/quantcastBidAdapter.js +++ b/modules/quantcastBidAdapter.js @@ -1,6 +1,7 @@ import * as utils from '../src/utils.js'; import { ajax } from '../src/ajax.js'; import { config } from '../src/config.js'; +import { getStorageManager } from '../src/storageManager.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; import find from 'core-js-pure/features/array/find.js'; @@ -18,6 +19,9 @@ export const QUANTCAST_TEST_PUBLISHER = 'test-publisher'; export const QUANTCAST_TTL = 4; export const QUANTCAST_PROTOCOL = 'https'; export const QUANTCAST_PORT = '8443'; +export const QUANTCAST_FPA = '__qca'; + +export const storage = getStorageManager(QUANTCAST_VENDOR_ID, BIDDER_CODE); function makeVideoImp(bid) { const video = {}; @@ -85,11 +89,6 @@ function checkTCFv1(vendorData) { } function checkTCFv2(tcData) { - if (tcData.purposeOneTreatment && tcData.publisherCC === 'DE') { - // special purpose 1 treatment for Germany - return true; - } - let restrictions = tcData.publisher ? tcData.publisher.restrictions : {}; let qcRestriction = restrictions && restrictions[PURPOSE_DATA_COLLECT] ? restrictions[PURPOSE_DATA_COLLECT][QUANTCAST_VENDOR_ID] @@ -106,15 +105,21 @@ function checkTCFv2(tcData) { return !!(vendorConsent && purposeConsent); } +function getQuantcastFPA() { + let fpa = storage.getCookie(QUANTCAST_FPA) + return fpa || '' +} + +let hasUserSynced = false; + /** * The documentation for Prebid.js Adapter 1.0 can be found at link below, * http://prebid.org/dev-docs/bidder-adapter-1.html */ export const spec = { code: BIDDER_CODE, - GVLID: 11, + GVLID: QUANTCAST_VENDOR_ID, supportedMediaTypes: ['banner', 'video'], - hasUserSynced: false, /** * Verify the `AdUnits.bids` response with `true` for valid request and `false` @@ -193,7 +198,8 @@ export const spec = { uspSignal: uspConsent ? 1 : 0, uspConsent, coppa: config.getConfig('coppa') === true ? 1 : 0, - prebidJsVersion: '$prebid.version$' + prebidJsVersion: '$prebid.version$', + fpa: getQuantcastFPA() }; const data = JSON.stringify(requestData); @@ -276,7 +282,7 @@ export const spec = { }, getUserSyncs(syncOptions, serverResponses) { const syncs = [] - if (!this.hasUserSynced && syncOptions.pixelEnabled) { + if (!hasUserSynced && syncOptions.pixelEnabled) { const responseWithUrl = find(serverResponses, serverResponse => utils.deepAccess(serverResponse.body, 'userSync.url') ); @@ -288,12 +294,12 @@ export const spec = { url: url }); } - this.hasUserSynced = true; + hasUserSynced = true; } return syncs; }, resetUserSync() { - this.hasUserSynced = false; + hasUserSynced = false; } }; diff --git a/modules/quantcastIdSystem.js b/modules/quantcastIdSystem.js new file mode 100644 index 00000000000..e86c130dc5b --- /dev/null +++ b/modules/quantcastIdSystem.js @@ -0,0 +1,44 @@ +/** + * This module adds QuantcastID to the User ID module + * The {@link module:modules/userId} module is required + * @module modules/quantcastIdSystem + * @requires module:modules/userId + */ + +import {submodule} from '../src/hook.js' +import { getStorageManager } from '../src/storageManager.js'; + +const QUANTCAST_FPA = '__qca'; + +export const storage = getStorageManager(); + +/** @type {Submodule} */ +export const quantcastIdSubmodule = { + /** + * used to link submodule with config + * @type {string} + */ + name: 'quantcastId', + + /** + * decode the stored id value for passing to bid requests + * @function + * @returns {{quantcastId: string} | undefined} + */ + decode(value) { + return value; + }, + + /** + * read Quantcast first party cookie and pass it along in quantcastId + * @function + * @returns {{id: {quantcastId: string} | undefined}}} + */ + getId() { + // Consent signals are currently checked on the server side. + let fpa = storage.getCookie(QUANTCAST_FPA); + return { id: fpa ? { quantcastId: fpa } : undefined } + } +}; + +submodule('userId', quantcastIdSubmodule); diff --git a/modules/qwarryBidAdapter.js b/modules/qwarryBidAdapter.js new file mode 100644 index 00000000000..7ed5e5c984c --- /dev/null +++ b/modules/qwarryBidAdapter.js @@ -0,0 +1,98 @@ +import { registerBidder } from '../src/adapters/bidderFactory.js'; +import { deepClone } from '../src/utils.js'; +import { ajax } from '../src/ajax.js'; +import { VIDEO } from '../src/mediaTypes.js'; + +const BIDDER_CODE = 'qwarry'; +export const ENDPOINT = 'https://bidder.qwarry.co/bid/adtag?prebid=true' + +export const spec = { + code: BIDDER_CODE, + supportedMediaTypes: ['banner', 'video'], + + isBidRequestValid: function (bid) { + return !!(bid.params && bid.params.zoneToken); + }, + + buildRequests: function (validBidRequests, bidderRequest) { + let bids = []; + validBidRequests.forEach(bidRequest => { + bids.push({ + bidId: bidRequest.bidId, + zoneToken: bidRequest.params.zoneToken, + pos: bidRequest.params.pos, + sizes: prepareSizes(bidRequest.sizes) + }) + }) + + let payload = { + requestId: bidderRequest.bidderRequestId, + bids, + referer: bidderRequest.refererInfo.referer + } + + if (bidderRequest && bidderRequest.gdprConsent) { + payload.gdprConsent = { + consentRequired: (typeof bidderRequest.gdprConsent.gdprApplies === 'boolean') ? bidderRequest.gdprConsent.gdprApplies : false, + consentString: bidderRequest.gdprConsent.consentString + } + } + + const options = { + contentType: 'application/json', + customHeaders: { + 'Rtb-Direct': true + } + } + + return { + method: 'POST', + url: ENDPOINT, + data: payload, + options + }; + }, + + interpretResponse: function (serverResponse, request) { + const serverBody = serverResponse.body; + if (!serverBody || typeof serverBody !== 'object') { + return []; + } + + const { prebidResponse } = serverBody; + if (!prebidResponse || typeof prebidResponse !== 'object') { + return []; + } + + let bids = []; + prebidResponse.forEach(bidResponse => { + let bid = deepClone(bidResponse); + bid.cpm = parseFloat(bidResponse.cpm); + + // banner or video + if (VIDEO === bid.format) { + bid.vastXml = bid.ad; + } + + bids.push(bid); + }) + + return bids; + }, + + onBidWon: function (bid) { + if (bid.winUrl) { + const cpm = bid.cpm; + const winUrl = bid.winUrl.replace(/\$\{AUCTION_PRICE\}/, cpm); + ajax(winUrl, null); + return true; + } + return false; + } +} + +function prepareSizes(sizes) { + return sizes && sizes.map(size => ({ width: size[0], height: size[1] })); +} + +registerBidder(spec); diff --git a/modules/qwarryBidAdapter.md b/modules/qwarryBidAdapter.md new file mode 100644 index 00000000000..056ccb51293 --- /dev/null +++ b/modules/qwarryBidAdapter.md @@ -0,0 +1,28 @@ +# Overview + +``` +Module Name: Qwarry Bidder Adapter +Module Type: Bidder Adapter +Maintainer: akascheev@asteriosoft.com +``` + +# Description + +Connects to Qwarry Bidder for bids. +Qwarry bid adapter supports Banner and Video ads. + +# Test Parameters +``` +const adUnits = [ + { + bids: [ + { + bidder: 'qwarry', + params: { + zoneToken: '?????????????????????', // zoneToken provided by Qwarry + } + } + ] + } +]; +``` diff --git a/modules/radsBidAdapter.js b/modules/radsBidAdapter.js index 1f07cc07b91..8f3cbe02f23 100644 --- a/modules/radsBidAdapter.js +++ b/modules/radsBidAdapter.js @@ -57,7 +57,7 @@ export const spec = { bid_id: bidId, }; } - prepareExtraParams(params, payload); + prepareExtraParams(params, payload, bidderRequest); return { method: 'GET', @@ -97,9 +97,46 @@ export const spec = { bidResponses.push(bidResponse); } return bidResponses; + }, + getUserSyncs: function(syncOptions, serverResponses, gdprConsent, uspConsent) { + if (!serverResponses || serverResponses.length === 0) { + return []; + } + + const syncs = [] + + let gdprParams = ''; + if (gdprConsent) { + if ('gdprApplies' in gdprConsent && typeof gdprConsent.gdprApplies === 'boolean') { + gdprParams = `gdpr=${Number(gdprConsent.gdprApplies)}&gdpr_consent=${gdprConsent.consentString}`; + } else { + gdprParams = `gdpr_consent=${gdprConsent.consentString}`; + } + } + + if (syncOptions.iframeEnabled) { + serverResponses[0].body.userSync.iframeUrl.forEach((url) => syncs.push({ + type: 'iframe', + url: appendToUrl(url, gdprParams) + })); + } + if (syncOptions.pixelEnabled && serverResponses.length > 0) { + serverResponses[0].body.userSync.imageUrl.forEach((url) => syncs.push({ + type: 'image', + url: appendToUrl(url, gdprParams) + })); + } + return syncs; } } +function appendToUrl(url, what) { + if (!what) { + return url; + } + return url + (url.indexOf('?') !== -1 ? '&' : '?') + what; +} + function objectToQueryString(obj, prefix) { let str = []; let p; @@ -125,10 +162,23 @@ function isVideoRequest(bid) { return bid.mediaType === 'video' || !!utils.deepAccess(bid, 'mediaTypes.video'); } -function prepareExtraParams(params, payload) { +function prepareExtraParams(params, payload, bidderRequest) { if (params.pfilter !== undefined) { payload.pfilter = params.pfilter; } + + if (bidderRequest && bidderRequest.gdprConsent) { + if (payload.pfilter !== undefined) { + payload.pfilter.gdpr_consent = bidderRequest.gdprConsent.consentString; + payload.pfilter.gdpr = bidderRequest.gdprConsent.gdprApplies; + } else { + payload.pfilter = { + 'gdpr_consent': bidderRequest.gdprConsent.consentString, + 'gdpr': bidderRequest.gdprConsent.gdprApplies + }; + } + } + if (params.bcat !== undefined) { payload.bcat = params.bcat; } diff --git a/modules/readpeakBidAdapter.js b/modules/readpeakBidAdapter.js index c72bbdd658f..31e430d79f9 100644 --- a/modules/readpeakBidAdapter.js +++ b/modules/readpeakBidAdapter.js @@ -46,6 +46,19 @@ export const spec = { } }; + if (bidderRequest.gdprConsent) { + request.user = { + ext: { + consent: bidderRequest.gdprConsent.consentString || '' + }, + }; + request.regs = { + ext: { + gdpr: bidderRequest.gdprConsent.gdprApplies !== undefined ? bidderRequest.gdprConsent.gdprApplies : true + } + }; + } + return { method: 'POST', url: ENDPOINT, @@ -87,6 +100,11 @@ function bidResponseAvailable(bidRequest, bidResponse) { currency: bidResponse.cur, native: nativeResponse(idToImpMap[id], idToBidMap[id]) }; + if (idToBidMap[id].adomain) { + bid.meta = { + advertiserDomains: idToBidMap[id].adomain + } + } bids.push(bid); } }); @@ -94,11 +112,21 @@ function bidResponseAvailable(bidRequest, bidResponse) { } function impression(slot) { + let bidFloorFromModule + if (typeof slot.getFloor === 'function') { + const floorInfo = slot.getFloor({ + currency: 'USD', + mediaType: 'native', + size: '\*' + }); + bidFloorFromModule = floorInfo.currency === 'USD' ? floorInfo.floor : undefined; + } return { id: slot.bidId, native: nativeImpression(slot), - bidfloor: slot.params.bidfloor || 0, - bidfloorcur: slot.params.bidfloorcur || 'USD' + bidfloor: bidFloorFromModule || slot.params.bidfloor || 0, + bidfloorcur: (bidFloorFromModule && 'USD') || 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 new file mode 100644 index 00000000000..f8636862d4c --- /dev/null +++ b/modules/reconciliationRtdProvider.js @@ -0,0 +1,295 @@ +/** + * This module adds reconciliation provider to the real time data module + * The {@link module:modules/realTimeData} module is required + * The module will add custom targetings to ad units + * The module will listen to post messages from rendered creatives with Reconciliation Tag + * The module will call tracking pixels to log info needed for reconciliation matching + * @module modules/reconciliationRtdProvider + * @requires module:modules/realTimeData + */ + +/** + * @typedef {Object} ModuleParams + * @property {string} publisherMemberId + * @property {?string} initUrl + * @property {?string} impressionUrl + * @property {?boolean} allowAccess + */ + +import { submodule } from '../src/hook.js'; +import { ajaxBuilder } from '../src/ajax.js'; +import * as utils from '../src/utils.js'; +import find from 'core-js-pure/features/array/find.js'; + +/** @type {Object} */ +const MessageType = { + IMPRESSION_REQUEST: 'rsdk:impression:req', + IMPRESSION_RESPONSE: 'rsdk:impression:res', +}; +/** @type {ModuleParams} */ +const DEFAULT_PARAMS = { + initUrl: 'https://confirm.fiduciadlt.com/init', + impressionUrl: 'https://confirm.fiduciadlt.com/pimp', + allowAccess: false, +}; +/** @type {ModuleParams} */ +let _moduleParams = {}; + +/** + * Handle postMesssage from ad creative, track impression + * and send response to reconciliation ad tag + * @param {Event} e + */ +function handleAdMessage(e) { + let data = {}; + let adUnitId = ''; + let adDeliveryId = ''; + + try { + data = JSON.parse(e.data); + } catch (e) { + return; + } + + if (data.type === MessageType.IMPRESSION_REQUEST) { + if (utils.isGptPubadsDefined()) { + // 1. Find the last iframed window before window.top where the tracker was injected + // (the tracker could be injected in nested iframes) + const adWin = getTopIFrameWin(e.source); + if (adWin && adWin !== window.top) { + // 2. Find the GPT slot for the iframed window + const adSlot = getSlotByWin(adWin); + // 3. Get AdUnit IDs for the selected slot + if (adSlot) { + adUnitId = adSlot.getAdUnitPath(); + adDeliveryId = adSlot.getTargeting('RSDK_ADID'); + adDeliveryId = adDeliveryId.length + ? adDeliveryId[0] + : `${utils.timestamp()}-${utils.generateUUID()}`; + } + } + } + + // Call local impression callback + const args = Object.assign({}, data.args, { + publisherDomain: window.location.hostname, + publisherMemberId: _moduleParams.publisherMemberId, + adUnitId, + adDeliveryId, + }); + + track.trackPost(_moduleParams.impressionUrl, args); + + // Send response back to the Advertiser tag + let response = { + type: MessageType.IMPRESSION_RESPONSE, + id: data.id, + args: Object.assign( + { + publisherDomain: window.location.hostname, + }, + data.args + ), + }; + + // If access is allowed - add ad unit id to response + if (_moduleParams.allowAccess) { + Object.assign(response.args, { + adUnitId, + adDeliveryId, + }); + } + + e.source.postMessage(JSON.stringify(response), '*'); + } +} + +/** + * Get top iframe window for nested Window object + * - top + * -- iframe.window <-- top iframe window + * --- iframe.window + * ---- iframe.window <-- win + * + * @param {Window} win nested iframe window object + * @param {Window} topWin top window + */ +export function getTopIFrameWin(win, topWin) { + topWin = topWin || window; + + if (!win) { + return null; + } + + try { + while (win.parent !== topWin) { + win = win.parent; + } + return win; + } catch (e) { + return null; + } +} + +/** + * get all slots on page + * @return {Object[]} slot GoogleTag slots + */ +function getAllSlots() { + return utils.isGptPubadsDefined() && window.googletag.pubads().getSlots(); +} + +/** + * get GPT slot by placement id + * @param {string} code placement id + * @return {?Object} + */ +function getSlotByCode(code) { + const slots = getAllSlots(); + if (!slots || !slots.length) { + return null; + } + return ( + find( + slots, + (s) => s.getSlotElementId() === code || s.getAdUnitPath() === code + ) || null + ); +} + +/** + * get GPT slot by iframe window + * @param {Window} win + * @return {?Object} + */ +export function getSlotByWin(win) { + const slots = getAllSlots(); + + if (!slots || !slots.length) { + return null; + } + + return ( + find(slots, (s) => { + let slotElement = document.getElementById(s.getSlotElementId()); + + if (slotElement) { + let slotIframe = slotElement.querySelector('iframe'); + + if (slotIframe && slotIframe.contentWindow === win) { + return true; + } + } + + return false; + }) || null + ); +} + +/** + * Init Reconciliation post messages listeners to handle + * impressions messages from ad creative + */ +function initListeners() { + window.addEventListener('message', handleAdMessage, false); +} + +/** + * Send init event to log + * @param {Array} adUnits + */ +function trackInit(adUnits) { + track.trackPost( + _moduleParams.initUrl, + { + adUnits, + publisherDomain: window.location.hostname, + publisherMemberId: _moduleParams.publisherMemberId, + } + ); +} + +/** + * Track event via POST request + * wrap method to allow stubbing in tests + * @param {string} url + * @param {Object} data + */ +export const track = { + trackPost(url, data) { + const ajax = ajaxBuilder(); + + ajax( + url, + function() {}, + JSON.stringify(data), + { + method: 'POST', + } + ); + } +} + +/** + * Set custom targetings for provided adUnits + * @param {string[]} adUnitsCodes + * @return {Object} key-value object with custom targetings + */ +function getReconciliationData(adUnitsCodes) { + const dataToReturn = {}; + const adUnitsToTrack = []; + + adUnitsCodes.forEach((adUnitCode) => { + if (!adUnitCode) { + return; + } + + const adSlot = getSlotByCode(adUnitCode); + const adUnitId = adSlot ? adSlot.getAdUnitPath() : adUnitCode; + const adDeliveryId = `${utils.timestamp()}-${utils.generateUUID()}`; + + dataToReturn[adUnitCode] = { + RSDK_AUID: adUnitId, + RSDK_ADID: adDeliveryId, + }; + + adUnitsToTrack.push({ + adUnitId, + adDeliveryId + }); + }, {}); + + // Track init event + trackInit(adUnitsToTrack); + + return dataToReturn; +} + +/** @type {RtdSubmodule} */ +export const reconciliationSubmodule = { + /** + * used to link submodule with realTimeData + * @type {string} + */ + name: 'reconciliation', + /** + * get data and send back to realTimeData module + * @function + * @param {string[]} adUnitsCodes + */ + getTargetingData: getReconciliationData, + init: init, +}; + +function init(moduleConfig) { + const params = moduleConfig.params; + if (params && params.publisherMemberId) { + _moduleParams = Object.assign({}, DEFAULT_PARAMS, params); + initListeners(); + } else { + utils.logError('missing params for Reconciliation provider'); + } + return true; +} + +submodule('realTimeData', reconciliationSubmodule); diff --git a/modules/reconciliationRtdProvider.md b/modules/reconciliationRtdProvider.md new file mode 100644 index 00000000000..53883ad99eb --- /dev/null +++ b/modules/reconciliationRtdProvider.md @@ -0,0 +1,49 @@ +The purpose of this Real Time Data Provider is to allow publishers to match impressions accross the supply chain. + +**Reconciliation SDK** +The purpose of Reconciliation SDK module is to collect supply chain structure information and vendor-specific impression IDs from suppliers participating in ad creative delivery and report it to the Reconciliation Service, allowing publishers, advertisers and other supply chain participants to match and reconcile ad server, SSP, DSP and veritifation system log file records. Reconciliation SDK was created as part of TAG DLT initiative ( https://www.tagtoday.net/pressreleases/dlt_9_7_2020 ). + +**Usage for Publishers:** + +Compile the Reconciliation Provider into your Prebid build: + +`gulp build --modules=reconciliationRtdProvider` + +Add Reconciliation real time data provider configuration by setting up a Prebid Config: + +```javascript +const reconciliationDataProvider = { + name: "reconciliation", + params: { + publisherMemberId: "test_prebid_publisher", // required + allowAccess: true, //optional + } +}; + +pbjs.setConfig({ + ..., + realTimeData: { + dataProviders: [ + reconciliationDataProvider + ] + } +}); +``` + +where: +- `publisherMemberId` (required) - ID associated with the publisher +- `access` (optional) true/false - Whether ad markup will recieve Ad Unit Id's via Reconciliation Tag + +**Example:** + +To view an example: + +- in your cli run: + +`gulp serve --modules=reconciliationRtdProvider,appnexusBidAdapter` + +Your could also change 'appnexusBidAdapter' to another one. + +- in your browser, navigate to: + +`http://localhost:9999/integrationExamples/gpt/reconciliationRtdProvider_example.html` diff --git a/modules/relaidoBidAdapter.js b/modules/relaidoBidAdapter.js index b3b8a647137..92709b7c047 100644 --- a/modules/relaidoBidAdapter.js +++ b/modules/relaidoBidAdapter.js @@ -6,36 +6,28 @@ import { getStorageManager } from '../src/storageManager.js'; const BIDDER_CODE = 'relaido'; const BIDDER_DOMAIN = 'api.relaido.jp'; -const ADAPTER_VERSION = '1.0.1'; +const ADAPTER_VERSION = '1.0.3'; const DEFAULT_TTL = 300; const UUID_KEY = 'relaido_uuid'; const storage = getStorageManager(); function isBidRequestValid(bid) { - if (!utils.isSafariBrowser() && !hasUuid()) { - utils.logWarn('uuid is not found.'); - return false; - } if (!utils.deepAccess(bid, 'params.placementId')) { utils.logWarn('placementId param is reqeuired.'); return false; } - if (hasVideoMediaType(bid)) { - if (!isVideoValid(bid)) { - utils.logWarn('Invalid mediaType video.'); - return false; - } - } else if (hasBannerMediaType(bid)) { - if (!isBannerValid(bid)) { - utils.logWarn('Invalid mediaType banner.'); - return false; - } + if (hasVideoMediaType(bid) && isVideoValid(bid)) { + return true; } else { - utils.logWarn('Invalid mediaTypes input banner or video.'); - return false; + utils.logWarn('Invalid mediaType video.'); + } + if (hasBannerMediaType(bid) && isBannerValid(bid)) { + return true; + } else { + utils.logWarn('Invalid mediaType banner.'); } - return true; + return false; } function buildRequests(validBidRequests, bidderRequest) { @@ -47,7 +39,21 @@ function buildRequests(validBidRequests, bidderRequest) { const bidDomain = bidRequest.params.domain || BIDDER_DOMAIN; const bidUrl = `https://${bidDomain}/bid/v1/prebid/${placementId}`; const uuid = getUuid(); - const mediaType = getMediaType(bidRequest); + let mediaType = ''; + let width = 0; + let height = 0; + + if (hasVideoMediaType(bidRequest) && isVideoValid(bidRequest)) { + const playerSize = getValidSizes(utils.deepAccess(bidRequest, 'mediaTypes.video.playerSize')); + width = playerSize[0][0]; + height = playerSize[0][1]; + mediaType = VIDEO; + } else if (hasBannerMediaType(bidRequest) && isBannerValid(bidRequest)) { + const sizes = getValidSizes(utils.deepAccess(bidRequest, 'mediaTypes.banner.sizes')); + width = sizes[0][0]; + height = sizes[0][1]; + mediaType = BANNER; + } let payload = { version: ADAPTER_VERSION, @@ -61,18 +67,10 @@ function buildRequests(validBidRequests, bidderRequest) { transaction_id: bidRequest.transactionId, media_type: mediaType, uuid: uuid, + width: width, + height: height }; - if (hasVideoMediaType(bidRequest)) { - const playerSize = utils.deepAccess(bidRequest, 'mediaTypes.video.playerSize'); - payload.width = playerSize[0][0]; - payload.height = playerSize[0][1]; - } else if (hasBannerMediaType(bidRequest)) { - const sizes = utils.deepAccess(bidRequest, 'mediaTypes.banner.sizes'); - payload.width = sizes[0][0]; - payload.height = sizes[0][1]; - } - // It may not be encoded, so add it at the end of the payload payload.ref = bidderRequest.refererInfo.referer; @@ -87,10 +85,9 @@ function buildRequests(validBidRequests, bidderRequest) { player: bidRequest.params.player, width: payload.width, height: payload.height, - mediaType: mediaType, + mediaType: payload.media_type }); } - return bidRequests; } @@ -101,10 +98,6 @@ function interpretResponse(serverResponse, bidRequest) { return []; } - if (body.uuid) { - storage.setDataInLocalStorage(UUID_KEY, body.uuid); - } - const playerUrl = bidRequest.player || body.playerUrl; const mediaType = bidRequest.mediaType || VIDEO; @@ -141,7 +134,6 @@ function getUserSyncs(syncOptions, serverResponses) { if (serverResponses.length > 0) { syncUrl = utils.deepAccess(serverResponses, '0.body.syncUrl') || syncUrl; } - receiveMessage(); return [{ type: 'iframe', url: syncUrl @@ -171,6 +163,7 @@ function onTimeout(data) { auction_id: utils.deepAccess(data, '0.auctionId'), bid_id: utils.deepAccess(data, '0.bidId'), ad_unit_code: utils.deepAccess(data, '0.adUnitCode'), + version: ADAPTER_VERSION, ref: window.location.href, }).replace(/\&$/, ''); const bidDomain = utils.deepAccess(data, '0.params.0.domain') || BIDDER_DOMAIN; @@ -219,37 +212,20 @@ function outstreamRender(bid) { }); } -function receiveMessage() { - window.addEventListener('message', setUuid); -} - -function setUuid(e) { - if (utils.isPlainObject(e.data) && e.data.relaido_uuid) { - storage.setDataInLocalStorage(UUID_KEY, e.data.relaido_uuid); - window.removeEventListener('message', setUuid); - } -} - function isBannerValid(bid) { if (!isMobile()) { return false; } - const sizes = utils.deepAccess(bid, 'mediaTypes.banner.sizes'); - if (sizes && utils.isArray(sizes)) { - if (utils.isArray(sizes[0])) { - const width = sizes[0][0]; - const height = sizes[0][1]; - if (width >= 300 && height >= 250) { - return true; - } - } + const sizes = getValidSizes(utils.deepAccess(bid, 'mediaTypes.banner.sizes')); + if (sizes.length > 0) { + return true; } return false; } function isVideoValid(bid) { - const playerSize = utils.deepAccess(bid, 'mediaTypes.video.playerSize'); - if (playerSize && utils.isArray(playerSize) && playerSize.length > 0) { + const playerSize = getValidSizes(utils.deepAccess(bid, 'mediaTypes.video.playerSize')); + if (playerSize.length > 0) { const context = utils.deepAccess(bid, 'mediaTypes.video.context'); if (context && context === 'outstream') { return true; @@ -258,12 +234,12 @@ function isVideoValid(bid) { return false; } -function hasUuid() { - return !!storage.getDataFromLocalStorage(UUID_KEY); -} - function getUuid() { - return storage.getDataFromLocalStorage(UUID_KEY) || ''; + const id = storage.getCookie(UUID_KEY) + if (id) return id; + const newId = utils.generateUUID(); + storage.setCookie(UUID_KEY, newId); + return newId; } export function isMobile() { @@ -274,15 +250,6 @@ export function isMobile() { return false; } -function getMediaType(bid) { - if (hasVideoMediaType(bid)) { - return VIDEO; - } else if (hasBannerMediaType(bid)) { - return BANNER; - } - return ''; -} - function hasBannerMediaType(bid) { return !!utils.deepAccess(bid, 'mediaTypes.banner'); } @@ -291,6 +258,25 @@ function hasVideoMediaType(bid) { return !!utils.deepAccess(bid, 'mediaTypes.video'); } +function getValidSizes(sizes) { + let result = []; + if (sizes && utils.isArray(sizes) && sizes.length > 0) { + for (let i = 0; i < sizes.length; i++) { + if (utils.isArray(sizes[i]) && sizes[i].length == 2) { + const width = sizes[i][0]; + const height = sizes[i][1]; + if (width == 1 && height == 1) { + return [[1, 1]]; + } + if ((width >= 300 && height >= 250)) { + result.push([width, height]); + } + } + } + } + return result; +} + export const spec = { code: BIDDER_CODE, supportedMediaTypes: [BANNER, VIDEO], 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/rhythmoneBidAdapter.js b/modules/rhythmoneBidAdapter.js index 1acbcfd0463..fa090044f05 100644 --- a/modules/rhythmoneBidAdapter.js +++ b/modules/rhythmoneBidAdapter.js @@ -253,6 +253,9 @@ function RhythmOneBidAdapter() { cpm: parseFloat(bid.price), width: bid.w, height: bid.h, + meta: { + advertiserDomains: bid.adomain + }, creativeId: bid.crid, currency: 'USD', netRevenue: true, diff --git a/modules/richaudienceBidAdapter.js b/modules/richaudienceBidAdapter.js index 43bef356a73..5e2a5e1bff5 100755 --- a/modules/richaudienceBidAdapter.js +++ b/modules/richaudienceBidAdapter.js @@ -9,28 +9,29 @@ let REFERER = ''; export const spec = { code: BIDDER_CODE, + gvlid: 108, aliases: ['ra'], supportedMediaTypes: [BANNER, VIDEO], /*** - * Determines whether or not the given bid request is valid - * - * @param {bidRequest} bid The bid params to validate. - * @returns {boolean} True if this is a valid bid, and false otherwise - */ + * Determines whether or not the given bid request is valid + * + * @param {bidRequest} bid The bid params to validate. + * @returns {boolean} True if this is a valid bid, and false otherwise + */ isBidRequestValid: function (bid) { return !!(bid.params && bid.params.pid && bid.params.supplyType); }, /*** - * Build a server request from the list of valid BidRequests - * @param {validBidRequests} is an array of the valid bids - * @param {bidderRequest} bidder request object - * @returns {ServerRequest} Info describing the request to the server - */ + * Build a server request from the list of valid BidRequests + * @param {validBidRequests} is an array of the valid bids + * @param {bidderRequest} bidder request object + * @returns {ServerRequest} Info describing the request to the server + */ buildRequests: function (validBidRequests, bidderRequest) { return validBidRequests.map(bid => { var payload = { - bidfloor: bid.params.bidfloor, + bidfloor: raiGetFloor(bid, config), ifa: bid.params.ifa, pid: bid.params.pid, supplyType: bid.params.supplyType, @@ -48,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) @@ -73,11 +77,11 @@ export const spec = { }); }, /*** - * Read the response from the server and build a list of bids - * @param {serverResponse} Response from the server. - * @param {bidRequest} Bid request object - * @returns {bidResponses} Array of bids which were nested inside the server - */ + * Read the response from the server and build a list of bids + * @param {serverResponse} Response from the server. + * @param {bidRequest} Bid request object + * @returns {bidResponses} Array of bids which were nested inside the server + */ interpretResponse: function (serverResponse, bidRequest) { const bidResponses = []; // try catch @@ -99,10 +103,16 @@ export const spec = { if (response.media_type === 'video') { bidResponse.vastXml = response.vastXML; try { - if (JSON.parse(bidRequest.data).videoData.format == 'outstream') { - bidResponse.renderer = Renderer.install({ - url: 'https://cdn3.richaudience.com/prebidVideo/player.js' - }); + if (bidResponse.vastXml != null) { + if (JSON.parse(bidRequest.data).videoData.format == 'outstream' || JSON.parse(bidRequest.data).videoData.format == 'banner') { + bidResponse.renderer = Renderer.install({ + id: bidRequest.bidId, + adunitcode: bidRequest.tagId, + loaded: false, + config: response.media_type, + url: 'https://cdn3.richaudience.com/prebidVideo/player.js' + }); + } bidResponse.renderer.setRender(renderer); } } catch (e) { @@ -117,13 +127,13 @@ export const spec = { return bidResponses }, /*** - * User Syncs - * - * @param {syncOptions} Publisher prebid configuration - * @param {serverResponses} Response from the server - * @param {gdprConsent} GPDR consent object - * @returns {Array} - */ + * User Syncs + * + * @param {syncOptions} Publisher prebid configuration + * @param {serverResponses} Response from the server + * @param {gdprConsent} GPDR consent object + * @returns {Array} + */ getUserSyncs: function (syncOptions, serverResponses, gdprConsent) { const syncs = []; @@ -131,11 +141,15 @@ export const spec = { var syncUrl = ''; var consent = ''; + var raiSync = {}; + + raiSync = raiGetSyncInclude(config); + if (gdprConsent && typeof gdprConsent.consentString === 'string' && typeof gdprConsent.consentString != 'undefined') { - consent = `consentString=’${gdprConsent.consentString}` + consent = `consentString=${gdprConsent.consentString}` } - if (syncOptions.iframeEnabled) { + if (syncOptions.iframeEnabled && raiSync.raiIframe != 'exclude') { syncUrl = 'https://sync.richaudience.com/dcf3528a0b8aa83634892d50e91c306e/?ord=' + rand if (consent != '') { syncUrl += `&${consent}` @@ -146,7 +160,7 @@ export const spec = { }); } - if (syncOptions.pixelEnabled && REFERER != null && syncs.length == 0) { + if (syncOptions.pixelEnabled && REFERER != null && syncs.length == 0 && raiSync.raiImage != 'exclude') { syncUrl = `https://sync.richaudience.com/bf7c142f4339da0278e83698a02b0854/?referrer=${REFERER}`; if (consent != '') { syncUrl += `&${consent}` @@ -193,6 +207,10 @@ function raiGetVideoInfo(bid) { playerSize: bid.mediaTypes.video.playerSize, mimes: bid.mediaTypes.video.mimes }; + } else { + videoData = { + format: 'banner' + } } return videoData; } @@ -201,7 +219,7 @@ function raiSetEids(bid) { let eids = []; if (bid && bid.userId) { - raiSetUserId(bid, eids, 'id5-sync.com', utils.deepAccess(bid, `userId.id5id`)); + raiSetUserId(bid, eids, 'id5-sync.com', utils.deepAccess(bid, `userId.id5id.uid`)); raiSetUserId(bid, eids, 'pubcommon', utils.deepAccess(bid, `userId.pubcid`)); raiSetUserId(bid, eids, 'criteo.com', utils.deepAccess(bid, `userId.criteoId`)); raiSetUserId(bid, eids, 'liveramp.com', utils.deepAccess(bid, `userId.idl_env`)); @@ -241,3 +259,50 @@ 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; +} + +function raiGetSyncInclude(config) { + try { + let raConfig = null; + let raiSync = {}; + if (config.getConfig('userSync').filterSettings != null && typeof config.getConfig('userSync').filterSettings != 'undefined') { + raConfig = config.getConfig('userSync').filterSettings + if (raConfig.iframe != null && typeof raConfig.iframe != 'undefined') { + raiSync.raiIframe = raConfig.iframe.bidders == 'richaudience' || raConfig.iframe.bidders == '*' ? raConfig.iframe.filter : 'exclude'; + } + if (raConfig.image != null && typeof raConfig.image != 'undefined') { + raiSync.raiImage = raConfig.image.bidders == 'richaudience' || raConfig.image.bidders == '*' ? raConfig.image.filter : 'exclude'; + } + } + return raiSync; + } catch (e) { + return null; + } +} + +function raiGetFloor(bid, config) { + try { + let raiFloor; + if (bid.params.bidfloor != null) { + raiFloor = bid.params.bidfloor; + } else if (typeof bid.getFloor == 'function') { + let floorSpec = bid.getFloor({ + currency: config.getConfig('currency.adServerCurrency'), + mediaType: bid.mediaType.banner ? 'banner' : 'video', + size: '*' + }) + + raiFloor = floorSpec.floor; + } + return raiFloor + } catch (e) { + return 0 + } +} diff --git a/modules/richaudienceBidAdapter.md b/modules/richaudienceBidAdapter.md index 932cdb8f8de..f888117b166 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": "key1=value1;key2=value2;key3=value3;" } }] } diff --git a/modules/riseBidAdapter.js b/modules/riseBidAdapter.js new file mode 100644 index 00000000000..e3265ad5d3e --- /dev/null +++ b/modules/riseBidAdapter.js @@ -0,0 +1,251 @@ +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: params.sessionId || utils.getBidIdParameter('auctionId', bid), + is_wrapper: !!params.isWrapper, + 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/rtbhouseBidAdapter.js b/modules/rtbhouseBidAdapter.js index 3337c3f1b59..036bdfaebeb 100644 --- a/modules/rtbhouseBidAdapter.js +++ b/modules/rtbhouseBidAdapter.js @@ -7,6 +7,7 @@ const BIDDER_CODE = 'rtbhouse'; const REGIONS = ['prebid-eu', 'prebid-us', 'prebid-asia']; const ENDPOINT_URL = 'creativecdn.com/bidder/prebid/bids'; const DEFAULT_CURRENCY_ARR = ['USD']; // NOTE - USD is the only supported currency right now; Hardcoded for bids +const SUPPORTED_MEDIA_TYPES = [BANNER, NATIVE]; const TTL = 55; // Codes defined by OpenRTB Native Ads 1.1 specification @@ -34,7 +35,7 @@ export const OPENRTB = { export const spec = { code: BIDDER_CODE, - supportedMediaTypes: [BANNER, NATIVE], + supportedMediaTypes: SUPPORTED_MEDIA_TYPES, isBidRequestValid: function (bid) { return !!(includes(REGIONS, bid.params.region) && bid.params.publisherId); @@ -55,6 +56,23 @@ export const spec = { request.regs = {ext: {gdpr: gdpr}}; request.user = {ext: {consent: consentStr}}; } + if (validBidRequests[0].schain) { + const schain = mapSchain(validBidRequests[0].schain); + if (schain) { + request.ext = { + schain: schain, + } + } + } + + if (validBidRequests[0].userIdAsEids) { + const eids = { eids: validBidRequests[0].userIdAsEids }; + if (request.user && request.user.ext) { + request.user.ext = { ...request.user.ext, ...eids }; + } else { + request.user = {ext: eids}; + } + } return { method: 'POST', @@ -85,6 +103,22 @@ export const spec = { }; registerBidder(spec); +/** + * @param {object} slot Ad Unit Params by Prebid + * @returns {int} floor by imp type + */ +function applyFloor(slot) { + const floors = []; + if (typeof slot.getFloor === 'function') { + Object.keys(slot.mediaTypes).forEach(type => { + if (includes(SUPPORTED_MEDIA_TYPES, type)) { + floors.push(slot.getFloor({ currency: DEFAULT_CURRENCY_ARR[0], mediaType: type, size: slot.sizes || '*' }).floor); + } + }); + } + return floors.length > 0 ? Math.max(...floors) : parseFloat(slot.params.bidfloor); +} + /** * @param {object} slot Ad Unit Params by Prebid * @returns {object} Imp by OpenRTB 2.5 §3.2.4 @@ -97,9 +131,9 @@ function mapImpression(slot) { tagid: slot.adUnitCode.toString() }; - const bidfloor = parseFloat(slot.params.bidfloor); + const bidfloor = applyFloor(slot); if (bidfloor) { - imp.bidfloor = bidfloor + imp.bidfloor = bidfloor; } return imp; @@ -151,12 +185,7 @@ function mapSource(slot) { const source = { tid: slot.transactionId, }; - const schain = mapSchain(slot.schain); - if (schain) { - source.ext = { - schain: schain - } - } + return source; } @@ -302,6 +331,9 @@ function interpretBannerBid(serverBid) { width: serverBid.w, height: serverBid.h, ttl: TTL, + meta: { + advertiserDomains: serverBid.adomain + }, netRevenue: true, currency: 'USD' } @@ -320,6 +352,9 @@ function interpretNativeBid(serverBid) { width: 1, height: 1, ttl: TTL, + meta: { + advertiserDomains: serverBid.adomain + }, netRevenue: true, currency: 'USD', native: interpretNativeAd(serverBid.adm), diff --git a/modules/rtdModule/index.js b/modules/rtdModule/index.js index 3aa7753d204..e235868f791 100644 --- a/modules/rtdModule/index.js +++ b/modules/rtdModule/index.js @@ -3,16 +3,49 @@ * @module modules/realTimeData */ +/** + * @interface UserConsentData + */ +/** + * @property + * @summary gdpr consent + * @name UserConsentData#gdpr + * @type {Object} + */ +/** + * @property + * @summary usp consent + * @name UserConsentData#usp + * @type {Object} + */ +/** + * @property + * @summary coppa + * @name UserConsentData#coppa + * @type {boolean} + */ + /** * @interface RtdSubmodule */ /** - * @function + * @function? * @summary return real time data - * @name RtdSubmodule#getData - * @param {AdUnit[]} adUnits - * @param {function} onDone + * @name RtdSubmodule#getTargetingData + * @param {string[]} adUnitsCodes + * @param {SubmoduleConfig} config + * @param {UserConsentData} userConsent + */ + +/** + * @function? + * @summary modify bid request data + * @name RtdSubmodule#getBidRequestData + * @param {SubmoduleConfig} config + * @param {UserConsentData} userConsent + * @param {Object} reqBidsConfigObj + * @param {function} callback */ /** @@ -23,14 +56,50 @@ */ /** - * @interface ModuleConfig + * @property + * @summary used to link submodule with config + * @name RtdSubmodule#config + * @type {Object} */ /** - * @property - * @summary sub module name - * @name ModuleConfig#name - * @type {string} + * @function + * @summary init sub module + * @name RtdSubmodule#init + * @param {SubmoduleConfig} config + * @param {UserConsentData} user consent + * @return {boolean} false to remove sub module + */ + +/** + * @function? + * @summary on auction init event + * @name RtdSubmodule#onAuctionInitEvent + * @param {Object} data + * @param {SubmoduleConfig} config + * @param {UserConsentData} userConsent + */ + +/** + * @function? + * @summary on auction end event + * @name RtdSubmodule#onAuctionEndEvent + * @param {Object} data + * @param {SubmoduleConfig} config + * @param {UserConsentData} userConsent + */ + +/** + * @function? + * @summary on bid response event + * @name RtdSubmodule#onBidResponseEvent + * @param {Object} data + * @param {SubmoduleConfig} config + * @param {UserConsentData} userConsent + */ + +/** + * @interface ModuleConfig */ /** @@ -40,41 +109,66 @@ * @type {number} */ +/** + * @property + * @summary list of sub modules + * @name ModuleConfig#dataProviders + * @type {SubmoduleConfig[]} + */ + +/** + * @interface SubModuleConfig + */ + /** * @property * @summary params for provide (sub module) - * @name ModuleConfig#params + * @name SubModuleConfig#params * @type {Object} */ /** * @property - * @summary timeout (if no auction dealy) - * @name ModuleConfig#timeout - * @type {number} + * @summary name + * @name ModuleConfig#name + * @type {string} + */ + +/** + * @property + * @summary delay auction for this sub module + * @name ModuleConfig#waitForIt + * @type {boolean} */ -import {getGlobal} from '../../src/prebidGlobal.js'; import {config} from '../../src/config.js'; -import {targeting} from '../../src/targeting.js'; -import {getHook, module} from '../../src/hook.js'; +import {module} from '../../src/hook.js'; import * as utils from '../../src/utils.js'; +import events from '../../src/events.js'; +import CONSTANTS from '../../src/constants.json'; +import {gdprDataHandler, uspDataHandler} from '../../src/adapterManager.js'; +import find from 'core-js-pure/features/array/find.js'; +import {getGlobal} from '../../src/prebidGlobal.js'; /** @type {string} */ const MODULE_NAME = 'realTimeData'; -/** @type {number} */ -const DEF_TIMEOUT = 1000; /** @type {RtdSubmodule[]} */ -let subModules = []; +let registeredSubModules = []; +/** @type {RtdSubmodule[]} */ +export let subModules = []; /** @type {ModuleConfig} */ let _moduleConfig; +/** @type {SubmoduleConfig[]} */ +let _dataProviders = []; +/** @type {UserConsentData} */ +let _userConsent; /** * enable submodule in User ID * @param {RtdSubmodule} submodule */ export function attachRealTimeDataProvider(submodule) { - subModules.push(submodule); + registeredSubModules.push(submodule); } export function init(config) { @@ -85,91 +179,154 @@ export function init(config) { } confListener(); // unsubscribe config listener _moduleConfig = realTimeData; - if (typeof (_moduleConfig.auctionDelay) === 'undefined') { - _moduleConfig.auctionDelay = 0; - } - // delay bidding process only if auctionDelay > 0 - if (!_moduleConfig.auctionDelay > 0) { - getHook('bidsBackCallback').before(setTargetsAfterRequestBids); - } else { - getGlobal().requestBids.before(requestBidsHook); + _dataProviders = realTimeData.dataProviders; + setEventsListeners(); + getGlobal().requestBids.before(setBidRequestsData, 40); + initSubModules(); + }); +} + +function getConsentData() { + return { + gdpr: gdprDataHandler.getConsentData(), + usp: uspDataHandler.getConsentData(), + coppa: !!(config.getConfig('coppa')) + } +} + +/** + * call each sub module init function by config order + * if no init function / init return failure / module not configured - remove it from submodules list + */ +function initSubModules() { + _userConsent = getConsentData(); + let subModulesByOrder = []; + _dataProviders.forEach(provider => { + const sm = find(registeredSubModules, s => s.name === provider.name); + const initResponse = sm && sm.init && sm.init(provider, _userConsent); + if (initResponse) { + subModulesByOrder.push(Object.assign(sm, {config: provider})); } }); + subModules = subModulesByOrder; +} + +/** + * call each sub module event function by config order + */ +function setEventsListeners() { + events.on(CONSTANTS.EVENTS.AUCTION_INIT, (args) => { + subModules.forEach(sm => { sm.onAuctionInitEvent && sm.onAuctionInitEvent(args, sm.config, _userConsent) }) + }); + events.on(CONSTANTS.EVENTS.AUCTION_END, (args) => { + getAdUnitTargeting(args); + subModules.forEach(sm => { sm.onAuctionEndEvent && sm.onAuctionEndEvent(args, sm.config, _userConsent) }) + }); + events.on(CONSTANTS.EVENTS.BID_RESPONSE, (args) => { + subModules.forEach(sm => { sm.onBidResponseEvent && sm.onBidResponseEvent(args, sm.config, _userConsent) }) + }); } /** - * get data from sub module - * @param {AdUnit[]} adUnits received from auction - * @param {function} callback callback function on data received + * loop through configured data providers If the data provider has registered getBidRequestData, + * call it, providing reqBidsConfigObj, consent data and module params + * this allows submodules to modify bidders + * @param {Object} reqBidsConfigObj required; This is the same param that's used in pbjs.requestBids. + * @param {function} fn required; The next function in the chain, used by hook.js */ -function getProviderData(adUnits, callback) { - const callbackExpected = subModules.length; - let dataReceived = []; - let processDone = false; - const dataWaitTimeout = setTimeout(() => { - processDone = true; - callback(dataReceived); - }, _moduleConfig.auctionDelay || _moduleConfig.timeout || DEF_TIMEOUT); +export function setBidRequestsData(fn, reqBidsConfigObj) { + _userConsent = getConsentData(); + const relevantSubModules = []; + const prioritySubModules = []; subModules.forEach(sm => { - sm.getData(adUnits, onDataReceived); + if (typeof sm.getBidRequestData !== 'function') { + return; + } + relevantSubModules.push(sm); + const config = sm.config; + if (config && config.waitForIt) { + prioritySubModules.push(sm); + } }); - function onDataReceived(data) { - if (processDone) { - return + const shouldDelayAuction = prioritySubModules.length && _moduleConfig.auctionDelay && _moduleConfig.auctionDelay > 0; + let callbacksExpected = prioritySubModules.length; + let isDone = false; + let waitTimeout; + + if (!relevantSubModules.length) { + return exitHook(); + } + + if (shouldDelayAuction) { + waitTimeout = setTimeout(exitHook, _moduleConfig.auctionDelay); + } + + relevantSubModules.forEach(sm => { + sm.getBidRequestData(reqBidsConfigObj, onGetBidRequestDataCallback.bind(sm), sm.config, _userConsent) + }); + + if (!shouldDelayAuction) { + return exitHook(); + } + + function onGetBidRequestDataCallback() { + if (isDone) { + return; } - dataReceived.push(data); - if (dataReceived.length === callbackExpected) { - processDone = true; - clearTimeout(dataWaitTimeout); - callback(dataReceived); + if (this.config && this.config.waitForIt) { + callbacksExpected--; + } + if (callbacksExpected <= 0) { + return exitHook(); } } + + function exitHook() { + isDone = true; + clearTimeout(waitTimeout); + fn.call(this, reqBidsConfigObj); + } } /** - * delete invalid data received from provider - * this is to ensure working flow for GPT - * @param {Object} data received from provider - * @return {Object} valid data for GPT targeting + * loop through configured data providers If the data provider has registered getTargetingData, + * call it, providing ad unit codes, consent data and module params + * the sub mlodle will return data to set on the ad unit + * this function used to place key values on primary ad server per ad unit + * @param {Object} auction object received on auction end event */ -export function validateProviderDataForGPT(data) { - // data must be an object, contains object with string as value - if (typeof data !== 'object') { - return {}; +export function getAdUnitTargeting(auction) { + const relevantSubModules = subModules.filter(sm => typeof sm.getTargetingData === 'function'); + if (!relevantSubModules.length) { + return; } - for (let key in data) { - if (data.hasOwnProperty(key)) { - for (let innerKey in data[key]) { - if (data[key].hasOwnProperty(innerKey)) { - if (typeof data[key][innerKey] !== 'string') { - utils.logWarn(`removing ${key}: {${innerKey}:${data[key][innerKey]} } from GPT targeting because of invalid type (must be string)`); - delete data[key][innerKey]; - } - } - } + + // get data + const adUnitCodes = auction.adUnitCodes; + if (!adUnitCodes) { + return; + } + let targeting = []; + for (let i = relevantSubModules.length - 1; i >= 0; i--) { + const smTargeting = relevantSubModules[i].getTargetingData(adUnitCodes, relevantSubModules[i].config, _userConsent); + if (smTargeting && typeof smTargeting === 'object') { + targeting.push(smTargeting); + } else { + utils.logWarn('invalid getTargetingData response for sub module', relevantSubModules[i].name); } } - return data; -} - -/** - * run hook after bids request and before callback - * get data from provider and set key values to primary ad server - * @param {function} next - next hook function - * @param {AdUnit[]} adUnits received from auction - */ -export function setTargetsAfterRequestBids(next, adUnits) { - getProviderData(adUnits, (data) => { - if (data && Object.keys(data).length) { - const _mergedData = deepMerge(data); - if (Object.keys(_mergedData).length) { - setDataForPrimaryAdServer(_mergedData); - } + // place data on auction adUnits + const mergedTargeting = deepMerge(targeting); + auction.adUnits.forEach(adUnit => { + const kv = adUnit.code && mergedTargeting[adUnit.code]; + if (!kv) { + return } - next(adUnits); + adUnit[CONSTANTS.JSON_MAPPING.ADSERVER_TARGETING] = Object.assign(adUnit[CONSTANTS.JSON_MAPPING.ADSERVER_TARGETING] || {}, kv); }); + return auction.adUnits; } /** @@ -198,54 +355,5 @@ export function deepMerge(arr) { }, {}); } -/** - * run hook before bids request - * get data from provider and set key values to primary ad server & bidders - * @param {function} fn - hook function - * @param {Object} reqBidsConfigObj - request bids object - */ -export function requestBidsHook(fn, reqBidsConfigObj) { - getProviderData(reqBidsConfigObj.adUnits || getGlobal().adUnits, (data) => { - if (data && Object.keys(data).length) { - const _mergedData = deepMerge(data); - if (Object.keys(_mergedData).length) { - setDataForPrimaryAdServer(_mergedData); - addIdDataToAdUnitBids(reqBidsConfigObj.adUnits || getGlobal().adUnits, _mergedData); - } - } - return fn.call(this, reqBidsConfigObj); - }); -} - -/** - * set data to primary ad server - * @param {Object} data - key values to set - */ -function setDataForPrimaryAdServer(data) { - data = validateProviderDataForGPT(data); - if (utils.isGptPubadsDefined()) { - targeting.setTargetingForGPT(data, null) - } else { - window.googletag = window.googletag || {}; - window.googletag.cmd = window.googletag.cmd || []; - window.googletag.cmd.push(() => { - targeting.setTargetingForGPT(data, null); - }); - } -} - -/** - * @param {AdUnit[]} adUnits - * @param {Object} data - key values to set - */ -function addIdDataToAdUnitBids(adUnits, data) { - adUnits.forEach(adUnit => { - adUnit.bids = adUnit.bids.map(bid => { - const rd = data[adUnit.code] || {}; - return Object.assign(bid, {realTimeData: rd}); - }) - }); -} - -init(config); module('realTimeData', attachRealTimeDataProvider); +init(config); diff --git a/modules/rtdModule/provider.md b/modules/rtdModule/provider.md index fb42e7188d3..116db160238 100644 --- a/modules/rtdModule/provider.md +++ b/modules/rtdModule/provider.md @@ -1,27 +1,33 @@ New provider must include the following: -1. sub module object: -``` -export const subModuleName = { - name: String, - getData: Function -}; -``` +1. sub module object with the following keys: -2. Function that returns the real time data according to the following structure: -``` +| param name | type | Scope | Description | Params | +| :------------ | :------------ | :------ | :------ | :------ | +| name | string | required | must match the name provided by the publisher in the on-page config | n/a | +| init | function | required | defines the function that does any auction-level initialization required | config, userConsent | +| getTargetingData | function | optional | defines a function that provides ad server targeting data to RTD-core | adUnitArray, config, userConsent | +| getBidRequestData | function | optional | defines a function that provides ad server targeting data to RTD-core | reqBidsConfigObj, callback, config, userConsent | +| onAuctionInitEvent | function | optional | listens to the AUCTION_INIT event and calls a sub-module function that lets it inspect and/or update the auction | auctionDetails, config, userConsent | +| onAuctionEndEvent | function |optional | listens to the AUCTION_END event and calls a sub-module function that lets it know when auction is done | auctionDetails, config, userConsent | +| onBidResponseEvent | function |optional | listens to the BID_RESPONSE event and calls a sub-module function that lets it know when a bid response has been collected | bidResponse, config, userConsent | + +2. `getTargetingData` function (if defined) should return ad unit targeting data according to the following structure: +```json { "adUnitCode":{ "key":"value", "key2":"value" }, "adUnitCode2":{ - "dataKey":"dataValue", + "dataKey":"dataValue" } } ``` 3. Hook to Real Time Data module: -``` +```javascript submodule('realTimeData', subModuleName); ``` + +4. See detailed documentation [here](https://docs.prebid.org/dev-docs/add-rtd-submodule.html) diff --git a/modules/rtdModule/realTimeData.md b/modules/rtdModule/realTimeData.md deleted file mode 100644 index b2859098b1f..00000000000 --- a/modules/rtdModule/realTimeData.md +++ /dev/null @@ -1,32 +0,0 @@ -## Real Time Data Configuration Example - -Example showing config using `browsi` sub module -``` - pbjs.setConfig({ - "realTimeData": { - "auctionDelay": 1000, - dataProviders[{ - "name": "browsi", - "params": { - "url": "testUrl.com", - "siteKey": "testKey", - "pubKey": "testPub", - "keyName":"bv" - } - }] - } - }); -``` - -Example showing real time data object received form `browsi` real time data provider -``` -{ - "adUnitCode":{ - "key":"value", - "key2":"value" - }, - "adUnitCode2":{ - "dataKey":"dataValue", - } -} -``` diff --git a/modules/rubiconAnalyticsAdapter.js b/modules/rubiconAnalyticsAdapter.js index 6defd7b45ae..5a2e02b8f89 100644 --- a/modules/rubiconAnalyticsAdapter.js +++ b/modules/rubiconAnalyticsAdapter.js @@ -5,7 +5,23 @@ import { ajax } from '../src/ajax.js'; import { config } from '../src/config.js'; import * as utils from '../src/utils.js'; import { getGlobal } from '../src/prebidGlobal.js'; +import { getStorageManager } from '../src/storageManager.js'; + +const RUBICON_GVL_ID = 52; +export const storage = getStorageManager(RUBICON_GVL_ID, 'rubicon'); +const COOKIE_NAME = 'rpaSession'; +const LAST_SEEN_EXPIRE_TIME = 1800000; // 30 mins +const END_EXPIRE_TIME = 21600000; // 6 hours + +const pbsErrorMap = { + 1: 'timeout-error', + 2: 'input-error', + 3: 'connect-error', + 4: 'request-error', + 999: 'generic-error' +} +let prebidGlobal = getGlobal(); const { EVENTS: { AUCTION_INIT, @@ -38,8 +54,24 @@ const cache = { auctions: {}, targeting: {}, timeouts: {}, + gpt: {}, }; +const BID_REJECTED_IPF = 'rejected-ipf'; + +export let rubiConf = { + 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 +config.getConfig('rubicon', config => { + utils.mergeDeep(rubiConf, config.rubicon); + if (utils.deepAccess(config, 'rubicon.updatePageView') === true) { + rubiConf.pvid = utils.generateUUID().slice(0, 8) + } +}); + export function getHostNameFromReferer(referer) { try { rubiconAdapter.referrerHostname = utils.parseUrl(referer, {noDecodeWholeURL: true}).hostname; @@ -87,6 +119,7 @@ function sendMessage(auctionId, bidWonId) { function formatBid(bid) { return utils.pick(bid, [ 'bidder', + 'bidderDetail', 'bidId', bidId => utils.deepAccess(bid, 'bidResponse.pbsBidId') || utils.deepAccess(bid, 'bidResponse.seatBidId') || bidId, 'status', 'error', @@ -94,7 +127,7 @@ function sendMessage(auctionId, bidWonId) { if (source) { return source; } - return serverConfig && Array.isArray(serverConfig.bidders) && serverConfig.bidders.indexOf(bid.bidder) !== -1 + return serverConfig && Array.isArray(serverConfig.bidders) && serverConfig.bidders.some(s2sBidder => s2sBidder.toLowerCase() === bid.bidder) !== -1 ? 'server' : 'client' }, 'clientLatencyMillis', @@ -106,7 +139,9 @@ function sendMessage(auctionId, bidWonId) { 'dimensions', 'mediaType', 'floorValue', - 'floorRule' + 'floorRuleValue', + 'floorRule', + 'adomains' ]) : undefined ]); } @@ -126,17 +161,21 @@ function sendMessage(auctionId, bidWonId) { }); } let auctionCache = cache.auctions[auctionId]; - let referrer = config.getConfig('pageUrl') || auctionCache.referrer; + let referrer = config.getConfig('pageUrl') || (auctionCache && auctionCache.referrer); let message = { eventTimeMillis: Date.now(), - integration: config.getConfig('rubicon.int_type') || DEFAULT_INTEGRATION, + integration: rubiConf.int_type || DEFAULT_INTEGRATION, version: '$prebid.version$', referrerUri: referrer, - referrerHostname: rubiconAdapter.referrerHostname || getHostNameFromReferer(referrer) + referrerHostname: rubiconAdapter.referrerHostname || getHostNameFromReferer(referrer), + channel: 'web', }; - const wrapperName = config.getConfig('rubicon.wrapperName'); - if (wrapperName) { - message.wrapperName = 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) => { @@ -149,7 +188,9 @@ function sendMessage(auctionId, bidWonId) { 'mediaTypes', 'dimensions', 'adserverTargeting', () => stringProperties(cache.targeting[bid.adUnit.adUnitCode] || {}), - 'adSlot' + 'gam', + 'pbAdSlot', + 'pattern' ]); adUnit.bids = []; adUnit.status = 'no-bid'; // default it to be no bid @@ -190,7 +231,8 @@ function sendMessage(auctionId, bidWonId) { clientTimeoutMillis: auctionCache.timeout, samplingFactor, accountId, - adUnits: Object.keys(adUnitMap).map(i => adUnitMap[i]) + adUnits: Object.keys(adUnitMap).map(i => adUnitMap[i]), + requestId: auctionId }; // pick our of top level floor data we want to send! @@ -198,22 +240,50 @@ function sendMessage(auctionId, bidWonId) { if (auctionCache.floorData.location === 'noData') { auction.floors = utils.pick(auctionCache.floorData, [ 'location', - 'fetchStatus' + 'fetchStatus', + 'floorProvider as provider' ]); } else { 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'), 'skipRate', 'fetchStatus', + 'floorMin', 'floorProvider as provider' ]); } } + // gather gdpr info + if (auctionCache.gdprConsent) { + auction.gdpr = utils.pick(auctionCache.gdprConsent, [ + 'gdprApplies as applies', + 'consentString', + 'apiVersion as version' + ]); + } + + // gather session info + if (auctionCache.session) { + message.session = utils.pick(auctionCache.session, [ + 'id', + 'pvid', + 'start', + 'expires' + ]); + if (!utils.isEmpty(auctionCache.session.fpkvs)) { + message.fpkvs = Object.keys(auctionCache.session.fpkvs).map(key => { + return { key, value: auctionCache.session.fpkvs[key] }; + }); + } + } + if (serverConfig) { auction.serverTimeoutMillis = serverConfig.timeout; } @@ -271,7 +341,7 @@ function getBidPrice(bid) { } // otherwise we convert and return try { - return Number(getGlobal().convertCurrency(cpm, currency, 'USD')); + return Number(prebidGlobal.convertCurrency(cpm, currency, 'USD')); } catch (err) { utils.logWarn('Rubicon Analytics Adapter: Could not determine the bidPriceUSD of the bid ', bid); } @@ -289,16 +359,55 @@ export function parseBidResponse(bid, previousBidResponse, auctionFloorData) { 'dealId', 'status', 'mediaType', - 'dimensions', () => utils.pick(bid, [ - 'width', - 'height' - ]), - 'seatBidId', + 'dimensions', () => { + const width = bid.width || bid.playerWidth; + const height = bid.height || bid.playerHeight; + return (width && height) ? {width, height} : undefined; + }, + // Handling use case where pbs sends back 0 or '0' bidIds + 'pbsBidId', pbsBidId => pbsBidId == 0 ? utils.generateUUID() : pbsBidId, + 'seatBidId', seatBidId => seatBidId == 0 ? utils.generateUUID() : seatBidId, 'floorValue', () => utils.deepAccess(bid, 'floorData.floorValue'), - 'floorRule', () => utils.debugTurnedOn() ? utils.deepAccess(bid, 'floorData.floorRule') : undefined + 'floorRuleValue', () => utils.deepAccess(bid, 'floorData.floorRuleValue'), + 'floorRule', () => utils.debugTurnedOn() ? utils.deepAccess(bid, 'floorData.floorRule') : undefined, + 'adomains', () => { + let adomains = utils.deepAccess(bid, 'meta.advertiserDomains'); + return Array.isArray(adomains) && adomains.length > 0 ? adomains.slice(0, 10) : undefined + } ]); } +/* + Filters and converts URL Params into an object and returns only KVs that match the 'utm_KEY' format +*/ +function getUtmParams() { + let search; + + try { + search = utils.parseQS(utils.getWindowLocation().search); + } catch (e) { + search = {}; + } + + return Object.keys(search).reduce((accum, param) => { + if (param.match(/utm_/)) { + accum[param.replace(/utm_/, '')] = search[param]; + } + return accum; + }, {}); +} + +function getFpkvs() { + rubiConf.fpkvs = Object.assign((rubiConf.fpkvs || {}), getUtmParams()); + + // convert all values to strings + Object.keys(rubiConf.fpkvs).forEach(key => { + rubiConf.fpkvs[key] = rubiConf.fpkvs[key] + ''; + }); + + return rubiConf.fpkvs; +} + let samplingFactor = 1; let accountId; // List of known rubicon aliases @@ -317,6 +426,87 @@ function setRubiconAliases(aliasRegistry) { }); } +function getRpaCookie() { + let encodedCookie = storage.getDataFromLocalStorage(COOKIE_NAME); + if (encodedCookie) { + try { + return JSON.parse(window.atob(encodedCookie)); + } catch (e) { + utils.logError(`Rubicon Analytics: Unable to decode ${COOKIE_NAME} value: `, e); + } + } + return {}; +} + +function setRpaCookie(decodedCookie) { + try { + storage.setDataInLocalStorage(COOKIE_NAME, window.btoa(JSON.stringify(decodedCookie))); + } catch (e) { + utils.logError(`Rubicon Analytics: Unable to encode ${COOKIE_NAME} value: `, e); + } +} + +function updateRpaCookie() { + const currentTime = Date.now(); + let decodedRpaCookie = getRpaCookie(); + if ( + !Object.keys(decodedRpaCookie).length || + (currentTime - decodedRpaCookie.lastSeen) > LAST_SEEN_EXPIRE_TIME || + decodedRpaCookie.expires < currentTime + ) { + decodedRpaCookie = { + id: utils.generateUUID(), + start: currentTime, + expires: currentTime + END_EXPIRE_TIME, // six hours later, + } + } + // possible that decodedRpaCookie is undefined, and if it is, we probably are blocked by storage or some other exception + if (Object.keys(decodedRpaCookie).length) { + decodedRpaCookie.lastSeen = currentTime; + decodedRpaCookie.fpkvs = {...decodedRpaCookie.fpkvs, ...getFpkvs()}; + decodedRpaCookie.pvid = rubiConf.pvid; + setRpaCookie(decodedRpaCookie) + } + return decodedRpaCookie; +} + +function subscribeToGamSlots() { + window.googletag.pubads().addEventListener('slotRenderEnded', event => { + const isMatchingAdSlot = utils.isAdUnitCodeMatchingSlot(event.slot); + // loop through auctions and adUnits and mark the info + Object.keys(cache.auctions).forEach(auctionId => { + (Object.keys(cache.auctions[auctionId].bids) || []).forEach(bidId => { + let bid = cache.auctions[auctionId].bids[bidId]; + // if this slot matches this bids adUnit, add the adUnit info + if (isMatchingAdSlot(bid.adUnit.adUnitCode)) { + // 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 + 'advertiserId', advertiserId => utils.isNumber(advertiserId) ? advertiserId : undefined, + 'creativeId', creativeId => utils.isNumber(event.sourceAgnosticCreativeId) ? event.sourceAgnosticCreativeId : utils.isNumber(creativeId) ? creativeId : undefined, + 'lineItemId', lineItemId => utils.isNumber(event.sourceAgnosticLineItemId) ? event.sourceAgnosticLineItemId : utils.isNumber(lineItemId) ? lineItemId : undefined, + 'adSlot', () => event.slot.getAdUnitPath(), + 'isSlotEmpty', () => event.isEmpty || undefined + ]); + } + }); + // 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) + } + } + }); + }); +} + let baseAdapter = adapter({analyticsType: 'endpoint'}); let rubiconAdapter = Object.assign({}, baseAdapter, { referrerHostname: '', @@ -361,7 +551,9 @@ let rubiconAdapter = Object.assign({}, baseAdapter, { }, disableAnalytics() { this.getUrl = baseAdapter.getUrl; - accountId = null; + accountId = undefined; + rubiConf = {}; + cache.gpt.registered = false; baseAdapter.disableAnalytics.apply(this, arguments); }, track({eventType, args}) { @@ -375,23 +567,42 @@ let rubiconAdapter = Object.assign({}, baseAdapter, { ]); cacheEntry.bids = {}; cacheEntry.bidsWon = {}; - cacheEntry.referrer = args.bidderRequests[0].refererInfo.referer; + cacheEntry.gamHasRendered = {}; + cacheEntry.referrer = utils.deepAccess(args, 'bidderRequests.0.refererInfo.referer'); const floorData = utils.deepAccess(args, 'bidderRequests.0.bids.0.floorData'); if (floorData) { cacheEntry.floorData = {...floorData}; } + cacheEntry.gdprConsent = utils.deepAccess(args, 'bidderRequests.0.gdprConsent'); + cacheEntry.session = storage.localStorageIsEnabled() && updateRpaCookie(); cache.auctions[args.auctionId] = cacheEntry; + // register to listen to gpt events if not done yet + 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: Object.assign(cache.auctions[args.auctionId].bids, args.bids.reduce((memo, bid) => { // mark adUnits we expect bidWon events for cache.auctions[args.auctionId].bidsWon[bid.adUnitCode] = false; + if (rubiConf.waitForGamSlots) { + cache.auctions[args.auctionId].gamHasRendered[bid.adUnitCode] = false; + } + memo[bid.bidId] = utils.pick(bid, [ 'bidder', bidder => bidder.toLowerCase(), 'bidId', 'status', () => 'no-bid', // default a bid to no-bid until response is recieved or bid is timed out - 'finalSource as source', + 'source', () => formatSource(bid.src), 'params', (params, bid) => { switch (bid.bidder) { // specify bidder params we want here @@ -451,6 +662,13 @@ let rubiconAdapter = Object.assign({}, baseAdapter, { } return ['banner']; }, + 'gam', () => { + if (utils.deepAccess(bid, 'ortb2Imp.ext.data.adserver.name') === 'gam') { + return {adSlot: bid.ortb2Imp.ext.data.adserver.adslot} + } + }, + 'pbAdSlot', () => utils.deepAccess(bid, 'ortb2Imp.ext.data.pbadslot'), + 'pattern', () => utils.deepAccess(bid, 'ortb2Imp.ext.data.aupname') ]) ]); return memo; @@ -458,10 +676,17 @@ let rubiconAdapter = Object.assign({}, baseAdapter, { break; case BID_RESPONSE: let auctionEntry = cache.auctions[args.auctionId]; + + if (!auctionEntry.bids[args.requestId] && args.originalRequestId) { + auctionEntry.bids[args.requestId] = {...auctionEntry.bids[args.originalRequestId]}; + auctionEntry.bids[args.requestId].bidId = args.requestId; + auctionEntry.bids[args.requestId].bidderDetail = args.targetingBidder; + } + let bid = auctionEntry.bids[args.requestId]; // If floor resolved gptSlot but we have not yet, then update the adUnit to have the adSlot name - if (!utils.deepAccess(bid, 'adUnit.adSlot') && utils.deepAccess(args, 'floorData.matchedFields.gptSlot')) { - bid.adUnit.adSlot = args.floorData.matchedFields.gptSlot; + if (!utils.deepAccess(bid, 'adUnit.gam.adSlot') && utils.deepAccess(args, 'floorData.matchedFields.gptSlot')) { + utils.deepSetValue(bid, 'adUnit.gam.adSlot', args.floorData.matchedFields.gptSlot); } // if we have not set enforcements yet set it if (!utils.deepAccess(auctionEntry, 'floorData.enforcements') && utils.deepAccess(args, 'floorData.enforcements')) { @@ -478,7 +703,7 @@ let rubiconAdapter = Object.assign({}, baseAdapter, { delete bid.error; // it's possible for this to be set by a previous timeout break; case NO_BID: - bid.status = args.status === BID_REJECTED ? 'rejected' : 'no-bid'; + bid.status = args.status === BID_REJECTED ? BID_REJECTED_IPF : 'no-bid'; delete bid.error; break; default: @@ -487,14 +712,26 @@ let rubiconAdapter = Object.assign({}, baseAdapter, { code: 'request-error' }; } - bid.clientLatencyMillis = Date.now() - cache.auctions[args.auctionId].timestamp; + bid.clientLatencyMillis = bid.timeToRespond || Date.now() - cache.auctions[args.auctionId].timestamp; bid.bidResponse = parseBidResponse(args, bid.bidResponse); break; case BIDDER_DONE: + const serverError = utils.deepAccess(args, 'serverErrors.0'); + const serverResponseTimeMs = args.serverResponseTimeMs; args.bids.forEach(bid => { let cachedBid = cache.auctions[bid.auctionId].bids[bid.bidId || bid.requestId]; if (typeof bid.serverResponseTimeMs !== 'undefined') { cachedBid.serverLatencyMillis = bid.serverResponseTimeMs; + } else if (serverResponseTimeMs && bid.source === 's2s') { + cachedBid.serverLatencyMillis = serverResponseTimeMs; + } + // if PBS said we had an error, and this bid has not been processed by BID_RESPONSE YET + if (serverError && (!cachedBid.status || ['no-bid', 'error'].indexOf(cachedBid.status) !== -1)) { + cachedBid.status = 'error'; + cachedBid.error = { + code: pbsErrorMap[serverError.code] || pbsErrorMap[999], + description: serverError.message + } } if (!cachedBid.status) { cachedBid.status = 'no-bid'; @@ -514,7 +751,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; @@ -529,16 +766,20 @@ 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 => { let auctionCache = cache.auctions[badBid.auctionId]; let bid = auctionCache.bids[badBid.bidId || badBid.requestId]; - bid.status = 'error'; - bid.error = { - code: 'timeout-error' - }; + // might be set already by bidder-done, so do not overwrite + if (bid.status !== 'error') { + bid.status = 'error'; + bid.error = { + code: 'timeout-error', + message: 'marked by prebid.js as timeout' // will help us diff if timeout was set by PBS or PBJS + }; + } }); break; } @@ -547,7 +788,8 @@ let rubiconAdapter = Object.assign({}, baseAdapter, { adapterManager.registerAnalyticsAdapter({ adapter: rubiconAdapter, - code: 'rubicon' + code: 'rubicon', + gvlid: RUBICON_GVL_ID }); export default rubiconAdapter; diff --git a/modules/rubiconBidAdapter.js b/modules/rubiconBidAdapter.js index 0c563987331..078b5404baf 100644 --- a/modules/rubiconBidAdapter.js +++ b/modules/rubiconBidAdapter.js @@ -2,27 +2,23 @@ import * as utils from '../src/utils.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; import {config} from '../src/config.js'; import {BANNER, VIDEO} from '../src/mediaTypes.js'; +import find from 'core-js-pure/features/array/find.js'; +import { Renderer } from '../src/Renderer.js'; +import { getGlobal } from '../src/prebidGlobal.js'; const DEFAULT_INTEGRATION = 'pbjs_lite'; const DEFAULT_PBS_INTEGRATION = 'pbjs'; +const DEFAULT_RENDERER_URL = 'https://video-outstream.rubiconproject.com/apex-2.0.0.js'; +// renderer code at https://github.com/rubicon-project/apex2 -// always use https, regardless of whether or not current page is secure -export const FASTLANE_ENDPOINT = 'https://fastlane.rubiconproject.com/a/api/fastlane.json'; -export const VIDEO_ENDPOINT = 'https://prebid-server.rubiconproject.com/openrtb2/auction'; -export const SYNC_ENDPOINT = 'https://eus.rubiconproject.com/usync.html'; +let rubiConf = {}; +// we are saving these as global to this module so that if a pub accidentally overwrites the entire +// rubicon object, then we do not lose other data +config.getConfig('rubicon', config => { + utils.mergeDeep(rubiConf, config.rubicon); +}); const GVLID = 52; -const DIGITRUST_PROP_NAMES = { - FASTLANE: { - id: 'dt.id', - keyv: 'dt.keyv', - pref: 'dt.pref' - }, - PREBID_SERVER: { - id: 'id', - keyv: 'keyv' - } -}; var sizeMap = { 1: '468x60', @@ -110,7 +106,11 @@ var sizeMap = { 274: '1800x200', 278: '320x500', 282: '320x400', - 288: '640x380' + 288: '640x380', + 548: '500x1000', + 550: '980x480', + 552: '300x200', + 558: '640x640' }; utils._each(sizeMap, (item, key) => sizeMap[item] = key); @@ -162,9 +162,9 @@ export const spec = { source: { tid: bidRequest.transactionId }, - tmax: config.getConfig('TTL') || 1000, + tmax: bidderRequest.timeout, imp: [{ - exp: 300, + exp: config.getConfig('s2sConfig.defaultTtl'), id: bidRequest.adUnitCode, secure: 1, ext: { @@ -174,9 +174,13 @@ export const spec = { }], ext: { prebid: { + channel: { + name: 'pbjs', + version: $$PREBID_GLOBAL$$.version + }, cache: { vastxml: { - returnCreative: false // don't return the VAST + returnCreative: rubiConf.returnVast === true } }, targeting: { @@ -187,7 +191,7 @@ export const spec = { }, bidders: { rubicon: { - integration: config.getConfig('rubicon.int_type') || DEFAULT_PBS_INTEGRATION + integration: rubiConf.int_type || DEFAULT_PBS_INTEGRATION } } } @@ -201,13 +205,23 @@ export const spec = { } } + let modules = (getGlobal()).installedModules; + if (modules && (!modules.length || modules.indexOf('rubiconAnalyticsAdapter') !== -1)) { + utils.deepSetValue(data, 'ext.prebid.analytics', {'rubicon': {'client-analytics': true}}); + } + let bidFloor; - if (typeof bidRequest.getFloor === 'function' && !config.getConfig('rubicon.disableFloors')) { - let floorInfo = bidRequest.getFloor({ - currency: 'USD', - mediaType: 'video', - size: parseSizes(bidRequest, 'video') - }); + if (typeof bidRequest.getFloor === 'function' && !rubiConf.disableFloors) { + let floorInfo; + try { + floorInfo = bidRequest.getFloor({ + currency: 'USD', + mediaType: 'video', + size: parseSizes(bidRequest, 'video') + }); + } catch (e) { + utils.logError('Rubicon: getFloor threw an error: ', e); + } bidFloor = typeof floorInfo === 'object' && floorInfo.currency === 'USD' && !isNaN(parseInt(floorInfo.floor)) ? parseFloat(floorInfo.floor) : undefined; } else { bidFloor = parseFloat(utils.deepAccess(bidRequest, 'params.floor')); @@ -222,11 +236,6 @@ export const spec = { addVideoParameters(data, bidRequest); - const digiTrust = _getDigiTrustQueryParams(bidRequest, 'PREBID_SERVER'); - if (digiTrust) { - utils.deepSetValue(data, 'user.ext.digitrust', digiTrust); - } - if (bidderRequest.gdprConsent) { // note - gdprApplies & consentString may be undefined in certain use-cases for consentManagement module let gdprApplies; @@ -242,59 +251,15 @@ export const spec = { utils.deepSetValue(data, 'regs.ext.us_privacy', bidderRequest.uspConsent); } - if (bidRequest.userId && typeof bidRequest.userId === 'object' && - (bidRequest.userId.tdid || bidRequest.userId.pubcid || bidRequest.userId.lipb || bidRequest.userId.idl_env)) { - utils.deepSetValue(data, 'user.ext.eids', []); - - if (bidRequest.userId.tdid) { - data.user.ext.eids.push({ - source: 'adserver.org', - uids: [{ - id: bidRequest.userId.tdid, - ext: { - rtiPartner: 'TDID' - } - }] - }); - } - - if (bidRequest.userId.pubcid) { - data.user.ext.eids.push({ - source: 'pubcommon', - uids: [{ - id: bidRequest.userId.pubcid, - }] - }); - } - - // support liveintent ID - if (bidRequest.userId.lipb && bidRequest.userId.lipb.lipbid) { - data.user.ext.eids.push({ - source: 'liveintent.com', - uids: [{ - id: bidRequest.userId.lipb.lipbid - }] - }); - - data.user.ext.tpid = { - source: 'liveintent.com', - uid: bidRequest.userId.lipb.lipbid - }; - - if (Array.isArray(bidRequest.userId.lipb.segments) && bidRequest.userId.lipb.segments.length) { - utils.deepSetValue(data, 'rp.target.LIseg', bidRequest.userId.lipb.segments); - } - } + const eids = utils.deepAccess(bidderRequest, 'bids.0.userIdAsEids'); + if (eids && eids.length) { + utils.deepSetValue(data, 'user.ext.eids', eids); + } - // support identityLink (aka LiveRamp) - if (bidRequest.userId.idl_env) { - data.user.ext.eids.push({ - source: 'liveramp.com', - uids: [{ - id: bidRequest.userId.idl_env - }] - }); - } + // set user.id value from config value + const configUserId = config.getConfig('user.id'); + if (configUserId) { + utils.deepSetValue(data, 'user.id', configUserId); } if (config.getConfig('coppa') === true) { @@ -305,44 +270,22 @@ 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: {} - } - }; + const multibid = config.getConfig('multibid'); + if (multibid) { + utils.deepSetValue(data, 'ext.prebid.multibid', multibid.reduce((result, i) => { + let obj = {}; - if (!utils.isEmpty(siteData)) { - bidderData.config.fpd.site = siteData; - } - - if (!utils.isEmpty(userData)) { - bidderData.config.fpd.user = userData; - } + Object.keys(i).forEach(key => { + obj[key.toLowerCase()] = i[key]; + }); - utils.deepSetValue(data, 'ext.prebid.bidderconfig.0', bidderData); - } + result.push(obj); - /** - * 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); + return result; + }, [])); } - /** - * GAM Ad Unit - * @type {(string|undefined)} - */ - const gamAdUnit = utils.deepAccess(bidRequest, 'fpd.context.adServer.adSlot'); - if (typeof gamAdUnit === 'string' && gamAdUnit) { - utils.deepSetValue(data.imp[0].ext, 'context.data.adslot', gamAdUnit); - } + applyFPD(bidRequest, VIDEO, data); // if storedAuctionResponse has been set, pass SRID if (bidRequest.storedAuctionResponse) { @@ -354,19 +297,19 @@ export const spec = { return { method: 'POST', - url: VIDEO_ENDPOINT, + url: `https://${rubiConf.videoHost || 'prebid-server'}.rubiconproject.com/openrtb2/auction`, data, bidRequest } }); - if (config.getConfig('rubicon.singleRequest') !== true) { + if (rubiConf.singleRequest !== true) { // bids are not grouped if single request mode is not enabled requests = videoRequests.concat(bidRequests.filter(bidRequest => bidType(bidRequest) === 'banner').map(bidRequest => { const bidParams = spec.createSlotParams(bidRequest, bidderRequest); return { method: 'GET', - url: FASTLANE_ENDPOINT, + url: `https://${rubiConf.bannerHost || 'fastlane'}.rubiconproject.com/a/api/fastlane.json`, data: spec.getOrderedParams(bidParams).reduce((paramString, key) => { const propValue = bidParams[key]; return ((utils.isStr(propValue) && propValue !== '') || utils.isNumber(propValue)) ? `${paramString}${encodeParam(key, propValue)}&` : paramString; @@ -397,7 +340,7 @@ export const spec = { // SRA request returns grouped bidRequest arrays not a plain bidRequest aggregate.push({ method: 'GET', - url: FASTLANE_ENDPOINT, + url: `https://${rubiConf.bannerHost || 'fastlane'}.rubiconproject.com/a/api/fastlane.json`, data: spec.getOrderedParams(combinedSlotParams).reduce((paramString, key) => { const propValue = combinedSlotParams[key]; return ((utils.isStr(propValue) && propValue !== '') || utils.isNumber(propValue)) ? `${paramString}${encodeParam(key, propValue)}&` : paramString; @@ -414,6 +357,7 @@ export const spec = { getOrderedParams: function(params) { const containsTgV = /^tg_v/ const containsTgI = /^tg_i/ + const containsUId = /^eid_|^tpid_/ const orderedParams = [ 'account_id', @@ -426,17 +370,15 @@ export const spec = { 'gdpr_consent', 'us_privacy', 'rp_schain', - 'tpid_tdid', - 'tpid_liveintent.com', - 'tg_v.LIseg', - 'dt.id', - 'dt.keyv', - 'dt.pref', - 'rf', - 'p_geo.latitude', - 'p_geo.longitude', - 'kw' - ].concat(Object.keys(params).filter(item => containsTgV.test(item))) + ].concat(Object.keys(params).filter(item => containsUId.test(item))) + .concat([ + 'x_liverampidl', + 'ppuid', + 'rf', + 'p_geo.latitude', + 'p_geo.longitude', + 'kw' + ]).concat(Object.keys(params).filter(item => containsTgV.test(item))) .concat(Object.keys(params).filter(item => containsTgI.test(item))) .concat([ 'tk_flint', @@ -504,17 +446,15 @@ export const spec = { const [latitude, longitude] = params.latLong || []; - const configIntType = config.getConfig('rubicon.int_type'); - const data = { 'account_id': params.accountId, 'site_id': params.siteId, 'zone_id': params.zoneId, 'size_id': parsedSizes[0], 'alt_size_ids': parsedSizes.slice(1).join(',') || undefined, - 'rp_floor': (params.floor = parseFloat(params.floor)) > 0.01 ? params.floor : 0.01, + 'rp_floor': (params.floor = parseFloat(params.floor)) >= 0.01 ? params.floor : undefined, 'rp_secure': '1', - 'tk_flint': `${configIntType || DEFAULT_INTEGRATION}_v$prebid.version$`, + 'tk_flint': `${rubiConf.int_type || DEFAULT_INTEGRATION}_v$prebid.version$`, 'x_source.tid': bidRequest.transactionId, 'x_source.pchain': params.pchain, 'p_screen_res': _getScreenResolution(), @@ -526,12 +466,17 @@ export const spec = { }; // If floors module is enabled and we get USD floor back, send it in rp_hard_floor else undfined - if (typeof bidRequest.getFloor === 'function' && !config.getConfig('rubicon.disableFloors')) { - let floorInfo = bidRequest.getFloor({ - currency: 'USD', - mediaType: 'banner', - size: '*' - }); + if (typeof bidRequest.getFloor === 'function' && !rubiConf.disableFloors) { + let floorInfo; + try { + floorInfo = bidRequest.getFloor({ + currency: 'USD', + mediaType: 'banner', + size: '*' + }); + } catch (e) { + utils.logError('Rubicon: getFloor threw an error: ', e); + } data['rp_hard_floor'] = typeof floorInfo === 'object' && floorInfo.currency === 'USD' && !isNaN(parseInt(floorInfo.floor)) ? floorInfo.floor : undefined; } @@ -539,23 +484,47 @@ export const spec = { // For SRA we need to explicitly put empty semi colons so AE treats it as empty, instead of copying the latter value data['p_pos'] = (params.position === 'atf' || params.position === 'btf') ? params.position : ''; - if (bidRequest.userId) { - if (bidRequest.userId.tdid) { - data['tpid_tdid'] = bidRequest.userId.tdid; - } - - // support liveintent ID - if (bidRequest.userId.lipb && bidRequest.userId.lipb.lipbid) { - data['tpid_liveintent.com'] = bidRequest.userId.lipb.lipbid; - if (Array.isArray(bidRequest.userId.lipb.segments) && bidRequest.userId.lipb.segments.length) { - data['tg_v.LIseg'] = bidRequest.userId.lipb.segments.join(','); + // pass publisher provided userId if configured + const configUserId = config.getConfig('user.id'); + if (configUserId) { + data['ppuid'] = configUserId; + } + // loop through userIds and add to request + if (bidRequest.userIdAsEids) { + bidRequest.userIdAsEids.forEach(eid => { + try { + // special cases + if (eid.source === 'adserver.org') { + data['tpid_tdid'] = eid.uids[0].id; + data['eid_adserver.org'] = eid.uids[0].id; + } else if (eid.source === 'liveintent.com') { + data['tpid_liveintent.com'] = eid.uids[0].id; + data['eid_liveintent.com'] = eid.uids[0].id; + if (eid.ext && Array.isArray(eid.ext.segments) && eid.ext.segments.length) { + data['tg_v.LIseg'] = eid.ext.segments.join(','); + } + } else if (eid.source === 'liveramp.com') { + data['x_liverampidl'] = eid.uids[0].id; + } 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.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 || ''}`; + } + // send AE "ppuid" signal if exists, and hasn't already been sent + if (!data['ppuid']) { + // get the first eid.uids[*].ext.stype === 'ppuid', if one exists + const ppId = find(eid.uids, uid => uid.ext && uid.ext.stype === 'ppuid'); + if (ppId && ppId.id) { + data['ppuid'] = ppId.id; + } + } + } catch (e) { + utils.logWarn('Rubicon: error reading eid:', eid, e); } - } - - // support identityLink (aka LiveRamp) - if (bidRequest.userId.idl_env) { - data['tpid_liveramp.com'] = bidRequest.userId.idl_env; - } + }); } if (bidderRequest.gdprConsent) { @@ -570,53 +539,9 @@ 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(/^\/+/, ''); - } + data['rp_maxbids'] = bidderRequest.bidLimit || 1; - // digitrust properties - const digitrustParams = _getDigiTrustQueryParams(bidRequest, 'FASTLANE'); - Object.assign(data, digitrustParams); + applyFPD(bidRequest, BANNER, data); if (config.getConfig('coppa') === true) { data['coppa'] = 1; @@ -684,7 +609,7 @@ export const spec = { cpm: bid.price || 0, bidderCode: seatbid.seat, ttl: 300, - netRevenue: config.getConfig('rubicon.netRevenue') !== false, // If anything other than false, netRev is true + netRevenue: rubiConf.netRevenue !== false, // If anything other than false, netRev is true width: bid.w || utils.deepAccess(bidRequest, 'mediaTypes.video.w') || utils.deepAccess(bidRequest, 'params.video.playerWidth'), height: bid.h || utils.deepAccess(bidRequest, 'mediaTypes.video.h') || utils.deepAccess(bidRequest, 'params.video.playerHeight'), }; @@ -697,6 +622,14 @@ export const spec = { bidObject.dealId = bid.dealid; } + if (bid.adomain) { + utils.deepSetValue(bidObject, 'meta.advertiserDomains', Array.isArray(bid.adomain) ? bid.adomain : [bid.adomain]); + } + + if (utils.deepAccess(bid, 'ext.bidder.rp.advid')) { + utils.deepSetValue(bidObject, 'meta.advertiserId', bid.ext.bidder.rp.advid); + } + let serverResponseTimeMs = utils.deepAccess(responseObj, 'ext.responsetimemillis.rubicon'); if (bidRequest && serverResponseTimeMs) { bidRequest.serverResponseTimeMs = serverResponseTimeMs; @@ -704,6 +637,7 @@ export const spec = { if (utils.deepAccess(bid, 'ext.prebid.type') === VIDEO) { bidObject.mediaType = VIDEO; + utils.deepSetValue(bidObject, 'meta.mediaType', VIDEO); const extPrebidTargeting = utils.deepAccess(bid, 'ext.prebid.targeting'); // If ext.prebid.targeting exists, add it as a property value named 'adserverTargeting' @@ -725,6 +659,11 @@ export const spec = { if (bid.adm) { bidObject.vastXml = bid.adm; } if (bid.nurl) { bidObject.vastUrl = bid.nurl; } if (!bidObject.vastUrl && bid.nurl) { bidObject.vastUrl = bid.nurl; } + + const videoContext = utils.deepAccess(bidRequest, 'mediaTypes.video.context'); + if (videoContext.toLowerCase() === 'outstream') { + bidObject.renderer = outstreamRenderer(bidObject); + } } else { utils.logWarn('Rubicon: video response received non-video media type'); } @@ -737,6 +676,8 @@ export const spec = { } let ads = responseObj.ads; + let lastImpId; + let multibid = 0; // video ads array is wrapped in an object if (typeof bidRequest === 'object' && !Array.isArray(bidRequest) && bidType(bidRequest) === 'video' && typeof ads === 'object') { @@ -749,12 +690,14 @@ export const spec = { } return ads.reduce((bids, ad, i) => { + (ad.impression_id && lastImpId === ad.impression_id) ? multibid++ : lastImpId = ad.impression_id; + if (ad.status !== 'ok') { return bids; } // associate bidRequests; assuming ads matches bidRequest - const associatedBidRequest = Array.isArray(bidRequest) ? bidRequest[i] : bidRequest; + const associatedBidRequest = Array.isArray(bidRequest) ? bidRequest[i - multibid] : bidRequest; if (associatedBidRequest && typeof associatedBidRequest === 'object') { let bid = { @@ -764,12 +707,12 @@ export const spec = { cpm: ad.cpm || 0, dealId: ad.deal, ttl: 300, // 5 minutes - netRevenue: config.getConfig('rubicon.netRevenue') !== false, // If anything other than false, netRev is true + netRevenue: rubiConf.netRevenue !== false, // If anything other than false, netRev is true rubicon: { advertiserId: ad.advertiser, networkId: ad.network }, meta: { - advertiserId: ad.advertiser, networkId: ad.network + advertiserId: ad.advertiser, networkId: ad.network, mediaType: BANNER } }; @@ -777,6 +720,10 @@ export const spec = { bid.mediaType = ad.creative_type; } + if (ad.adomain) { + bid.meta.advertiserDomains = Array.isArray(ad.adomain) ? ad.adomain : [ad.adomain]; + } + if (ad.creative_type === VIDEO) { bid.width = associatedBidRequest.params.video.playerWidth; bid.height = associatedBidRequest.params.video.playerHeight; @@ -807,7 +754,7 @@ export const spec = { }, getUserSyncs: function (syncOptions, responses, gdprConsent, uspConsent) { if (!hasSynced && syncOptions.iframeEnabled) { - // data is only assigned if params are available to pass to SYNC_ENDPOINT + // data is only assigned if params are available to pass to syncEndpoint let params = ''; if (gdprConsent && typeof gdprConsent.consentString === 'string') { @@ -826,7 +773,7 @@ export const spec = { hasSynced = true; return { type: 'iframe', - url: SYNC_ENDPOINT + params + url: `https://${rubiConf.syncHost || 'eus'}.rubiconproject.com/usync.html` + params }; } }, @@ -849,38 +796,6 @@ function _getScreenResolution() { return [window.screen.width, window.screen.height].join('x'); } -function _getDigiTrustQueryParams(bidRequest = {}, endpointName) { - if (!endpointName || !DIGITRUST_PROP_NAMES[endpointName]) { - return null; - } - const propNames = DIGITRUST_PROP_NAMES[endpointName]; - - function getDigiTrustId() { - const bidRequestDigitrust = utils.deepAccess(bidRequest, 'userId.digitrustid.data'); - if (bidRequestDigitrust) { - return bidRequestDigitrust; - } - - let digiTrustUser = (window.DigiTrust && (config.getConfig('digiTrustId') || window.DigiTrust.getUser({member: 'T9QSFKPDN9'}))); - return (digiTrustUser && digiTrustUser.success && digiTrustUser.identity) || null; - } - - let digiTrustId = getDigiTrustId(); - // Verify there is an ID and this user has not opted out - if (!digiTrustId || (digiTrustId.privacy && digiTrustId.privacy.optout)) { - return null; - } - - const digiTrustQueryParams = { - [propNames.id]: digiTrustId.id, - [propNames.keyv]: digiTrustId.keyv - }; - if (propNames.pref) { - digiTrustQueryParams[propNames.pref] = 0; - } - return digiTrustQueryParams; -} - /** * @param {BidRequest} bidRequest * @param bidderRequest @@ -908,6 +823,64 @@ function _renderCreative(script, impId) { `; } +function hideGoogleAdsDiv(adUnit) { + const el = adUnit.querySelector("div[id^='google_ads']"); + if (el) { + el.style.setProperty('display', 'none'); + } +} + +function hideSmartAdServerIframe(adUnit) { + const el = adUnit.querySelector("script[id^='sas_script']"); + const nextSibling = el && el.nextSibling; + if (nextSibling && nextSibling.localName === 'iframe') { + nextSibling.style.setProperty('display', 'none'); + } +} + +function renderBid(bid) { + // hide existing ad units + const adUnitElement = document.getElementById(bid.adUnitCode); + hideGoogleAdsDiv(adUnitElement); + hideSmartAdServerIframe(adUnitElement); + + // configure renderer + const config = bid.renderer.getConfig(); + bid.renderer.push(() => { + window.MagniteApex.renderAd({ + width: bid.width, + height: bid.height, + vastUrl: bid.vastUrl, + placement: { + attachTo: `#${bid.adUnitCode}`, + align: config.align || 'center', + position: config.position || 'append' + }, + closeButton: config.closeButton || false, + label: config.label || undefined, + collapse: config.collapse || true + }); + }); +} + +function outstreamRenderer(rtbBid) { + const renderer = Renderer.install({ + id: rtbBid.adId, + url: rubiConf.rendererUrl || DEFAULT_RENDERER_URL, + config: rubiConf.rendererConfig || {}, + loaded: false, + adUnitCode: rtbBid.adUnitCode + }); + + try { + renderer.setRender(renderBid); + } catch (err) { + utils.logWarn('Prebid Error calling setRender on renderer', err); + } + + return renderer; +} + function parseSizes(bid, mediaType) { let params = bid.params; if (mediaType === 'video') { @@ -995,6 +968,80 @@ function addVideoParameters(data, bidRequest) { data.imp[0].video.h = size[1] } +function applyFPD(bidRequest, mediaType, data) { + const BID_FPD = { + user: {ext: {data: {...bidRequest.params.visitor}}}, + site: {ext: {data: {...bidRequest.params.inventory}}} + }; + + if (bidRequest.params.keywords) BID_FPD.site.keywords = (utils.isArray(bidRequest.params.keywords)) ? bidRequest.params.keywords.join(',') : bidRequest.params.keywords; + + let fpd = utils.mergeDeep({}, config.getConfig('ortb2') || {}, BID_FPD); + let impData = utils.deepAccess(bidRequest.ortb2Imp, 'ext.data') || {}; + const SEGTAX = {user: [3], site: [1, 2]}; + const MAP = {user: 'tg_v.', site: 'tg_i.', adserver: 'tg_i.dfp_ad_unit_code', pbadslot: 'tg_i.pbadslot', keywords: 'kw'}; + const validate = function(prop, key, parentName) { + if (key === 'data' && Array.isArray(prop)) { + return prop.filter(name => name.segment && utils.deepAccess(name, 'ext.segtax') && SEGTAX[parentName] && + SEGTAX[parentName].indexOf(utils.deepAccess(name, 'ext.segtax')) !== -1).map(value => { + let segments = value.segment.filter(obj => obj.id).reduce((result, obj) => { + result.push(obj.id); + return result; + }, []); + if (segments.length > 0) return segments.toString(); + }).toString(); + } else 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(); + } + }; + const addBannerData = function(obj, name, key, isParent = true) { + let val = validate(obj, key, name); + let loc = (MAP[key] && isParent) ? `${MAP[key]}` : (key === 'data') ? `${MAP[name]}iab` : `${MAP[name]}${key}`; + data[loc] = (data[loc]) ? data[loc].concat(',', val) : val; + } + + Object.keys(impData).forEach((key) => { + if (key === 'adserver') { + ['name', 'adslot'].forEach(prop => { + if (impData[key][prop]) impData[key][prop] = impData[key][prop].toString().replace(/^\/+/, ''); + }); + } else if (key === 'pbadslot') { + impData[key] = impData[key].toString().replace(/^\/+/, ''); + } + }); + + if (mediaType === BANNER) { + ['site', 'user'].forEach(name => { + Object.keys(fpd[name]).forEach((key) => { + if (name === 'site' && key === 'content' && fpd[name][key].data) { + addBannerData(fpd[name][key].data, name, 'data'); + } else if (key !== 'ext') { + addBannerData(fpd[name][key], name, key); + } else if (fpd[name][key].data) { + Object.keys(fpd[name].ext.data).forEach((key) => { + addBannerData(fpd[name].ext.data[key], name, key, false); + }); + } + }); + }); + Object.keys(impData).forEach((key) => { + (key === 'adserver') ? addBannerData(impData[key].adslot, name, key) : addBannerData(impData[key], 'site', key); + }); + } else { + if (Object.keys(impData).length) { + utils.mergeDeep(data.imp[0].ext, {data: impData}); + } + + utils.mergeDeep(data, fpd); + } +} + /** * @param sizes * @returns {*} @@ -1072,6 +1119,7 @@ function bidType(bid, log = false) { } } +export const resetRubiConf = () => rubiConf = {}; export function masSizeOrdering(sizes) { const MAS_SIZE_PRIORITY = [15, 2, 9]; @@ -1141,7 +1189,6 @@ export function hasValidVideoParams(bid) { var requiredParams = { mimes: arrayType, protocols: arrayType, - maxduration: numberType, linearity: numberType, api: arrayType } @@ -1166,7 +1213,7 @@ export function hasValidSupplyChainParams(schain) { if (!schain.nodes) return isValid; isValid = schain.nodes.reduce((status, node) => { if (!status) return status; - return requiredFields.every(field => node[field]); + return requiredFields.every(field => node.hasOwnProperty(field)); }, true); if (!isValid) utils.logError('Rubicon: required schain params missing'); return isValid; diff --git a/modules/rubiconBidAdapter.md b/modules/rubiconBidAdapter.md index beb29a6baf1..0ef22a81972 100644 --- a/modules/rubiconBidAdapter.md +++ b/modules/rubiconBidAdapter.md @@ -70,9 +70,9 @@ globalsupport@rubiconproject.com for more information. bids: [{ bidder: 'rubicon', params: { - accountId: '7780', - siteId: '87184', - zoneId: '412394', + accountId: 7780, + siteId: 87184, + zoneId: 412394, video: { language: 'en' } 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/seedtagBidAdapter.js b/modules/seedtagBidAdapter.js index 018339fabe4..c25d833a71c 100644 --- a/modules/seedtagBidAdapter.js +++ b/modules/seedtagBidAdapter.js @@ -6,20 +6,47 @@ const BIDDER_CODE = 'seedtag'; const SEEDTAG_ALIAS = 'st'; const SEEDTAG_SSP_ENDPOINT = 'https://s.seedtag.com/c/hb/bid'; const SEEDTAG_SSP_ONTIMEOUT_ENDPOINT = 'https://s.seedtag.com/se/hb/timeout'; - +const ALLOWED_PLACEMENTS = { + inImage: true, + inScreen: true, + inArticle: true, + banner: true, + video: true +} const mediaTypesMap = { [BANNER]: 'display', [VIDEO]: 'video' }; +const deviceConnection = { + FIXED: 'fixed', + MOBILE: 'mobile', + UNKNOWN: 'unknown' +}; + +const getConnectionType = () => { + const connection = navigator.connection || navigator.mozConnection || navigator.webkitConnection || {} + switch (connection.type || connection.effectiveType) { + case 'wifi': + case 'ethernet': + return deviceConnection.FIXED + case 'cellular': + case 'wimax': + return deviceConnection.MOBILE + default: + const isMobile = /iPad|iPhone|iPod/.test(navigator.userAgent) || /android/i.test(navigator.userAgent) + return isMobile ? deviceConnection.UNKNOWN : deviceConnection.FIXED + } +}; + function mapMediaType(seedtagMediaType) { if (seedtagMediaType === 'display') return BANNER; if (seedtagMediaType === 'video') return VIDEO; else return seedtagMediaType; } -function getMediaTypeFromBid(bid) { - return bid.mediaTypes && Object.keys(bid.mediaTypes)[0] +function hasVideoMediaType(bid) { + return !!bid.mediaTypes && !!bid.mediaTypes.video } function hasMandatoryParams(params) { @@ -27,14 +54,11 @@ function hasMandatoryParams(params) { !!params.publisherId && !!params.adUnitId && !!params.placement && - (params.placement === 'inImage' || - params.placement === 'inScreen' || - params.placement === 'banner' || - params.placement === 'video') + !!ALLOWED_PLACEMENTS[params.placement] ); } -function hasVideoMandatoryParams(mediaTypes) { +function hasMandatoryVideoParams(mediaTypes) { const isVideoInStream = !!mediaTypes.video && mediaTypes.video.context === 'instream'; const isPlayerSize = @@ -52,20 +76,21 @@ function buildBidRequests(validBidRequests) { return mediaTypesMap[pbjsType]; } ); + const bidRequest = { id: validBidRequest.bidId, transactionId: validBidRequest.transactionId, sizes: validBidRequest.sizes, supplyTypes: mediaTypes, adUnitId: params.adUnitId, - placement: params.placement + placement: params.placement, }; if (params.adPosition) { bidRequest.adPosition = params.adPosition; } - if (params.video) { + if (hasVideoMediaType(validBidRequest)) { bidRequest.videoParams = params.video || {}; bidRequest.videoParams.w = validBidRequest.mediaTypes.video.playerSize[0][0]; @@ -88,8 +113,10 @@ function buildBid(seedtagBid) { currency: seedtagBid.currency, netRevenue: true, mediaType: mediaType, - ttl: seedtagBid.ttl + ttl: seedtagBid.ttl, + nurl: seedtagBid.nurl }; + if (mediaType === VIDEO) { bid.vastXml = seedtagBid.content; } else { @@ -124,8 +151,8 @@ export const spec = { * @return boolean True if this is a valid bid, and false otherwise. */ isBidRequestValid(bid) { - return getMediaTypeFromBid(bid) === VIDEO - ? hasMandatoryParams(bid.params) && hasVideoMandatoryParams(bid.mediaTypes) + return hasVideoMediaType(bid) + ? hasMandatoryParams(bid.params) && hasMandatoryVideoParams(bid.mediaTypes) : hasMandatoryParams(bid.params); }, @@ -142,6 +169,7 @@ export const spec = { cmp: !!bidderRequest.gdprConsent, timeout: bidderRequest.timeout, version: '$prebid.version$', + connectionType: getConnectionType(), bidRequests: buildBidRequests(validBidRequests) }; @@ -198,8 +226,18 @@ export const spec = { * @param {data} Containing timeout specific data */ onTimeout(data) { - getTimeoutUrl(data); - utils.triggerPixel(SEEDTAG_SSP_ONTIMEOUT_ENDPOINT); + const url = getTimeoutUrl(data); + utils.triggerPixel(url); + }, + + /** + * Function to call when the adapter wins the auction + * @param {bid} Bid information received from the server + */ + onBidWon: function (bid) { + if (bid && bid.nurl) { + utils.triggerPixel(bid.nurl); + } } } registerBidder(spec); diff --git a/modules/sharedIdSystem.js b/modules/sharedIdSystem.js index 5c2a3df0595..defa8d22639 100644 --- a/modules/sharedIdSystem.js +++ b/modules/sharedIdSystem.js @@ -8,6 +8,7 @@ import * as utils from '../src/utils.js' import {ajax} from '../src/ajax.js'; import {submodule} from '../src/hook.js'; +import { uspDataHandler, coppaDataHandler } from '../src/adapterManager.js'; const MODULE_NAME = 'sharedId'; const ID_SVC = 'https://id.sharedid.org/id'; @@ -21,6 +22,7 @@ const TIME_MAX = Math.pow(2, 48) - 1; const TIME_LEN = 10; const RANDOM_LEN = 16; const id = factory(); +const GVLID = 887; /** * Constructs cookie value * @param value @@ -275,6 +277,26 @@ 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) { + const usPrivacyString = uspDataHandler.getConsentData(); + let sharedIdUrl = ID_SVC; + if (usPrivacyString) { + sharedIdUrl = `${ID_SVC}?us_privacy=${usPrivacyString}`; + } + if (!consentData || typeof consentData.gdprApplies !== 'boolean' || !consentData.gdprApplies) return sharedIdUrl; + if (usPrivacyString) { + sharedIdUrl = `${sharedIdUrl}&gdpr=1&gdpr_consent=${consentData.consentString}` + return sharedIdUrl; + } + sharedIdUrl = `${ID_SVC}?gdpr=1&gdpr_consent=${consentData.consentString}`; + return sharedIdUrl +} + /** @type {Submodule} */ export const sharedIdSubmodule = { /** @@ -283,6 +305,11 @@ export const sharedIdSubmodule = { */ name: MODULE_NAME, + /** + * Vendor id of Prebid + * @type {Number} + */ + gvlid: GVLID, /** * decode the stored id value for passing to bid requests * @function @@ -296,24 +323,37 @@ export const sharedIdSubmodule = { /** * performs action to obtain id and return a value. * @function - * @param {SubmoduleParams} [configParams] + * @param {SubmoduleConfig} [config] + * @param {ConsentData|undefined} consentData * @returns {sharedId} */ - getId(configParams) { + getId(config, consentData) { + const coppa = coppaDataHandler.getCoppa(); + if (coppa) { + utils.logInfo('SharedId: IDs not provided for coppa requests, exiting SharedId'); + return; + } 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}; }, /** * performs actions even if the id exists and returns a value - * @param configParams + * @param config + * @param consentData * @param storedId * @returns {{callback: *}} */ - extendId(configParams, storedId) { + extendId(config, consentData, storedId) { + const coppa = coppaDataHandler.getCoppa(); + if (coppa) { + utils.logInfo('SharedId: IDs not provided for coppa requests, exiting SharedId'); + return; + } + const configParams = (config && config.params) || {}; utils.logInfo('SharedId: Existing shared id ' + storedId.id); const resp = function (callback) { const needSync = isIdSynced(configParams, storedId); @@ -322,7 +362,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 0d183be05df..68ccde0da46 100644 --- a/modules/sharethroughBidAdapter.js +++ b/modules/sharethroughBidAdapter.js @@ -1,7 +1,8 @@ import { registerBidder } from '../src/adapters/bidderFactory.js'; import * as utils from '../src/utils.js'; +import { config } from '../src/config.js'; -const VERSION = '3.2.1'; +const VERSION = '3.3.2'; const BIDDER_CODE = 'sharethrough'; const STR_ENDPOINT = 'https://btlr.sharethrough.com/WYu2BXv1/v1'; const DEFAULT_SIZE = [1, 1]; @@ -31,6 +32,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,8 +49,8 @@ export const sharethroughAdapterSpec = { query.us_privacy = bidderRequest.uspConsent } - if (bidRequest.userId && bidRequest.userId.tdid) { - query.ttduid = bidRequest.userId.tdid; + if (config.getConfig('coppa') === true) { + query.coppa = true } if (bidRequest.schain) { @@ -58,6 +61,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 = { @@ -67,7 +78,7 @@ export const sharethroughAdapterSpec = { }; return { - method: 'GET', + method: 'POST', url: STR_ENDPOINT, data: query, strData: strData @@ -98,6 +109,7 @@ export const sharethroughAdapterSpec = { currency: 'USD', netRevenue: true, ttl: 360, + meta: { advertiserDomains: creative.creative && creative.creative.adomain ? creative.creative.adomain : [] }, ad: generateAd(body, req) }]; }, @@ -129,6 +141,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/shinezBidAdapter.js b/modules/shinezBidAdapter.js new file mode 100644 index 00000000000..d5734d23fdc --- /dev/null +++ b/modules/shinezBidAdapter.js @@ -0,0 +1,83 @@ +import { registerBidder } from '../src/adapters/bidderFactory.js'; +import { BANNER } from '../src/mediaTypes.js'; + +const BIDDER_CODE = 'shinez'; +const BIDDER_SHORT_CODE = 'shz'; +const ADAPTER_VERSION = '1.0.0'; + +const TARGET_URL = 'https://shinez-ssp.shinez.workers.dev/prebid'; + +export const spec = { + code: BIDDER_CODE, + version: ADAPTER_VERSION, + aliases: { + code: BIDDER_SHORT_CODE + }, + supportedMediaTypes: [ BANNER ], + isBidRequestValid: isBidRequestValid, + buildRequests: buildRequests, + interpretResponse: interpretResponse, +}; + +export const internal = { + TARGET_URL +} + +function isBidRequestValid(bid) { + return !!(bid && bid.params && + bid.params.placementId && typeof bid.params.placementId === 'string' && + (bid.params.unit == null || (typeof bid.params.unit === 'string' && bid.params.unit.length > 0)) + ); +} + +function buildRequests(validBidRequests, bidderRequest) { + const utcOffset = (new Date()).getTimezoneOffset(); + const data = []; + validBidRequests.forEach(function(bidRequest) { + data.push(_buildServerBidRequest(bidRequest, bidderRequest, utcOffset)); + }); + const request = { + method: 'POST', + url: TARGET_URL, + data: data + }; + return request; +} + +function interpretResponse(serverResponse, request) { + const bids = []; + serverResponse.body.forEach(function(serverBid) { + bids.push(_convertServerBid(serverBid)); + }); + return bids; +} + +function _buildServerBidRequest(bidRequest, bidderRequest, utcOffset) { + return { + bidId: bidRequest.bidId, + transactionId: bidRequest.transactionId, + crumbs: bidRequest.crumbs, + mediaTypes: bidRequest.mediaTypes, + refererInfo: bidderRequest.refererInfo, + placementId: bidRequest.params.placementId, + utcOffset: utcOffset, + adUnitCode: bidRequest.adUnitCode, + unit: bidRequest.params.unit + } +} + +function _convertServerBid(response) { + return { + requestId: response.bidId, + cpm: response.cpm, + currency: response.currency, + width: response.width, + height: response.height, + ad: response.ad, + ttl: response.ttl, + creativeId: response.creativeId, + netRevenue: response.netRevenue + }; +} + +registerBidder(spec); diff --git a/modules/shinezBidAdapter.md b/modules/shinezBidAdapter.md new file mode 100644 index 00000000000..e040cfbf36b --- /dev/null +++ b/modules/shinezBidAdapter.md @@ -0,0 +1,33 @@ +# Overview + +``` +Module Name: Shinez Bidder Adapter +Module Type: Bidder Adapter +Maintainer: tech-team@shinez.io +``` + +# Description + +Connects to shinez.io demand sources. + +The Shinez adapter requires setup and approval from the Shinez team. +Please reach out to tech-team@shinez.io for more information. + +# Test Parameters + +```javascript +var adUnits = [{ + code: "test-div", + mediaTypes: { + banner: { + sizes: [[300, 250]] + } + }, + bids: [{ + bidder: "shinez", + params: { + placementId: "00654321" + } + }] +}]; +``` \ No newline at end of file diff --git a/modules/sirdataRtdProvider.js b/modules/sirdataRtdProvider.js new file mode 100644 index 00000000000..373468b2f14 --- /dev/null +++ b/modules/sirdataRtdProvider.js @@ -0,0 +1,402 @@ +/** + * This module adds Sirdata provider to the real time data module + * The {@link module:modules/realTimeData} module is required + * The module will fetch segments (user-centric) and categories (page-centric) from Sirdata server + * The module will automatically handle user's privacy and choice in California (IAB TL CCPA Framework) and in Europe (IAB EU TCF FOR GDPR) + * @module modules/sirdataRtdProvider + * @requires module:modules/realTimeData + */ +import {getGlobal} from '../src/prebidGlobal.js'; +import * as utils from '../src/utils.js'; +import {submodule} from '../src/hook.js'; +import {ajax} from '../src/ajax.js'; +import findIndex from 'core-js-pure/features/array/find-index.js'; +import { getRefererInfo } from '../src/refererDetection.js'; +import { config } from '../src/config.js'; + +/** @type {string} */ +const MODULE_NAME = 'realTimeData'; +const SUBMODULE_NAME = 'SirdataRTDModule'; + +export function getSegmentsAndCategories(reqBidsConfigObj, onDone, moduleConfig, userConsent) { + const adUnits = reqBidsConfigObj.adUnits || getGlobal().adUnits; + moduleConfig.params = moduleConfig.params || {}; + + var tcString = (userConsent && userConsent.gdpr && userConsent.gdpr.consentString ? userConsent.gdpr.consentString : ''); + var gdprApplies = (userConsent && userConsent.gdpr && userConsent.gdpr.gdprApplies ? userConsent.gdpr.gdprApplies : ''); + + moduleConfig.params.partnerId = moduleConfig.params.partnerId ? moduleConfig.params.partnerId : 1; + moduleConfig.params.key = moduleConfig.params.key ? moduleConfig.params.key : 1; + + var sirdataDomain; + var sendWithCredentials; + + if (userConsent.coppa || (userConsent.usp && (userConsent.usp[0] == '1' && (userConsent.usp[1] == 'N' || userConsent.usp[2] == 'Y')))) { + // if children or "Do not Sell" management in California, no segments, page categories only whatever TCF signal + sirdataDomain = 'cookieless-data.com'; + sendWithCredentials = false; + gdprApplies = null; + tcString = ''; + } else if (getGlobal().getConfig('consentManagement.gdpr')) { + // Default endpoint is cookieless if gdpr management is set. Needed because the cookie-based endpoint will fail and return error if user is located in Europe and no consent has been given + sirdataDomain = 'cookieless-data.com'; + sendWithCredentials = false; + } + + // default global endpoint is cookie-based if no rules falls into cookieless or consent has been given or GDPR doesn't apply + if (!sirdataDomain || !gdprApplies || (utils.deepAccess(userConsent, 'gdpr.vendorData.vendor.consents') && userConsent.gdpr.vendorData.vendor.consents[53] && userConsent.gdpr.vendorData.purpose.consents[1] && userConsent.gdpr.vendorData.purpose.consents[4])) { + sirdataDomain = 'sddan.com'; + sendWithCredentials = true; + } + + var actualUrl = moduleConfig.params.actualUrl || getRefererInfo().referer; + + const url = 'https://kvt.' + sirdataDomain + '/api/v1/public/p/' + moduleConfig.params.partnerId + '/d/' + moduleConfig.params.key + '/s?callback=&gdpr=' + gdprApplies + '&gdpr_consent=' + tcString + (actualUrl ? '&url=' + actualUrl : ''); + ajax(url, { + success: function (response, req) { + if (req.status === 200) { + try { + const data = JSON.parse(response); + if (data && data.segments) { + addSegmentData(adUnits, data, moduleConfig, onDone); + } else { + onDone(); + } + } catch (e) { + onDone(); + utils.logError('unable to parse Sirdata data' + e); + } + } else if (req.status === 204) { + onDone(); + } + }, + error: function () { + onDone(); + utils.logError('unable to get Sirdata data'); + } + }, + null, + { + contentType: 'text/plain', + method: 'GET', + withCredentials: sendWithCredentials, + referrerPolicy: 'unsafe-url', + crossOrigin: true + }); +} + +export function setGlobalOrtb2(segments, categories) { + try { + let addOrtb2 = {}; + let testGlobal = getGlobal().getConfig('ortb2') || {}; + if (!utils.deepAccess(testGlobal, 'user.ext.data.sd_rtd') || !utils.deepEqual(testGlobal.user.ext.data.sd_rtd, segments)) { + utils.deepSetValue(addOrtb2, 'user.ext.data.sd_rtd', segments || {}); + } + if (!utils.deepAccess(testGlobal, 'site.ext.data.sd_rtd') || !utils.deepEqual(testGlobal.site.ext.data.sd_rtd, categories)) { + utils.deepSetValue(addOrtb2, 'site.ext.data.sd_rtd', categories || {}); + } + if (!utils.isEmpty(addOrtb2)) { + let ortb2 = {ortb2: utils.mergeDeep({}, testGlobal, addOrtb2)}; + getGlobal().setConfig(ortb2); + } + } catch (e) { + utils.logError(e) + } + + return true; +} + +export function setBidderOrtb2(bidder, segments, categories) { + try { + let addOrtb2 = {}; + let testBidder = utils.deepAccess(config.getBidderConfig(), bidder + '.ortb2') || {}; + if (!utils.deepAccess(testBidder, 'user.ext.data.sd_rtd') || !utils.deepEqual(testBidder.user.ext.data.sd_rtd, segments)) { + utils.deepSetValue(addOrtb2, 'user.ext.data.sd_rtd', segments || {}); + } + if (!utils.deepAccess(testBidder, 'site.ext.data.sd_rtd') || !utils.deepEqual(testBidder.site.ext.data.sd_rtd, categories)) { + utils.deepSetValue(addOrtb2, 'site.ext.data.sd_rtd', categories || {}); + } + if (!utils.isEmpty(addOrtb2)) { + let ortb2 = {ortb2: utils.mergeDeep({}, testBidder, addOrtb2)}; + getGlobal().setBidderConfig({ bidders: [bidder], config: ortb2 }); + } + } catch (e) { + utils.logError(e) + } + + return true; +} + +export function loadCustomFunction (todo, adUnit, list, data, bid) { + try { + if (typeof todo == 'function') { + todo(adUnit, list, data, bid); + } + } catch (e) { utils.logError(e); } + return true; +} + +export function getSegAndCatsArray(data, minScore) { + var sirdataData = {'segments': [], 'categories': []}; + minScore = minScore && typeof minScore == 'number' ? minScore : 30; + try { + if (data && data.contextual_categories) { + for (let catId in data.contextual_categories) { + let value = data.contextual_categories[catId]; + if (value >= minScore && sirdataData.categories.indexOf(catId) === -1) { + sirdataData.categories.push(catId.toString()); + } + } + } + } catch (e) { utils.logError(e); } + try { + if (data && data.segments) { + for (let segId in data.segments) { + sirdataData.segments.push(data.segments[segId].toString()); + } + } + } catch (e) { utils.logError(e); } + return sirdataData; +} + +export function addSegmentData(adUnits, data, moduleConfig, onDone) { + moduleConfig = moduleConfig || {}; + moduleConfig.params = moduleConfig.params || {}; + const globalMinScore = moduleConfig.params.hasOwnProperty('contextualMinRelevancyScore') ? moduleConfig.params.contextualMinRelevancyScore : 30; + var sirdataData = getSegAndCatsArray(data, globalMinScore); + + if (!sirdataData || (sirdataData.segments.length < 1 && sirdataData.categories.length < 1)) { utils.logError('no cats'); onDone(); return adUnits; } + + const sirdataList = sirdataData.segments.concat(sirdataData.categories); + + var curationData = {'segments': [], 'categories': []}; + var curationId = '1'; + const biddersParamsExist = (!!(moduleConfig.params && moduleConfig.params.bidders)); + + // Global ortb2 + if (!biddersParamsExist) { + setGlobalOrtb2(sirdataData.segments, sirdataData.categories); + } + + // Google targeting + if (typeof window.googletag !== 'undefined' && (moduleConfig.params.setGptKeyValues || !moduleConfig.params.hasOwnProperty('setGptKeyValues'))) { + try { + // For curation Google is pid 27449 + curationId = (moduleConfig.params.gptCurationId ? moduleConfig.params.gptCurationId : '27449'); + if (data.shared_taxonomy && data.shared_taxonomy[curationId]) { + curationData = getSegAndCatsArray(data.shared_taxonomy[curationId], globalMinScore); + } + window.googletag.pubads().getSlots().forEach(function(n) { + if (typeof n.setTargeting !== 'undefined') { + n.setTargeting('sd_rtd', sirdataList.concat(curationData.segments).concat(curationData.categories)); + } + }) + } catch (e) { utils.logError(e); } + } + + // Bid targeting level for FPD non-generic biders + var bidderIndex = ''; + var indexFound = false; + + adUnits.forEach(adUnit => { + if (!biddersParamsExist && !utils.deepAccess(adUnit, 'ortb2Imp.ext.data.sd_rtd')) { + utils.deepSetValue(adUnit, 'ortb2Imp.ext.data.sd_rtd', sirdataList); + } + + adUnit.hasOwnProperty('bids') && adUnit.bids.forEach(bid => { + bidderIndex = (moduleConfig.params.hasOwnProperty('bidders') ? findIndex(moduleConfig.params.bidders, function(i) { return i.bidder === bid.bidder; }) : false); + indexFound = (!!(typeof bidderIndex == 'number' && bidderIndex >= 0)); + try { + curationData = {'segments': [], 'categories': []}; + let minScore = (indexFound && moduleConfig.params.bidders[bidderIndex].hasOwnProperty('contextualMinRelevancyScore') ? moduleConfig.params.bidders[bidderIndex].contextualMinRelevancyScore : globalMinScore) + + if (!biddersParamsExist || (indexFound && (!moduleConfig.params.bidders[bidderIndex].hasOwnProperty('adUnitCodes') || moduleConfig.params.bidders[bidderIndex].adUnitCodes.indexOf(adUnit.code) !== -1))) { + switch (bid.bidder) { + case 'appnexus': + case 'appnexusAst': + case 'brealtime': + case 'emxdigital': + case 'pagescience': + case 'gourmetads': + case 'matomy': + case 'featureforward': + case 'oftmedia': + case 'districtm': + case 'adasta': + case 'beintoo': + case 'gravity': + case 'msq_classic': + case 'msq_max': + case '366_apx': + // For curation Xandr is pid 27446 + curationId = (indexFound && moduleConfig.params.bidders[bidderIndex].hasOwnProperty('curationId') ? moduleConfig.params.bidders[bidderIndex].curationId : '27446'); + if (data.shared_taxonomy && data.shared_taxonomy[curationId]) { + curationData = getSegAndCatsArray(data.shared_taxonomy[curationId], minScore); + } + if (indexFound && moduleConfig.params.bidders[bidderIndex].hasOwnProperty('customFunction')) { + loadCustomFunction(moduleConfig.params.bidders[bidderIndex].customFunction, adUnit, sirdataList.concat(curationData.segments).concat(curationData.categories), data, bid); + } else { + utils.deepSetValue(bid, 'params.keywords.sd_rtd', sirdataList.concat(curationData.segments).concat(curationData.categories)); + } + break; + + case 'smartadserver': + case 'smart': + var target = []; + if (bid.hasOwnProperty('params') && bid.params.hasOwnProperty('target')) { + target.push(bid.params.target); + } + // For curation Smart is pid 27440 + curationId = (indexFound && moduleConfig.params.bidders[bidderIndex].hasOwnProperty('curationId') ? moduleConfig.params.bidders[bidderIndex].curationId : '27440'); + if (data.shared_taxonomy && data.shared_taxonomy[curationId]) { + curationData = getSegAndCatsArray(data.shared_taxonomy[curationId], minScore); + } + if (indexFound && moduleConfig.params.bidders[bidderIndex].hasOwnProperty('customFunction')) { + loadCustomFunction(moduleConfig.params.bidders[bidderIndex].customFunction, adUnit, sirdataList.concat(curationData.segments).concat(curationData.categories), data, bid); + } else { + sirdataList.concat(curationData.segments).concat(curationData.categories).forEach(function(entry) { + if (target.indexOf('sd_rtd=' + entry) === -1) { + target.push('sd_rtd=' + entry); + } + }); + utils.deepSetValue(bid, 'params.target', target.join(';')); + } + break; + + case 'rubicon': + // For curation Magnite is pid 27518 + curationId = (indexFound && moduleConfig.params.bidders[bidderIndex].hasOwnProperty('curationId') ? moduleConfig.params.bidders[bidderIndex].curationId : '27452'); + if (data.shared_taxonomy && data.shared_taxonomy[curationId]) { + curationData = getSegAndCatsArray(data.shared_taxonomy[curationId], minScore); + } + if (indexFound && moduleConfig.params.bidders[bidderIndex].hasOwnProperty('customFunction')) { + loadCustomFunction(moduleConfig.params.bidders[bidderIndex].customFunction, adUnit, sirdataList.concat(curationData.segments).concat(curationData.categories), data, bid); + } else { + setBidderOrtb2(bid.bidder, data.segments.concat(curationData.segments), sirdataList.concat(curationData.segments).concat(curationData.categories)); + } + break; + + case 'ix': + var ixConfig = getGlobal().getConfig('ix.firstPartyData.sd_rtd'); + if (!ixConfig) { + // For curation index is pid 27248 + curationId = (indexFound && moduleConfig.params.bidders[bidderIndex].hasOwnProperty('curationId') ? moduleConfig.params.bidders[bidderIndex].curationId : '27248'); + if (data.shared_taxonomy && data.shared_taxonomy[curationId]) { + curationData = getSegAndCatsArray(data.shared_taxonomy[curationId], minScore); + } + if (indexFound && moduleConfig.params.bidders[bidderIndex].hasOwnProperty('customFunction')) { + loadCustomFunction(moduleConfig.params.bidders[bidderIndex].customFunction, adUnit, sirdataList.concat(curationData.segments).concat(curationData.categories), data, bid); + } else { + var cappIxCategories = []; + var ixLength = 0; + var ixLimit = (indexFound && moduleConfig.params.bidders[bidderIndex].hasOwnProperty('sizeLimit') ? moduleConfig.params.bidders[bidderIndex].sizeLimit : 1000); + // Push ids For publisher use and for curation if exists but limit size because the bidder uses GET parameters + sirdataList.concat(curationData.segments).concat(curationData.categories).forEach(function(entry) { + if (ixLength < ixLimit) { + cappIxCategories.push(entry); + ixLength += entry.toString().length; + } + }); + getGlobal().setConfig({ix: {firstPartyData: {sd_rtd: cappIxCategories}}}); + } + } + break; + + case 'proxistore': + // For curation Proxistore is pid 27484 + curationId = (indexFound && moduleConfig.params.bidders[bidderIndex].hasOwnProperty('curationId') ? moduleConfig.params.bidders[bidderIndex].curationId : '27484'); + if (data.shared_taxonomy && data.shared_taxonomy[curationId]) { + curationData = getSegAndCatsArray(data.shared_taxonomy[curationId], minScore); + } else { + data.shared_taxonomy[curationId] = {contextual_categories: {}}; + } + if (indexFound && moduleConfig.params.bidders[bidderIndex].hasOwnProperty('customFunction')) { + loadCustomFunction(moduleConfig.params.bidders[bidderIndex].customFunction, adUnit, sirdataList.concat(curationData.segments).concat(curationData.categories), data, bid); + } else { + utils.deepSetValue(bid, 'ortb2.user.ext.data', {segments: sirdataData.segments.concat(curationData.segments), contextual_categories: {...data.contextual_categories, ...data.shared_taxonomy[curationId].contextual_categories}}); + } + break; + + case 'criteo': + // For curation Smart is pid 27443 + curationId = (indexFound && moduleConfig.params.bidders[bidderIndex].hasOwnProperty('curationId') ? moduleConfig.params.bidders[bidderIndex].curationId : '27443'); + if (data.shared_taxonomy && data.shared_taxonomy[curationId]) { + curationData = getSegAndCatsArray(data.shared_taxonomy[curationId], minScore); + } + if (indexFound && moduleConfig.params.bidders[bidderIndex].hasOwnProperty('customFunction')) { + loadCustomFunction(moduleConfig.params.bidders[bidderIndex].customFunction, adUnit, sirdataList.concat(curationData.segments).concat(curationData.categories), data, bid); + } else { + setBidderOrtb2(bid.bidder, sirdataList.concat(curationData.segments).concat(curationData.categories), sirdataList.concat(curationData.segments).concat(curationData.categories)); + } + break; + + case 'triplelift': + // For curation Triplelift is pid 27518 + curationId = (indexFound && moduleConfig.params.bidders[bidderIndex].hasOwnProperty('curationId') ? moduleConfig.params.bidders[bidderIndex].curationId : '27518'); + if (data.shared_taxonomy && data.shared_taxonomy[curationId]) { + curationData = getSegAndCatsArray(data.shared_taxonomy[curationId], minScore); + } + if (indexFound && moduleConfig.params.bidders[bidderIndex].hasOwnProperty('customFunction')) { + loadCustomFunction(moduleConfig.params.bidders[bidderIndex].customFunction, adUnit, sirdataList.concat(curationData.segments).concat(curationData.categories), data, bid); + } else { + setBidderOrtb2(bid.bidder, data.segments.concat(curationData.segments), sirdataList.concat(curationData.segments).concat(curationData.categories)); + } + break; + + case 'avct': + case 'avocet': + // For curation Avocet is pid 27522 + curationId = (indexFound && moduleConfig.params.bidders[bidderIndex].hasOwnProperty('curationId') ? moduleConfig.params.bidders[bidderIndex].curationId : '27522'); + if (data.shared_taxonomy && data.shared_taxonomy[curationId]) { + curationData = getSegAndCatsArray(data.shared_taxonomy[curationId], minScore); + } + if (indexFound && moduleConfig.params.bidders[bidderIndex].hasOwnProperty('customFunction')) { + loadCustomFunction(moduleConfig.params.bidders[bidderIndex].customFunction, adUnit, sirdataList.concat(curationData.segments).concat(curationData.categories), data, bid); + } else { + setBidderOrtb2(bid.bidder, data.segments.concat(curationData.segments), sirdataList.concat(curationData.segments).concat(curationData.categories)); + } + break; + + case 'smaato': + // For curation Smaato is pid 27520 + curationId = (indexFound && moduleConfig.params.bidders[bidderIndex].hasOwnProperty('curationId') ? moduleConfig.params.bidders[bidderIndex].curationId : '27520'); + if (data.shared_taxonomy && data.shared_taxonomy[curationId]) { + curationData = getSegAndCatsArray(data.shared_taxonomy[curationId], minScore); + } + if (indexFound && moduleConfig.params.bidders[bidderIndex].hasOwnProperty('customFunction')) { + loadCustomFunction(moduleConfig.params.bidders[bidderIndex].customFunction, adUnit, sirdataList.concat(curationData.segments).concat(curationData.categories), data, bid); + } else { + setBidderOrtb2(bid.bidder, data.segments.concat(curationData.segments), sirdataList.concat(curationData.segments).concat(curationData.categories)); + } + break; + + default: + if (!biddersParamsExist || indexFound) { + if (!utils.deepAccess(bid, 'ortb2.site.ext.data.sd_rtd')) { + utils.deepSetValue(bid, 'ortb2.site.ext.data.sd_rtd', sirdataData.categories); + } + if (!utils.deepAccess(bid, 'ortb2.user.ext.data.sd_rtd')) { + utils.deepSetValue(bid, 'ortb2.user.ext.data.sd_rtd', sirdataData.segments); + } + } + } + } + } catch (e) { utils.logError(e) } + }) + }); + + onDone(); + return adUnits; +} + +export function init(config) { + return true; +} + +export const sirdataSubmodule = { + name: SUBMODULE_NAME, + init: init, + getBidRequestData: getSegmentsAndCategories +}; + +submodule(MODULE_NAME, sirdataSubmodule); diff --git a/modules/sirdataRtdProvider.md b/modules/sirdataRtdProvider.md new file mode 100644 index 00000000000..f67e34db43a --- /dev/null +++ b/modules/sirdataRtdProvider.md @@ -0,0 +1,169 @@ +# Sirdata Real-Time Data Submodule + +Module Name: Sirdata Rtd Provider +Module Type: Rtd Provider +Maintainer: bob@sirdata.com + +# Description + +Sirdata provides a disruptive API that allows its partners to leverage its +cutting-edge contextualization technology and its audience segments based on +cookies and consent or without cookies nor consent! + +User-based segments and page-level automatic contextual categories will be +attached to bid request objects sent to different SSPs in order to optimize +targeting. + +Automatic integration with Google Ad Manager and major bidders like Xandr/Appnexus, +Smartadserver, Index Exchange, Proxistore, Magnite/Rubicon or Triplelift ! + +User's country and choice management are included in the module, so it's 100% +compliant with local and regional laws like GDPR and CCPA/CPRA. + +ORTB2 compliant and FPD support for Prebid versions < 4.29 + +Contact bob@sirdata.com for information. + +### Publisher Usage + +Compile the Sirdata RTD module into your Prebid build: + +`gulp build --modules=rtdModule,sirdataRtdProvider` + +Add the Sirdata RTD provider to your Prebid config. + +Segments ids (user-centric) and category ids (page-centric) will be provided +salted and hashed : you can use them with a dedicated and private matching table. +Should you want to allow a SSP or a partner to curate your media and operate +cross-publishers campaigns with our data, please ask Sirdata (bob@sirdata.com) to +open it for you account. + +``` +pbjs.setConfig( + ... + realTimeData: { + auctionDelay: 1000, + dataProviders: [ + { + name: "SirdataRTDModule", + waitForIt: true, + params: { + partnerId: 1, + key: 1, + setGptKeyValues: true, + contextualMinRelevancyScore: 50, //Min score to filter contextual category globally (0-100 scale) + actualUrl: actual_url, //top location url, for contextual categories + bidders: [{ + bidder: 'appnexus', + adUnitCodes: ['adUnit-1','adUnit-2'], + customFunction: overrideAppnexus, + curationId: '111', + },{ + bidder: 'ix', + sizeLimit: 1200 //specific to Index Exchange, + contextualMinRelevancyScore: 50, //Min score to filter contextual category for curation in the bidder (0-100 scale) + }] + } + } + ] + } + ... +} +``` + +### Parameter Descriptions for the Sirdata Configuration Section + +| Name |Type | Description | Notes | +| :------------ | :------------ | :------------ |:------------ | +| name | String | Real time data module name | Mandatory. Always 'SirdataRTDModule' | +| waitForIt | Boolean | Mandatory. Required to ensure that the auction is delayed until prefetch is complete | Optional. Defaults to false but recommended to true | +| params | Object | | Optional | +| params.partnerId | Integer | Partner ID, required to get results and provided by Sirdata. Use 1 for tests and get one running at bob@sirdata.com | Mandatory. Defaults 1. | +| params.key | Integer | Key linked to Partner ID, required to get results and provided by Sirdata. Use 1 for tests and get one running at bob@sirdata.com | Mandatory. Defaults 1. | +| params.setGptKeyValues | Boolean | This parameter Sirdata to set Targeting for GPT/GAM | Optional. Defaults to true. | +| params.contextualMinRelevancyScore | Integer | Min score to keep filter category in the bidders (0-100 scale). Optional. Defaults to 30. | +| params.bidders | Object | Dictionary of bidders you would like to supply Sirdata data for. | Optional. In case no bidder is specified Sirdata will atend to ad data custom and ortb2 to all bidders, adUnits & Globalconfig | + +Bidders can receive common setting : +| Name |Type | Description | Notes | +| :------------ | :------------ | :------------ |:------------ | +| bidder | String | Bidder name | Mandatory if params.bidders are specified | +| adUnitCodes | Array of String | Use if you want to limit data injection to specified adUnits for the bidder | Optional. Default is false and data shared with the bidder isn't filtered | +| customFunction | Function | Use it to override the way data is shared with a bidder | Optional. Default is false | +| curationId | String | Specify the curation ID of the bidder. Provided by Sirdata, request it at bob@sirdata.com | Optional. Default curation ids are specified for main bidders | +| contextualMinRelevancyScore | Integer | Min score to filter contextual categories for curation in the bidder (0-100 scale). Optional. Defaults to 30 or global params.contextualMinRelevancyScore if exits. | +| sizeLimit | Integer | used only for bidder 'ix' to limit the size of the get parameter in Index Exchange ad call | Optional. Default is 1000 | + + +### Overriding data sharing function +As indicated above, it is possible to provide your own bid augmentation +functions. This is useful if you know a bid adapter's API supports segment +fields which aren't specifically being added to request objects in the Prebid +bid adapter. + +Please see the following example, which provides a function to modify bids for +a bid adapter called ix and overrides the appnexus. + +data Object format for usage in this kind of function : +{ + "segments":[111111,222222], + "contextual_categories":{"333333":100}, + "shared_taxonomy":{ + "27446":{ //CurationId + "segments":[444444,555555], + "contextual_categories":{"666666":100} + } + } +} + +``` +function overrideAppnexus (adUnit, segmentsArray, dataObject, bid) { + for (var i = 0; i < segmentsArray.length; i++) { + if (segmentsArray[i]) { + bid.params.user.segments.push(segmentsArray[i]); + } + } +} + +pbjs.setConfig( + ... + realTimeData: { + auctionDelay: 1000, + dataProviders: [ + { + name: "SirdataRTDModule", + waitForIt: true, + params: { + partnerId: 1, + key: 1, + setGptKeyValues: true, + contextualMinRelevancyScore: 50, //Min score to keep contextual category in the bidders (0-100 scale) + actualUrl: actual_url, //top location url, for contextual categories + bidders: [{ + bidder: 'appnexus', + customFunction: overrideAppnexus, + curationId: '111' + },{ + bidder: 'ix', + sizeLimit: 1200, //specific to Index Exchange + customFunction: function(adUnit, segmentsArray, dataObject, bid) { + bid.params.contextual.push(dataObject.contextual_categories); + }, + }] + } + } + ] + } + ... +} +``` + +### Testing + +To view an example of available segments returned by Sirdata's backends: + +`gulp serve --modules=rtdModule,sirdataRtdProvider,appnexusBidAdapter` + +and then point your browser at: + +`http://localhost:9999/integrationExamples/gpt/sirdataRtdProvider_example.html` \ No newline at end of file diff --git a/modules/sizeMappingV2.js b/modules/sizeMappingV2.js index 4df537e0eb3..ffd242e57ac 100644 --- a/modules/sizeMappingV2.js +++ b/modules/sizeMappingV2.js @@ -66,7 +66,7 @@ export function isUsingNewSizeMapping(adUnits) { }); // checks for the presence of sizeConfig property at the adUnit.bids[].bidder object - adUnit.bids.forEach(bidder => { + adUnit.bids && utils.isArray(adUnit.bids) && adUnit.bids.forEach(bidder => { if (bidder.sizeConfig) { if (isUsingSizeMappingBool === false) { isUsingSizeMappingBool = true; @@ -168,8 +168,15 @@ export function checkAdUnitSetupHook(adUnits) { } const validatedAdUnits = []; adUnits.forEach(adUnit => { + const bids = adUnit.bids; const mediaTypes = adUnit.mediaTypes; let validatedBanner, validatedVideo, validatedNative; + + if (!bids || !utils.isArray(bids)) { + utils.logError(`Detected adUnit.code '${adUnit.code}' did not have 'adUnit.bids' defined or 'adUnit.bids' is not an array. Removing adUnit from auction.`); + return; + } + if (!mediaTypes || Object.keys(mediaTypes).length === 0) { utils.logError(`Detected adUnit.code '${adUnit.code}' did not have a 'mediaTypes' object defined. This is a required field for the auction, so this adUnit has been removed.`); return; diff --git a/modules/smaatoBidAdapter.js b/modules/smaatoBidAdapter.js index ce0edb1e19c..fbb4c14e5f8 100644 --- a/modules/smaatoBidAdapter.js +++ b/modules/smaatoBidAdapter.js @@ -5,7 +5,7 @@ import { BANNER, VIDEO } from '../src/mediaTypes.js'; const BIDDER_CODE = 'smaato'; const SMAATO_ENDPOINT = 'https://prebid.ad.smaato.net/oapi/prebid'; -const CLIENT = 'prebid_js_$prebid.version$_1.0' +const CLIENT = 'prebid_js_$prebid.version$_1.1' /** * Transform BidRequest to OpenRTB-formatted BidRequest Object @@ -98,8 +98,10 @@ const buildOpenRtbBidRequestPayload = (validBidRequests, bidderRequest) => { } }; - Object.assign(request.user, config.getConfig('fpd.user')); - Object.assign(request.site, config.getConfig('fpd.context')); + let ortb2 = config.getConfig('ortb2') || {}; + + Object.assign(request.user, ortb2.user); + Object.assign(request.site, ortb2.site); if (bidderRequest.gdprConsent && bidderRequest.gdprConsent.gdprApplies === true) { utils.deepSetValue(request, 'regs.ext.gdpr', bidderRequest.gdprConsent.gdprApplies ? 1 : 0); @@ -110,6 +112,18 @@ const buildOpenRtbBidRequestPayload = (validBidRequests, bidderRequest) => { utils.deepSetValue(request, 'regs.ext.us_privacy', bidderRequest.uspConsent); } + if (utils.deepAccess(validBidRequests[0], 'params.app')) { + const geo = utils.deepAccess(validBidRequests[0], 'params.app.geo'); + utils.deepSetValue(request, 'device.geo', geo); + const ifa = utils.deepAccess(validBidRequests[0], 'params.app.ifa') + utils.deepSetValue(request, 'device.ifa', ifa); + } + + const eids = utils.deepAccess(validBidRequests[0], 'userIdAsEids'); + if (eids && eids.length) { + utils.deepSetValue(request, 'user.ext.eids', eids); + } + utils.logInfo('[SMAATO] OpenRTB Request:', request); return JSON.stringify(request); } @@ -185,7 +199,7 @@ export const spec = { ttl: ttlSec, creativeId: b.crid, dealId: b.dealid || null, - netRevenue: true, + netRevenue: utils.deepAccess(b, 'ext.net', true), currency: res.cur, meta: { advertiserDomains: b.adomain, diff --git a/modules/smartadserverBidAdapter.js b/modules/smartadserverBidAdapter.js index 97dd43fc5ba..bb9364c72c3 100644 --- a/modules/smartadserverBidAdapter.js +++ b/modules/smartadserverBidAdapter.js @@ -13,8 +13,10 @@ import { createEidsArray } from './userId/eids.js'; const BIDDER_CODE = 'smartadserver'; +const GVL_ID = 45; export const spec = { code: BIDDER_CODE, + gvlid: GVL_ID, aliases: ['smart'], // short code supportedMediaTypes: [BANNER, VIDEO], /** @@ -83,10 +85,11 @@ export const spec = { w: size[0], h: size[1] })); - } else if (videoMediaType && videoMediaType.context === 'instream') { + } else if (videoMediaType && (videoMediaType.context === 'instream' || videoMediaType.context === 'outstream')) { // Specific attributes for instream. let playerSize = videoMediaType.playerSize[0]; - payload.isVideo = true; + payload.isVideo = videoMediaType.context === 'instream'; + payload.mediaType = VIDEO; payload.videoData = { videoProtocol: bid.params.video.protocol, playerWidth: playerSize[0], @@ -98,6 +101,7 @@ export const spec = { } if (bidderRequest && bidderRequest.gdprConsent) { + payload.addtl_consent = bidderRequest.gdprConsent.addtlConsent; payload.gdpr_consent = bidderRequest.gdprConsent.consentString; payload.gdpr = bidderRequest.gdprConsent.gdprApplies; // we're handling the undefined case server side } @@ -131,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 = { @@ -143,13 +147,15 @@ export const spec = { dealId: response.dealId, currency: response.currency, netRevenue: response.isNetCpm, - ttl: response.ttl + ttl: response.ttl, + dspPixels: response.dspPixels }; - if (bidRequest.isVideo) { + if (bidRequest.mediaType === VIDEO) { bidResponse.mediaType = VIDEO; bidResponse.vastUrl = response.adUrl; bidResponse.vastXml = response.ad; + bidResponse.content = response.ad; } else { bidResponse.adUrl = response.adUrl; bidResponse.ad = response.ad; @@ -177,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/smartadserverBidAdapter.md b/modules/smartadserverBidAdapter.md index c6f68363d7c..05e29359fd2 100644 --- a/modules/smartadserverBidAdapter.md +++ b/modules/smartadserverBidAdapter.md @@ -94,4 +94,43 @@ Please reach out to your Technical account manager for more information. } }] }; +``` + +## Outstream Video + +``` + var outstreamVideoAdUnit = { + code: 'test-div', + mediaTypes: { + video: { + context: 'outstream', + playerSize: [640, 480] + } + }, + renderer: { + url: 'https://acdn.adnxs.com/video/outstream/ANOutstreamVideo.js', + render: function (bid) { + bid.renderer.push(() => { + ANOutstreamVideo.renderAd({ + targetId: bid.adUnitCode, + adResponse: bid + }); + }); + } + }, + bids: [{ + bidder: "smart", + params: { + domain: 'https://prg.smartadserver.com', + siteId: 207435, + pageId: 896536, + formatId: 85089, + bidfloor: 5, + video: { + protocol: 6, + startDelay: 1 + } + } + }] + }; ``` \ No newline at end of file diff --git a/modules/smarticoBidAdapter.js b/modules/smarticoBidAdapter.js new file mode 100644 index 00000000000..9107ce5f908 --- /dev/null +++ b/modules/smarticoBidAdapter.js @@ -0,0 +1,116 @@ +import { registerBidder } from '../src/adapters/bidderFactory.js'; +import { BANNER } from '../src/mediaTypes.js'; +import find from 'core-js-pure/features/array/find.js'; + +const SMARTICO_CONFIG = { + bidRequestUrl: 'https://trmads.eu/preBidRequest', + widgetUrl: 'https://trmads.eu/get', + method: 'POST' +} + +const BIDDER_CODE = 'smartico'; + +export const spec = { + code: BIDDER_CODE, + supportedMediaTypes: [BANNER], + isBidRequestValid: function (bid) { + return !!(bid && bid.params && bid.params.token && bid.params.placementId); + }, + buildRequests: function (validBidRequests, bidderRequest) { + var i + var j + var bid + var bidParam + var bidParams = [] + var sizes + var frameWidth = Math.round(window.screen.width) + var frameHeight = Math.round(window.screen.height) + for (i = 0; i < validBidRequests.length; i++) { + bid = validBidRequests[i] + if (bid.sizes) { + sizes = bid.sizes + } else if (typeof (BANNER) != 'undefined' && bid.mediaTypes && bid.mediaTypes[BANNER] && bid.mediaTypes[BANNER].sizes) { + sizes = bid.mediaTypes[BANNER].sizes + } else if (frameWidth && frameHeight) { + sizes = [[frameWidth, frameHeight]] + } else { + sizes = [] + } + for (j = 0; j < sizes.length; j++) { + bidParam = { + token: bid.params.token || '', + bidId: bid.bidId, + 'banner-format-width': sizes[j][0], + 'banner-format-height': sizes[j][1] + } + if (bid.params.bannerFormat) { + bidParam['banner-format'] = bid.params.bannerFormat + } + if (bid.params.language) { + bidParam.language = bid.params.language + } + if (bid.params.region) { + bidParam.region = bid.params.region + } + if (bid.params.regions && (bid.params.regions instanceof String || (bid.params.regions instanceof Array && bid.params.regions.length))) { + bidParam.regions = bid.params.regions + if (bidParam.regions instanceof Array) { + bidParam.regions = bidParam.regions.join(',') + } + } + bidParams.push(bidParam) + } + } + + var ServerRequestObjects = { + method: SMARTICO_CONFIG.method, + url: SMARTICO_CONFIG.bidRequestUrl, + bids: validBidRequests, + data: {bidParams: bidParams, auctionId: bidderRequest.auctionId, origin: window.location.origin} + } + + return ServerRequestObjects; + }, + interpretResponse: function (serverResponse, bidRequest) { + var i + var bid + var bidObject + var url + var html + var ad + var token + var language + var scriptId + var bidResponses = [] + + for (i = 0; i < serverResponse.length; i++) { + ad = serverResponse[i]; + bid = find(bidRequest.bids, bid => bid.bidId === ad.bidId) + if (bid) { + token = bid.params.token || '' + + language = bid.params.language || SMARTICO_CONFIG.language || '' + + scriptId = encodeURIComponent('smartico-widget-' + bid.params.placementId + '-' + i) + + url = SMARTICO_CONFIG.widgetUrl + '?token=' + encodeURIComponent(token) + '&auction-id=' + encodeURIComponent(bid.auctionId) + '&from-auction-buffer=1&own_session=1&ad=' + encodeURIComponent(ad.id) + '&scriptid=' + scriptId + (ad.bannerFormatAlias ? '&banner-format=' + encodeURIComponent(ad.bannerFormatAlias) : '') + (language ? '&language=' + encodeURIComponent(language) : '') + + html = ' + + +
+ + + `; + + return adcode; +} + +const spec = { + code: BIDDER_CODE, + aliases: [], + supportedMediaTypes: [BANNER], + isBidRequestValid(bid) { + // as per OneCode integration, bids without params are valid + return true; + }, + buildRequests(validBidRequests, bidderRequest) { + if ((!validBidRequests) || (validBidRequests.length < 1)) { + return false; + } + + const siteId = setOnAny(validBidRequests, 'params.siteId'); + const publisherId = setOnAny(validBidRequests, 'params.publisherId'); + const page = setOnAny(validBidRequests, 'params.page') || bidderRequest.refererInfo.referer; + const domain = setOnAny(validBidRequests, 'params.domain') || utils.parseUrl(page).hostname; + const tmax = setOnAny(validBidRequests, 'params.tmax') ? parseInt(setOnAny(validBidRequests, 'params.tmax'), 10) : TMAX; + const pbver = '$prebid.version$'; + const testMode = setOnAny(validBidRequests, 'params.test') ? 1 : undefined; + + let ref; + + try { + if (W.self === W.top && document.referrer) { ref = document.referrer; } + } catch (e) { + } + + const payload = { + id: bidderRequest.auctionId, + site: { + id: siteId, + publisher: publisherId ? { id: publisherId } : undefined, + page, + domain, + ref + }, + imp: validBidRequests.map(slot => mapImpression(slot)), + tmax, + user: {}, + regs: {}, + test: testMode, + }; + + applyGdpr(bidderRequest, payload); + applyClientHints(payload); + + return { + method: 'POST', + url: BIDDER_URL + '?cs=' + cookieSupport() + '&bdver=' + BIDDER_VERSION + '&pbver=' + pbver + '&inver=0', + data: JSON.stringify(payload), + bidderRequest, + }; + }, + + interpretResponse(serverResponse, request) { + const { bidderRequest } = request; + const response = serverResponse.body; + const bids = []; + const site = JSON.parse(request.data).site; // get page and referer data from request + site.sn = response.sn || 'mc_adapter'; // WPM site name (wp_sn) + let seat; + + if (response.seatbid !== undefined) { + /* + Match response to request, by comparing bid id's + 'bidid-' prefix indicates oneCode (parameterless) request and response + */ + response.seatbid.forEach(seatbid => { + seat = seatbid.seat; + seatbid.bid.forEach(serverBid => { + // get data from bid response + const { adomain, crid, impid, exp, ext, price, w, h } = serverBid; + + const bidRequest = bidderRequest.bids.filter(b => { + const { bidId, params } = b; + const { id, siteId } = params || {}; + const currentBidId = id && siteId ? id : 'bidid-' + bidId; + return currentBidId === impid; + })[0]; + + // get data from linked bidRequest + const { bidId, params } = bidRequest || {}; + + // get slot id for current bid + site.slot = params && params.id; + + if (ext) { + /* + bid response might include ext object containing siteId / slotId, as detected by OneCode + update site / slot data in this case + */ + const { siteid, slotid } = ext; + site.id = siteid || site.id; + site.slot = slotid || site.slot; + } + + if (bidRequest && site.id && !strIncludes(site.id, 'bidid')) { + // found a matching request; add this bid + + // store site data for future notification + oneCodeDetection[bidId] = [site.id, site.slot]; + + const bid = { + requestId: bidId, + creativeId: crid || 'mcad_' + bidderRequest.auctionId + '_' + site.slot, + cpm: price, + currency: response.cur, + ttl: exp || 300, + width: w, + height: h, + bidderCode: BIDDER_CODE, + mediaType: 'banner', + meta: { + advertiserDomains: adomain, + networkName: seat, + }, + netRevenue: true, + ad: renderCreative(site, response.id, serverBid, seat, bidderRequest), + }; + + if (bid.cpm > 0) { + // check bidFloor (if present in params) + const { bidFloor } = params || {}; + + if (!bidFloor || bid.cpm >= bidFloor) { + bids.push(bid); + } else { + utils.logWarn('Discarding bid due to bidFloor setting', bid.cpm, bidFloor); + } + } + } else { + utils.logWarn('Discarding response - no matching request / site id', serverBid.impid); + } + }); + }); + } + + return bids; + }, + getUserSyncs(syncOptions) { + if (syncOptions.iframeEnabled) { + return [{ + type: 'iframe', + url: SYNC_URL + '?tcf=' + consentApiVersion, + }]; + } + utils.logWarn('sspBC adapter requires iframe based user sync.'); + }, + + onTimeout(timeoutData) { + const payload = getNotificationPayload(timeoutData); + if (payload) { + payload.event = 'timeout'; + sendNotification(payload); + return payload; + } + }, + + onBidWon(bid) { + const payload = getNotificationPayload(bid); + if (payload) { + payload.event = 'bidWon'; + sendNotification(payload); + return payload; + } + }, +}; + +registerBidder(spec); + +export { + spec, +}; diff --git a/modules/sspBCBidAdapter.md b/modules/sspBCBidAdapter.md new file mode 100644 index 00000000000..f22e8e6c458 --- /dev/null +++ b/modules/sspBCBidAdapter.md @@ -0,0 +1,47 @@ +# Overview + +Module Name: sspBC Bidder Adapter +Module Type: Bidder Adapter +Maintainer: wojciech.bialy@grupawp.pl + +# Description + +Module that connects to Wirtualna Polska Media header bidding endpoint to fetch bids. +Only banner format is supported. +Supported currencies: USD, EUR, PLN + +Required parameters: +- none + +Optional parameters: +- site id +- adslot id +- publisher id +- domain +- page +- tmax +- bidFloor +- test + +# Test Parameters +``` +var adUnits = [ + // Banner adUnit + { + code: 'banner-div', + mediaTypes: { + banner: { + sizes: [[300, 250]] + } + }, + bids: [{ + bidder: 'sspBC', + params: { + id: "006", + siteId: "235911", + test: 1 + } + }] + } +]; +``` diff --git a/modules/stroeerCoreBidAdapter.js b/modules/stroeerCoreBidAdapter.js new file mode 100644 index 00000000000..ec442f5125a --- /dev/null +++ b/modules/stroeerCoreBidAdapter.js @@ -0,0 +1,196 @@ +import {registerBidder} from '../src/adapters/bidderFactory.js'; +import {BANNER} from '../src/mediaTypes.js'; +import * as utils from '../src/utils.js'; + +const GVL_ID = 136; +const BIDDER_CODE = 'stroeerCore'; +const DEFAULT_HOST = 'hb.adscale.de'; +const DEFAULT_PATH = '/dsh'; +const DEFAULT_PORT = ''; +const FIVE_MINUTES_IN_SECONDS = 300; +const USER_SYNC_IFRAME_URL = 'https://js.adscale.de/pbsync.html'; + +const isSecureWindow = () => utils.getWindowSelf().location.protocol === 'https:'; +const isMainPageAccessible = () => getMostAccessibleTopWindow() === utils.getWindowTop(); + +function getTopWindowReferrer() { + try { + return utils.getWindowTop().document.referrer; + } catch (e) { + return utils.getWindowSelf().referrer; + } +} + +function getMostAccessibleTopWindow() { + let res = utils.getWindowSelf(); + + try { + while (utils.getWindowTop().top !== res && res.parent.location.href.length) { + res = res.parent; + } + } catch (ignore) { + } + + return res; +} + +function elementInView(elementId) { + const resolveElement = (elId) => { + const win = utils.getWindowSelf(); + + return win.document.getElementById(elId); + }; + + const visibleInWindow = (el, win) => { + const rect = el.getBoundingClientRect(); + const inView = (rect.top + rect.height >= 0) && (rect.top <= win.innerHeight); + + if (win !== win.parent) { + return inView && visibleInWindow(win.frameElement, win.parent); + } + + return inView; + }; + + try { + return visibleInWindow(resolveElement(elementId), utils.getWindowSelf()); + } catch (e) { + // old browser, element not found, cross-origin etc. + } + return undefined; +} + +function buildUrl({host: hostname = DEFAULT_HOST, port = DEFAULT_PORT, securePort, path: pathname = DEFAULT_PATH}) { + if (securePort) { + port = securePort; + } + + return utils.buildUrl({protocol: 'https', hostname, port, pathname}); +} + +function getGdprParams(gdprConsent) { + if (gdprConsent) { + const consentString = encodeURIComponent(gdprConsent.consentString || '') + const isGdpr = gdprConsent.gdprApplies ? 1 : 0; + + return `?gdpr=${isGdpr}&gdpr_consent=${consentString}` + } else { + return ''; + } +} + +export const spec = { + code: BIDDER_CODE, + gvlid: GVL_ID, + supportedMediaTypes: [BANNER], + + isBidRequestValid: (function () { + const validators = []; + + const createValidator = (checkFn, errorMsgFn) => { + return (bidRequest) => { + if (checkFn(bidRequest)) { + return true; + } else { + utils.logError(`invalid bid: ${errorMsgFn(bidRequest)}`, 'ERROR'); + return false; + } + } + }; + + function isBanner(bidReq) { + return (!bidReq.mediaTypes && !bidReq.mediaType) || + (bidReq.mediaTypes && bidReq.mediaTypes.banner) || + bidReq.mediaType === BANNER; + } + + validators.push(createValidator((bidReq) => isBanner(bidReq), + bidReq => `bid request ${bidReq.bidId} is not a banner`)); + validators.push(createValidator((bidReq) => typeof bidReq.params === 'object', + bidReq => `bid request ${bidReq.bidId} does not have custom params`)); + validators.push(createValidator((bidReq) => utils.isStr(bidReq.params.sid), + bidReq => `bid request ${bidReq.bidId} does not have a sid string field`)); + + return function (bidRequest) { + return validators.every(f => f(bidRequest)); + } + }()), + + buildRequests: function (validBidRequests = [], bidderRequest) { + const anyBid = bidderRequest.bids[0]; + + const payload = { + id: bidderRequest.auctionId, + bids: [], + ref: getTopWindowReferrer(), + ssl: isSecureWindow(), + mpa: isMainPageAccessible(), + timeout: bidderRequest.timeout - (Date.now() - bidderRequest.auctionStart) + }; + + const userIds = anyBid.userId; + + if (!utils.isEmpty(userIds)) { + payload.user = { + euids: userIds + }; + } + + const gdprConsent = bidderRequest.gdprConsent; + + if (gdprConsent && gdprConsent.consentString != null && gdprConsent.gdprApplies != null) { + payload.gdpr = { + consent: bidderRequest.gdprConsent.consentString, applies: bidderRequest.gdprConsent.gdprApplies + }; + } + + function bidSizes(bid) { + return utils.deepAccess(bid, 'mediaTypes.banner.sizes') || bid.sizes /* for prebid < 3 */ || []; + } + + validBidRequests.forEach(bid => { + payload.bids.push({ + bid: bid.bidId, sid: bid.params.sid, siz: bidSizes(bid), viz: elementInView(bid.adUnitCode) + }); + }); + + return { + method: 'POST', url: buildUrl(anyBid.params), data: payload + } + }, + + interpretResponse: function (serverResponse) { + const bids = []; + + if (serverResponse.body && typeof serverResponse.body === 'object') { + serverResponse.body.bids.forEach(bidResponse => { + bids.push({ + requestId: bidResponse.bidId, + cpm: bidResponse.cpm || 0, + width: bidResponse.width || 0, + height: bidResponse.height || 0, + ad: bidResponse.ad, + ttl: FIVE_MINUTES_IN_SECONDS, + currency: 'EUR', + netRevenue: true, + creativeId: '', + }); + }); + } + + return bids; + }, + + getUserSyncs: function (syncOptions, serverResponses, gdprConsent) { + if (serverResponses.length > 0 && syncOptions.iframeEnabled) { + return [{ + type: 'iframe', + url: USER_SYNC_IFRAME_URL + getGdprParams(gdprConsent) + }]; + } + + return []; + } +}; + +registerBidder(spec); diff --git a/modules/stroeerCoreBidAdapter.md b/modules/stroeerCoreBidAdapter.md new file mode 100644 index 00000000000..fe6e92057c6 --- /dev/null +++ b/modules/stroeerCoreBidAdapter.md @@ -0,0 +1,31 @@ +## Overview + +``` +Module Name: Stroeer Bidder Adapter +Module Type: Bidder Adapter +Maintainer: help@cz.stroeer-labs.com +``` + + +## Ad unit configuration for publishers + +```javascript +const adUnits = [{ + code: 'div-gpt-ad-1460505748561-0', + mediaTypes: { + banner: { + sizes: [[300, 250]], + } + }, + bids: [{ + bidder: 'stroeerCore', + params: { + sid: "06b782cc-091b-4f53-9cd2-0291679aa1ac" + } + }] +}]; +``` +### Config Notes + +* Slot id (`sid`) is required. The adapter will ignore bid requests from prebid if `sid` is not provided. This must be in the decoded form. For example, "1234" as opposed to "MTM0ODA=". +* The server ignores dimensions that are not supported by the slot or by the platform (such as 987x123). diff --git a/modules/sublimeBidAdapter.js b/modules/sublimeBidAdapter.js index 1f8cb59f442..41fdb72e76e 100644 --- a/modules/sublimeBidAdapter.js +++ b/modules/sublimeBidAdapter.js @@ -9,7 +9,23 @@ const DEFAULT_CURRENCY = 'EUR'; const DEFAULT_PROTOCOL = 'https'; const DEFAULT_TTL = 600; const SUBLIME_ANTENNA = 'antenna.ayads.co'; -const SUBLIME_VERSION = '0.5.2'; +const SUBLIME_VERSION = '0.7.1'; + +/** + * Identify the current device type + * @returns {string} + */ +function detectDevice() { + const isMobile = /(?:phone|windowss+phone|ipod|blackberry|Galaxy Nexus|SM-G892A|(?:android|bbd+|meego|silk|googlebot) .+?mobile|palm|windowss+ce|opera mini|avantgo|docomo)/i; + + const isTablet = /(?:ipad|playbook|Tablet|(?:android|bb\\d+|meego|silk)(?! .+? mobile))/i; + + return ( + (isMobile.test(navigator.userAgent) && 'm') || // mobile + (isTablet.test(navigator.userAgent) && 't') || // tablet + 'd' // desktop + ); +} /** * Debug log message @@ -23,7 +39,8 @@ export function log(msg, obj) { // Default state export const state = { zoneId: '', - transactionId: '' + transactionId: '', + notifyId: '' }; /** @@ -38,8 +55,9 @@ export function setState(value) { /** * Send pixel to our debug endpoint * @param {string} eventName - Event name that will be send in the e= query string + * @param {string} [sspName] - The optionnal name of the AD provider */ -export function sendEvent(eventName) { +export function sendEvent(eventName, sspName) { const ts = Date.now(); const eventObject = { t: ts, @@ -47,11 +65,18 @@ export function sendEvent(eventName) { z: state.zoneId, e: eventName, src: 'pa', - puid: state.transactionId, - trId: state.transactionId, - ver: SUBLIME_VERSION, + puid: state.transactionId || state.notifyId, + trId: state.transactionId || state.notifyId, + pbav: SUBLIME_VERSION, + pubtimeout: config.getConfig('bidderTimeout'), + pubpbv: '$prebid.version$', + device: detectDevice(), }; + if (eventName === 'bidwon') { + eventObject.sspname = sspName || ''; + } + log('Sending pixel for event: ' + eventName, eventObject); const queryString = utils.formatQS(eventObject); @@ -101,6 +126,7 @@ function buildRequests(validBidRequests, bidderRequest) { setState({ transactionId: bid.transactionId, + notifyId: bid.params.notifyId, zoneId: bid.params.zoneId, debug: bid.params.debug || false, }); @@ -117,6 +143,7 @@ function buildRequests(validBidRequests, bidderRequest) { h: size[1], })), transactionId: bid.transactionId, + notifyId: bid.params.notifyId, zoneId: bid.params.zoneId, }; @@ -125,10 +152,10 @@ function buildRequests(validBidRequests, bidderRequest) { return { method: 'POST', url: protocol + '://' + bidHost + '/bid', - data: payload, + data: JSON.stringify(payload), options: { - contentType: 'application/json', - withCredentials: true + contentType: 'text/plain', + withCredentials: false }, } }); @@ -176,7 +203,8 @@ function interpretResponse(serverResponse, bidRequest) { netRevenue: response.netRevenue || true, ttl: response.ttl || DEFAULT_TTL, ad: response.ad, - pbav: SUBLIME_VERSION + pbav: SUBLIME_VERSION, + sspname: response.sspname || null }; bidResponses.push(bidResponse); @@ -191,7 +219,7 @@ function interpretResponse(serverResponse, bidRequest) { */ function onBidWon(bid) { log('Bid won', bid); - sendEvent('bidwon'); + sendEvent('bidwon', bid.sspname); } /** @@ -207,6 +235,7 @@ export const spec = { code: BIDDER_CODE, gvlid: BIDDER_GVLID, aliases: [], + sendEvent: sendEvent, isBidRequestValid: isBidRequestValid, buildRequests: buildRequests, interpretResponse: interpretResponse, diff --git a/modules/sublimeBidAdapter.md b/modules/sublimeBidAdapter.md index e57f4a1fdb0..5cd1c95b682 100644 --- a/modules/sublimeBidAdapter.md +++ b/modules/sublimeBidAdapter.md @@ -9,7 +9,7 @@ Maintainer: pbjs@sublimeskinz.com # Description Connects to Sublime for bids. -Sublime bid adapter supports Skinz and M-Skinz formats. +Sublime bid adapter supports Skinz. # Nota Bene @@ -53,10 +53,13 @@ var adUnits = [{ bids: [{ bidder: 'sublime', params: { - zoneId: + zoneId: , + notifyId: } }] }]; ``` -Where you replace `` by your Sublime Zone id +Where you replace: +- `` by your Sublime Zone id; +- `` by your Sublime Notify id diff --git a/modules/synacormediaBidAdapter.js b/modules/synacormediaBidAdapter.js index abbae1e7354..aaff637c790 100644 --- a/modules/synacormediaBidAdapter.js +++ b/modules/synacormediaBidAdapter.js @@ -6,7 +6,8 @@ import { BANNER, VIDEO } from '../src/mediaTypes.js'; import includes from 'core-js-pure/features/array/includes.js'; import {config} from '../src/config.js'; -const BID_HOST = 'https://prebid.technoratimedia.com'; +const BID_SCHEME = 'https://'; +const BID_DOMAIN = 'technoratimedia.com'; const USER_SYNC_HOST = 'https://ad-cdn.technoratimedia.com'; const VIDEO_PARAMS = [ 'minduration', 'maxduration', 'startdelay', 'placement', 'linearity', 'mimes', 'protocols', 'api' ]; const BLOCKED_AD_SIZES = [ @@ -23,7 +24,7 @@ export const spec = { bid.mediaTypes.hasOwnProperty('video'); }, isBidRequestValid: function(bid) { - const hasRequiredParams = bid && bid.params && bid.params.hasOwnProperty('placementId') && bid.params.hasOwnProperty('seatId'); + const hasRequiredParams = bid && bid.params && (bid.params.hasOwnProperty('placementId') || bid.params.hasOwnProperty('tagId')) && bid.params.hasOwnProperty('seatId'); const hasAdSizes = bid && getAdUnitSizes(bid).filter(size => BLOCKED_AD_SIZES.indexOf(size.join('x')) === -1).length > 0 return !!(hasRequiredParams && hasAdSizes); }, @@ -36,7 +37,7 @@ export const spec = { const openRtbBidRequest = { id: bidderRequest.auctionId, site: { - domain: location.hostname, + domain: config.getConfig('publisherDomain') || location.hostname, page: refererInfo.referer, ref: document.referrer }, @@ -60,7 +61,7 @@ export const spec = { } else { seatId = bid.params.seatId; } - const placementId = bid.params.placementId; + const tagIdOrplacementId = bid.params.tagId || bid.params.placementId; const bidFloor = bid.params.bidfloor ? parseFloat(bid.params.bidfloor) : null; if (isNaN(bidFloor)) { logWarn(`Synacormedia: there is an invalid bid floor: ${bid.params.bidfloor}`); @@ -76,9 +77,9 @@ export const spec = { let imps = []; if (videoOrBannerKey === 'banner') { - imps = this.buildBannerImpressions(adSizes, bid, placementId, pos, bidFloor, videoOrBannerKey); + imps = this.buildBannerImpressions(adSizes, bid, tagIdOrplacementId, pos, bidFloor, videoOrBannerKey); } else if (videoOrBannerKey === 'video') { - imps = this.buildVideoImpressions(adSizes, bid, placementId, pos, bidFloor, videoOrBannerKey); + imps = this.buildVideoImpressions(adSizes, bid, tagIdOrplacementId, pos, bidFloor, videoOrBannerKey); } if (imps.length > 0) { imps.forEach(i => openRtbBidRequest.imp.push(i)); @@ -93,7 +94,7 @@ export const spec = { if (openRtbBidRequest.imp.length && seatId) { return { method: 'POST', - url: `${BID_HOST}/openrtb/bids/${seatId}?src=$$REPO_AND_VERSION$$`, + url: `${BID_SCHEME}${seatId}.${BID_DOMAIN}/openrtb/bids/${seatId}?src=$$REPO_AND_VERSION$$`, data: openRtbBidRequest, options: { contentType: 'application/json', @@ -103,7 +104,7 @@ export const spec = { } }, - buildBannerImpressions: function(adSizes, bid, placementId, pos, bidFloor, videoOrBannerKey) { + buildBannerImpressions: function (adSizes, bid, tagIdOrPlacementId, pos, bidFloor, videoOrBannerKey) { let format = []; let imps = []; adSizes.forEach((size, i) => { @@ -124,7 +125,7 @@ export const spec = { format, pos }, - tagid: placementId, + tagid: tagIdOrPlacementId, }; if (bidFloor !== null && !isNaN(bidFloor)) { imp.bidfloor = bidFloor; @@ -134,7 +135,7 @@ export const spec = { return imps; }, - buildVideoImpressions: function(adSizes, bid, placementId, pos, bidFloor, videoOrBannerKey) { + buildVideoImpressions: function(adSizes, bid, tagIdOrPlacementId, pos, bidFloor, videoOrBannerKey) { let imps = []; adSizes.forEach((size, i) => { if (!size || size.length != 2) { @@ -144,7 +145,7 @@ export const spec = { const size1 = size[1]; const imp = { id: `${videoOrBannerKey.substring(0, 1)}${bid.bidId}-${size0}x${size1}`, - tagid: placementId + tagid: tagIdOrPlacementId }; if (bidFloor !== null && !isNaN(bidFloor)) { imp.bidfloor = bidFloor; @@ -156,6 +157,9 @@ export const spec = { pos }; if (bid.mediaTypes.video) { + if (!bid.params.video) { + bid.params.video = {}; + } this.setValidVideoParams(bid.mediaTypes.video, bid.params.video); } if (bid.params.video) { @@ -181,7 +185,6 @@ export const spec = { logWarn('Synacormedia: server returned empty/non-json response: ' + JSON.stringify(serverResponse.body)); return; } - const {id, seatbid: seatbids} = serverResponse.body; let bids = []; if (id && seatbids) { @@ -189,7 +192,7 @@ export const spec = { seatbid.bid.forEach(bid => { const creative = updateMacros(bid, bid.adm); const nurl = updateMacros(bid, bid.nurl); - const [, impType, impid] = bid.impid.match(/^([vb])(.*)$/); + const [, impType, impid] = bid.impid.match(/^([vb])([\w\d]+)/); let height = bid.h; let width = bid.w; const isVideo = impType === 'v'; @@ -218,7 +221,6 @@ export const spec = { } const bidObj = { requestId: impid, - adId: bid.id.replace(/~/g, '-'), cpm: parseFloat(bid.price), width: parseInt(width, 10), height: parseInt(height, 10), @@ -229,6 +231,11 @@ export const spec = { ad: creative, ttl: 60 }; + + if (bid.adomain != undefined || bid.adomain != null) { + bidObj.meta = { advertiserDomains: bid.adomain }; + } + if (isVideo) { const [, uuid] = nurl.match(/ID=([^&]*)&?/); if (!config.getConfig('cache.url')) { diff --git a/modules/synacormediaBidAdapter.md b/modules/synacormediaBidAdapter.md index fd71f07b3a3..523c66fd1d9 100644 --- a/modules/synacormediaBidAdapter.md +++ b/modules/synacormediaBidAdapter.md @@ -33,7 +33,7 @@ https://track.technoratimedia.com/openrtb/tags?ID=%%PATTERN:hb_cache_id_synacorm bidder: "synacormedia", params: { seatId: "prebid", - placementId: "demo1", + tagId: "demo1", bidfloor: 0.10, pos: 1 } @@ -52,7 +52,7 @@ https://track.technoratimedia.com/openrtb/tags?ID=%%PATTERN:hb_cache_id_synacorm bidder: "synacormedia", params: { seatId: "prebid", - placementId: "demo1", + tagId: "demo1", bidfloor: 0.20, pos: 1, video: { diff --git a/modules/tapadIdSystem.js b/modules/tapadIdSystem.js new file mode 100644 index 00000000000..a49d62897ad --- /dev/null +++ b/modules/tapadIdSystem.js @@ -0,0 +1,61 @@ +import { uspDataHandler } from '../src/adapterManager.js'; +import { submodule } from '../src/hook.js'; +import * as ajax from '../src/ajax.js' +import * as utils from '../src/utils.js'; + +export const graphUrl = 'https://rtga.tapad.com/v1/graph'; + +export const tapadIdSubmodule = { + name: 'tapadId', + /** + * decode the stored id value for passing to bid requests + * @function + * @returns {{tapadId: string} | undefined} + */ + decode(id) { + return { tapadId: id }; + }, + /* + * @function + * @summary initiate Real Time Graph + * @param {SubmoduleParams} [configParams] + * @param {ConsentData} [consentData] + * @returns {IdResponse }} + */ + getId(config) { + const uspData = uspDataHandler.getConsentData(); + if (uspData && uspData !== '1---') { + return { id: undefined }; + } + const configParams = config.params || {}; + + if (configParams.companyId == null || isNaN(Number(configParams.companyId))) { + utils.logMessage('Please provide a valid Company Id. Contact prebid@tapad.com for assistance.'); + } + + return { + callback: (complete) => { + ajax.ajaxBuilder(10000)( + `${graphUrl}?company_id=${configParams.companyId}&tapad_id_type=TAPAD_ID`, + { + success: (response) => { + const responseJson = JSON.parse(response); + if (responseJson.hasOwnProperty('tapadId')) { + complete(responseJson.tapadId); + } + }, + error: (_, e) => { + if (e.status === 404) { + complete(undefined); + } + if (e.status === 403) { + utils.logMessage('Invalid Company Id. Contact prebid@tapad.com for assistance.'); + } + } + } + ); + } + } + } +} +submodule('userId', tapadIdSubmodule); diff --git a/modules/tapadIdSystem.md b/modules/tapadIdSystem.md new file mode 100644 index 00000000000..4bab1b4689d --- /dev/null +++ b/modules/tapadIdSystem.md @@ -0,0 +1,43 @@ +### Tapad ID + +Tapad's ID module provides access to a universal identifier that publishers, ad tech platforms and advertisers can use for data collection and collation without reliance on third-party cookies. +Tapad's ID module is free to use and promotes collaboration across the industry by facilitating interoperability between DSPs, SSPs and publishers. + +To register as an authorized user of the Tapad ID module, or for more information, documentation and access to Tapad’s Terms and Conditions please contact [prebid@tapad.com](mailto:prebid@tapad.com). + +Tapad’s Privacy landing page containing links to region-specific Privacy Notices may be found here: [https://tapad.com/privacy.html](https://tapad.com/privacy.html). + +Add it to your Prebid.js package with: + + +`gulp build --modules=userId,tapadIdSystem` + +#### Tapad ID Configuration + +| Param under userSync.userIds[] | Scope | Type | Description | Example | +| --- | --- | --- | --- | --- | +| name | Required | String | `"tapadId"` | `"tapadId"` | +| params | Required | Object | Details for Tapad initialization. | | +| params.company_id | Required | Number | Tapad Company Id provided by Tapad | 1234567890 | + +#### Tapad ID Example + +```js +pbjs.setConfig({ + userSync: { + userIds: [ + { + name: "tapadId", + params: { + companyId: 1234567890 + }, + storage: { + type: "cookie", + name: "tapad_id", + expires: 1 + } + } + ] + } +}); +``` diff --git a/modules/tappxBidAdapter.js b/modules/tappxBidAdapter.js new file mode 100644 index 00000000000..566795a204b --- /dev/null +++ b/modules/tappxBidAdapter.js @@ -0,0 +1,417 @@ +'use strict'; + +import * as utils from '../src/utils.js'; +import { registerBidder } from '../src/adapters/bidderFactory.js'; +import { BANNER, VIDEO } from '../src/mediaTypes.js'; +import { config } from '../src/config.js'; + +const BIDDER_CODE = 'tappx'; +const TTL = 360; +const CUR = 'USD'; +const TAPPX_BIDDER_VERSION = '0.1.10514'; +const TYPE_CNN = 'prebidjs'; +const VIDEO_SUPPORT = ['instream']; + +var hostDomain; + +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: function(bid) { + return validBasic(bid) && validMediaType(bid) + }, + + /** + * Takes an array of valid bid requests, all of which are guaranteed to have passed the isBidRequestValid() test. + * Make a server request from the list of BidRequests. + * + * @param {*} validBidRequests + * @param {*} bidderRequest + * @return ServerRequest Info describing the request to the server. + */ + buildRequests: function(validBidRequests, bidderRequest) { + let requests = []; + validBidRequests.forEach(oneValidRequest => { + requests.push(buildOneRequest(oneValidRequest, bidderRequest)); + }); + return requests; + }, + + /** + * Parse the response and generate one or more bid objects. + * + * @param {*} serverResponse + * @param {*} originalRequest + */ + interpretResponse: function(serverResponse, originalRequest) { + const responseBody = serverResponse.body; + if (!serverResponse.body) { + utils.logWarn('[TAPPX]: Empty response body HTTP 204, no bids'); + return []; + } + + const bids = []; + responseBody.seatbid.forEach(serverSeatBid => { + serverSeatBid.bid.forEach(serverBid => { + bids.push(interpretBid(serverBid, originalRequest)); + }); + }); + + return bids; + }, + + /** + * If the publisher allows user-sync activity, the platform will call this function and the adapter may register pixels and/or iframe user syncs. + * + * @param {*} syncOptions + * @param {*} serverResponses + * @param {*} gdprConsent + */ + getUserSyncs: (syncOptions, serverResponses, gdprConsent, uspConsent) => { + let url = `https://${hostDomain}/cs/usersync.php?`; + + // GDPR & CCPA + if (gdprConsent) { + url += '&gdpr=' + (gdprConsent.gdprApplies ? 1 : 0); + url += '&gdpr_consent=' + encodeURIComponent(gdprConsent.consentString || ''); + } + if (uspConsent) { + url += '&us_privacy=' + encodeURIComponent(uspConsent); + } + + // SyncOptions + if (syncOptions.iframeEnabled) { + url += '&type=iframe' + return [{ + type: 'iframe', + url: url + }]; + } else { + url += '&type=img' + return [{ + type: 'image', + url: url + }]; + } + } +} + +function validBasic(bid) { + if (bid.params == null) { + utils.logWarn(`[TAPPX]: Please review the mandatory Tappx parameters.`); + return false; + } + + if (bid.params.tappxkey == null) { + utils.logWarn(`[TAPPX]: Please review the mandatory Tappxkey parameter.`); + return false; + } + + if (bid.params.host == null) { + utils.logWarn(`[TAPPX]: Please review the mandatory Host parameter.`); + return false; + } + + let classicEndpoint = true + if ((new RegExp(`^(vz.*|zz.*)\.*$`, 'i')).test(bid.params.host)) { + classicEndpoint = false + } + + if (classicEndpoint && bid.params.endpoint == null) { + utils.logWarn(`[TAPPX]: Please review the mandatory endpoint Tappx parameters.`); + return false; + } + + return true; +} + +function validMediaType(bid) { + const video = utils.deepAccess(bid, 'mediaTypes.video'); + + // Video validations + if (typeof video != 'undefined') { + if (VIDEO_SUPPORT.indexOf(video.context) === -1) { + utils.logWarn(`[TAPPX]: Please review the mandatory Tappx parameters for Video. Only "instream" is suported.`); + return false; + } + } + + return true; +} + +/** + * Parse the response and generate one bid object. + * + * @param {object} serverBid Bid by OpenRTB 2.5 + * @returns {object} Prebid banner bidObject + */ +function interpretBid(serverBid, request) { + let bidReturned = { + requestId: request.bids.bidId, + cpm: serverBid.price, + currency: serverBid.cur ? serverBid.cur : CUR, + width: serverBid.w, + height: serverBid.h, + ttl: TTL, + creativeId: serverBid.crid, + netRevenue: true, + } + + if (typeof serverBid.dealId != 'undefined') { bidReturned.dealId = serverBid.dealId } + + if (typeof request.bids.mediaTypes != 'undefined' && typeof request.bids.mediaTypes.video != 'undefined') { + bidReturned.vastXml = serverBid.adm; + bidReturned.vastUrl = serverBid.lurl; + bidReturned.ad = serverBid.adm; + bidReturned.mediaType = VIDEO; + } else { + bidReturned.ad = serverBid.adm; + bidReturned.mediaType = BANNER; + } + + if (typeof bidReturned.adomain != 'undefined' || bidReturned.adomain != null) { + bidReturned.meta = { advertiserDomains: request.bids.adomain }; + } + + return bidReturned; +} + +/** +* Build and makes the request +* +* @param {*} validBidRequests +* @param {*} bidderRequest +* @return response ad +*/ +function buildOneRequest(validBidRequests, bidderRequest) { + let hostInfo = getHostInfo(validBidRequests); + const ENDPOINT = hostInfo.endpoint; + hostDomain = hostInfo.domain; + + const TAPPXKEY = utils.deepAccess(validBidRequests, 'params.tappxkey'); + const BIDFLOOR = utils.deepAccess(validBidRequests, 'params.bidfloor'); + const BIDEXTRA = utils.deepAccess(validBidRequests, 'params.ext'); + const bannerMediaType = utils.deepAccess(validBidRequests, 'mediaTypes.banner'); + const videoMediaType = utils.deepAccess(validBidRequests, 'mediaTypes.video'); + const { refererInfo } = bidderRequest; + + // let requests = []; + let payload = {}; + let publisher = {}; + let tagid; + let api = {}; + + // > App/Site object + if (utils.deepAccess(validBidRequests, 'params.app')) { + let app = {}; + app.name = utils.deepAccess(validBidRequests, 'params.app.name'); + app.bundle = utils.deepAccess(validBidRequests, 'params.app.bundle'); + app.domain = utils.deepAccess(validBidRequests, 'params.app.domain'); + publisher.name = utils.deepAccess(validBidRequests, 'params.app.publisher.name'); + publisher.domain = utils.deepAccess(validBidRequests, 'params.app.publisher.domain'); + tagid = `${app.name}_typeAdBanVid_${getOs()}`; + payload.app = app; + api[0] = utils.deepAccess(validBidRequests, 'params.api') ? utils.deepAccess(validBidRequests, 'params.api') : [3, 5]; + } else { + let site = {}; + site.name = (bidderRequest && refererInfo) ? utils.parseUrl(refererInfo.referer).hostname : window.location.hostname; + site.bundle = (bidderRequest && refererInfo) ? utils.parseUrl(refererInfo.referer).hostname : window.location.hostname; + site.domain = (bidderRequest && refererInfo) ? utils.parseUrl(refererInfo.referer).hostname : window.location.hostname; + publisher.name = (bidderRequest && refererInfo) ? utils.parseUrl(refererInfo.referer).hostname : window.location.hostname; + publisher.domain = (bidderRequest && refererInfo) ? utils.parseUrl(refererInfo.referer).hostname : window.location.hostname; + tagid = `${site.name}_typeAdBanVid_${getOs()}`; + payload.site = site; + } + // < App/Site object + + // > Imp object + let imp = {}; + let w; + let h; + + if (bannerMediaType) { + let banner = {}; + w = bannerMediaType.sizes[0][0]; + h = bannerMediaType.sizes[0][1]; + banner.w = w; + banner.h = h; + if ( + ((bannerMediaType.sizes[0].indexOf(480) >= 0) && (bannerMediaType.sizes[0].indexOf(320) >= 0)) || + ((bannerMediaType.sizes[0].indexOf(768) >= 0) && (bannerMediaType.sizes[0].indexOf(1024) >= 0))) { + banner.pos = 7 + } else { + banner.pos = 4 + } + + banner.api = api; + + let format = {}; + format[0] = {}; + format[0].w = w; + format[0].h = h; + banner.format = format; + + imp.banner = banner; + } + + if (videoMediaType) { + let video = {}; + w = videoMediaType.playerSize[0][0]; + h = videoMediaType.playerSize[0][1]; + video.w = w; + video.h = h; + + video.mimes = videoMediaType.mimes; + + imp.video = video; + } + + imp.id = validBidRequests.bidId; + imp.tagid = tagid; + imp.secure = 1; + + imp.bidfloor = utils.deepAccess(validBidRequests, 'params.bidfloor'); + if (utils.isFn(validBidRequests.getFloor)) { + try { + let floor = validBidRequests.getFloor({ + currency: CUR, + mediaType: '*', + size: '*' + }); + if (utils.isPlainObject(floor) && !isNaN(floor.floor) && floor.currency === 'USD') { + imp.bidfloor = floor.floor; + } else { + utils.logWarn('[TAPPX]: ', 'Currency not valid. Use only USD with Tappx.'); + } + } catch (e) { + utils.logWarn('[TAPPX]: ', e); + imp.bidfloor = utils.deepAccess(validBidRequests, 'params.bidfloor'); // Be sure that we have an imp.bidfloor + } + } + + let bidder = {}; + bidder.tappxkey = TAPPXKEY; + bidder.endpoint = ENDPOINT; + bidder.host = hostInfo.url; + bidder.bidfloor = BIDFLOOR; + bidder.ext = (typeof BIDEXTRA == 'object') ? BIDEXTRA : undefined; + + imp.ext = {}; + imp.ext.bidder = bidder; + // < Imp object + + // > Device object + let device = {}; + // Mandatory + device.os = getOs(); + device.ip = 'peer'; + device.ua = navigator.userAgent; + device.ifa = validBidRequests.ifa; + + // Optional + device.h = screen.height; + device.w = screen.width; + device.dnt = utils.getDNT() ? 1 : 0; + device.language = getLanguage(); + device.make = navigator.vendor ? navigator.vendor : ''; + + let geo = {}; + geo.country = utils.deepAccess(validBidRequests, 'params.geo.country'); + // < Device object + + // > Params + let params = {}; + params.host = 'tappx.com'; + params.bidfloor = BIDFLOOR; + // < Params + + // > GDPR + let regs = {}; + regs.gdpr = 0; + if (!(bidderRequest.gdprConsent == null)) { + if (typeof bidderRequest.gdprConsent.gdprApplies === 'boolean') { regs.gdpr = bidderRequest.gdprConsent.gdprApplies; } + if (regs.gdpr) { regs.consent = bidderRequest.gdprConsent.consentString; } + } + + // CCPA + regs.ext = {}; + if (!(bidderRequest.uspConsent == null)) { + regs.ext.us_privacy = bidderRequest.uspConsent; + } + + // COPPA compliance + if (config.getConfig('coppa') === true) { + regs.coppa = config.getConfig('coppa') === true ? 1 : 0; + } + + // Universal ID + const eidsArr = utils.deepAccess(validBidRequests, 'userIdAsEids'); + payload.user = { + ext: { + eids: eidsArr + } + }; + // < GDPR + + // > Payload + payload.id = validBidRequests.auctionId; + payload.test = utils.deepAccess(validBidRequests, 'params.test') ? 1 : 0; + payload.at = 1; + payload.tmax = bidderRequest.timeout ? bidderRequest.timeout : 600; + payload.bidder = BIDDER_CODE; + payload.imp = [imp]; + + payload.device = device; + payload.params = params; + payload.regs = regs; + // < Payload + + return { + method: 'POST', + url: `${hostInfo.url}?type_cnn=${TYPE_CNN}&v=${TAPPX_BIDDER_VERSION}`, + data: JSON.stringify(payload), + bids: validBidRequests + }; +} + +function getLanguage() { + const language = navigator.language ? 'language' : 'userLanguage'; + return navigator[language].split('-')[0]; +} + +function getOs() { + let ua = navigator.userAgent; + if (ua == null) { return 'unknown'; } else if (ua.match(/(iPhone|iPod|iPad)/)) { return 'ios'; } else if (ua.match(/Android/)) { return 'android'; } else if (ua.match(/Window/)) { return 'windows'; } else { return 'unknown'; } +} + +function getHostInfo(validBidRequests) { + let domainInfo = {}; + let endpoint = utils.deepAccess(validBidRequests, 'params.endpoint'); + let hostParam = utils.deepAccess(validBidRequests, 'params.host'); + + domainInfo.domain = hostParam.split('/', 1)[0]; + + let regexNewEndpoints = new RegExp(`^(vz.*|zz.*)\.pub\.tappx\.com$`, 'i'); + let regexClassicEndpoints = new RegExp(`^([a-z]{3}|testing)\.[a-z]{3}\.tappx\.com$`, 'i'); + + if (regexNewEndpoints.test(domainInfo.domain)) { + domainInfo.newEndpoint = true; + domainInfo.endpoint = domainInfo.domain.split('.', 1)[0] + domainInfo.url = `https://${hostParam}` + } else if (regexClassicEndpoints.test(domainInfo.domain)) { + domainInfo.newEndpoint = false; + domainInfo.endpoint = endpoint + domainInfo.url = `https://${hostParam}${endpoint}` + } + + return domainInfo; +} + +registerBidder(spec); diff --git a/modules/tappxBidAdapter.md b/modules/tappxBidAdapter.md new file mode 100644 index 00000000000..776b24bb07c --- /dev/null +++ b/modules/tappxBidAdapter.md @@ -0,0 +1,70 @@ +# Overview +``` +Module Name: Tappx Bidder Adapter +Module Type: Bidder Adapter +Maintainer: prebid@tappx.com +``` + +# Description +Module that connects to :tappx demand sources. +Suppots Banner and Instream Video. +Please use ```tappx``` as the bidder code. +Ads sizes available: [300,250], [320,50], [320,480], [480,320], [728,90], [768,1024], [1024,768] + +# Banner Test Parameters +``` + var adUnits = [ + { + code: 'banner-ad-div', + mediaTypes: { + banner: { + sizes: [[320,50]] + } + }, + bids: [ + { + bidder: "tappx", + params: { + host: "testing.ssp.tappx.com/rtb/v2/", + tappxkey: "pub-1234-android-1234", + endpoint: "ZZ1234PBJS", + bidfloor: 0.005, + test: true // Optional for testing purposes + } + } + ] + } + ]; +``` + + +# Video Test Parameters +``` + var adUnits = [ + { + code: 'video-ad-div', + renderer: { + options: { + text: "Tappx instream Video" + } + }, + mediaTypes: { + video: { + context: "instream", + mimes : [ "video/mp4", "application/javascript" ], + playerSize: [320, 250] + } + }, + bids: [{ + bidder: 'tappx', + params: { + host: "testing.ssp.tappx.com/rtb/v2/", + tappxkey: "pub-1234-desktop-1234", + endpoint: "VZ12TESTCTV", + bidfloor: 0.005, + test: true + } + }] + } + ]; +``` diff --git a/modules/teadsBidAdapter.js b/modules/teadsBidAdapter.js index 08ae1854669..32be7e62bbd 100644 --- a/modules/teadsBidAdapter.js +++ b/modules/teadsBidAdapter.js @@ -1,6 +1,7 @@ import {registerBidder} from '../src/adapters/bidderFactory.js'; const utils = require('../src/utils.js'); const BIDDER_CODE = 'teads'; +const GVL_ID = 132; const ENDPOINT_URL = 'https://a.teads.tv/hb/bid-request'; const gdprStatus = { GDPR_APPLIES_PUBLISHER: 12, @@ -11,6 +12,7 @@ const gdprStatus = { export const spec = { code: BIDDER_CODE, + gvlid: GVL_ID, supportedMediaTypes: ['video', 'banner'], /** * Determines whether or not the given bid request is valid. @@ -97,6 +99,9 @@ export const spec = { currency: bid.currency, netRevenue: true, ttl: bid.ttl, + meta: { + advertiserDomains: bid && bid.adomain ? bid.adomain : [] + }, ad: bid.ad, requestId: bid.bidId, creativeId: bid.creativeId, @@ -133,8 +138,8 @@ function getTimeToFirstByte(win) { performance.getEntriesByType('navigation')[0] && performance.getEntriesByType('navigation')[0].responseStart && performance.getEntriesByType('navigation')[0].requestStart && - performance.getEntriesByType('navigation')[0].responseStart >= 0 && - performance.getEntriesByType('navigation')[0].requestStart >= 0 && + performance.getEntriesByType('navigation')[0].responseStart > 0 && + performance.getEntriesByType('navigation')[0].requestStart > 0 && Math.round( performance.getEntriesByType('navigation')[0].responseStart - performance.getEntriesByType('navigation')[0].requestStart ); @@ -146,8 +151,8 @@ function getTimeToFirstByte(win) { const ttfbWithTimingV1 = performance && performance.timing.responseStart && performance.timing.requestStart && - performance.timing.responseStart >= 0 && - performance.timing.requestStart >= 0 && + performance.timing.responseStart > 0 && + performance.timing.requestStart > 0 && performance.timing.responseStart - performance.timing.requestStart; return ttfbWithTimingV1 ? ttfbWithTimingV1.toString() : ''; diff --git a/modules/telariaBidAdapter.js b/modules/telariaBidAdapter.js index acc20f6b183..b2904045144 100644 --- a/modules/telariaBidAdapter.js +++ b/modules/telariaBidAdapter.js @@ -124,7 +124,7 @@ function getDefaultSrcPageUrl() { } function getEncodedValIfNotEmpty(val) { - return !utils.isEmpty(val) ? encodeURIComponent(val) : ''; + return (val !== '' && val !== undefined) ? encodeURIComponent(val) : ''; } /** @@ -276,7 +276,6 @@ function createBid(status, reqBid, response, width, height, bidderCode) { width: width, height: height, bidderCode: bidderCode, - adId: response.id, currency: 'USD', netRevenue: true, ttl: 300, diff --git a/modules/temedyaBidAdapter.js b/modules/temedyaBidAdapter.js new file mode 100644 index 00000000000..4577fb295cd --- /dev/null +++ b/modules/temedyaBidAdapter.js @@ -0,0 +1,144 @@ +import * as utils from '../src/utils.js'; +import { registerBidder } from '../src/adapters/bidderFactory.js'; +import { BANNER, NATIVE } from '../src/mediaTypes.js'; + +const BIDDER_CODE = 'temedya'; +const ENDPOINT_URL = 'https://adm.vidyome.com/'; +const ENDPOINT_METHOD = 'GET'; +const CURRENCY = 'TRY'; + +export const spec = { + code: BIDDER_CODE, + supportedMediaTypes: [BANNER, NATIVE], + /** + * Determines whether or not the given bid request is valid. + * + * @param {BidRequest} bid The bid params to validate. + * @return boolean True if this is a valid bid, and false otherwise. + */ + isBidRequestValid: function (bid) { + return !!(bid.params.widgetId); + }, + /** + * Make a server request from the list of BidRequests. + * + * @param {validBidRequests[]} - an array of bids + * @return ServerRequest Info describing the request to the server. + */ + buildRequests: function (validBidRequests, bidderRequest) { + return validBidRequests.map(req => { + const mediaType = this._isBannerRequest(req) ? 'display' : NATIVE; + const data = { + wid: req.params.widgetId, + type: mediaType, + count: (req.params.count > 6 ? 6 : req.params.count) || 1, + mediaType: mediaType, + requestid: req.bidId + }; + if (mediaType === 'display') { + data.sizes = utils.parseSizesInput( + req.mediaTypes && req.mediaTypes.banner && req.mediaTypes.banner.sizes + ).join('|') + } + /** @type {ServerRequest} */ + return { + method: ENDPOINT_METHOD, + url: ENDPOINT_URL, + data: utils.parseQueryStringParameters(data), + options: { withCredentials: false, requestId: req.bidId, mediaType: mediaType } + }; + }); + }, + /** + * Unpack the response from the server into a list of bids. + * + * @param {ServerResponse} serverResponse A successful response from the server. + * @return {Bid[]} An array of bids which were nested inside the server. + */ + interpretResponse: function (serverResponse, bidRequest) { + try { + const bidResponse = serverResponse.body; + const bidResponses = []; + if (bidResponse && bidRequest.options.mediaType == NATIVE) { + bidResponse.ads.forEach(function(ad) { + bidResponses.push({ + requestId: bidRequest.options.requestId, + cpm: parseFloat(ad.assets.cpm) || 1, + width: 320, + height: 240, + creativeId: ad.assets.id, + currency: ad.currency || CURRENCY, + netRevenue: false, + mediaType: NATIVE, + ttl: 360, + native: { + title: ad.assets.title, + body: ad.assets.body || '', + icon: { + url: ad.assets.files[0], + width: 320, + height: 240 + }, + image: { + url: ad.assets.files[0], + width: 320, + height: 240 + }, + privacyLink: '', + clickUrl: ad.assets.click_url, + displayUrl: ad.assets.click_url, + cta: '', + sponsoredBy: ad.assets.sponsor || '', + impressionTrackers: [bidResponse.base.widget.impression + '&ids=' + ad.id + ':' + ad.assets.id], + }, + }); + }); + } else if (bidResponse && bidRequest.options.mediaType == 'display') { + bidResponse.ads.forEach(function(ad) { + let w = ad.assets.width || 300; + let h = ad.assets.height || 250; + let htmlTag = ''; + htmlTag += ''; + bidResponses.push({ + requestId: bidRequest.options.requestId, + cpm: parseFloat(ad.assets.cpm) || 1, + width: w, + height: h, + creativeId: ad.assets.id, + currency: ad.currency || CURRENCY, + netRevenue: false, + ttl: 360, + mediaType: BANNER, + ad: htmlTag + }); + }); + } + return bidResponses; + } catch (err) { + utils.logError(err); + return []; + } + }, + /** + * @param {BidRequest} req + * @return {boolean} + * @private + */ + _isBannerRequest(req) { + return !!(req.mediaTypes && req.mediaTypes.banner); + } +} +registerBidder(spec); diff --git a/modules/temedyaBidAdapter.md b/modules/temedyaBidAdapter.md new file mode 100644 index 00000000000..31ac4f3689b --- /dev/null +++ b/modules/temedyaBidAdapter.md @@ -0,0 +1,64 @@ +# Overview + +Module Name: TE Medya Bidder Adapter +Module Type: Bidder Adapter +Maintainer: prebid@temedya.com + +# Description + +Module that connects to TE Medya's demand sources. + +TE Medya supports Native and Banner. + + +# Test Parameters +# Native +``` + + var adUnits = [ + { + code:'tme_div_id', + mediaTypes:{ + native: { + title: { + required: true + } + } + }, + bids:[ + { + bidder: 'temedya', + params: { + widgetId: 753497, + count: 1 + } + } + ] + } + ]; +``` +# Test Parameters +# Banner +``` + + var adUnits = [ + { + code:'tme_div_id', + mediaTypes:{ + banner: { + banner: { + sizes:[300, 250] + } + } + }, + bids:[ + { + bidder: 'temedya', + params: { + widgetId: 753497 + } + } + ] + } + ]; +``` \ No newline at end of file diff --git a/modules/tripleliftBidAdapter.js b/modules/tripleliftBidAdapter.js index d6b1f95351d..e8d248eea03 100644 --- a/modules/tripleliftBidAdapter.js +++ b/modules/tripleliftBidAdapter.js @@ -3,20 +3,17 @@ import { registerBidder } from '../src/adapters/bidderFactory.js'; import * as utils from '../src/utils.js'; import { config } from '../src/config.js'; +const GVLID = 28; const BIDDER_CODE = 'triplelift'; const STR_ENDPOINT = 'https://tlx.3lift.com/header/auction?'; let gdprApplies = true; let consentString = null; export const tripleliftAdapterSpec = { - + gvlid: GVLID, code: BIDDER_CODE, supportedMediaTypes: [BANNER, VIDEO], isBidRequestValid: function (bid) { - if (bid.mediaTypes.video) { - let video = _getORTBVideo(bid); - if (!video.w || !video.h) return false; - } return typeof bid.params.inventoryCode !== 'undefined'; }, @@ -111,24 +108,31 @@ function _getSyncType(syncOptions) { function _buildPostBody(bidRequests) { let data = {}; let { schain } = bidRequests[0]; + const globalFpd = _getGlobalFpd(); + data.imp = bidRequests.map(function(bidRequest, index) { let imp = { id: index, tagid: bidRequest.params.inventoryCode, floor: _getFloor(bidRequest) }; - if (bidRequest.mediaTypes.video) { + // remove the else to support multi-imp + if (_isInstreamBidRequest(bidRequest)) { imp.video = _getORTBVideo(bidRequest); } else if (bidRequest.mediaTypes.banner) { imp.banner = { format: _sizes(bidRequest.sizes) }; }; + if (!utils.isEmpty(bidRequest.ortb2Imp)) { + imp.fpd = _getAdUnitFpd(bidRequest.ortb2Imp); + } return imp; }); let eids = [ - ...getUnifiedIdEids(bidRequests), - ...getIdentityLinkEids(bidRequests), - ...getCriteoEids(bidRequests) + ...getUnifiedIdEids([bidRequests[0]]), + ...getIdentityLinkEids([bidRequests[0]]), + ...getCriteoEids([bidRequests[0]]), + ...getPubCommonEids([bidRequests[0]]) ]; if (eids.length > 0) { @@ -137,14 +141,24 @@ function _buildPostBody(bidRequests) { }; } - if (schain) { - data.ext = { - schain - } + let ext = _getExt(schain, globalFpd); + + if (!utils.isEmpty(ext)) { + data.ext = ext; } return data; } +function _isInstreamBidRequest(bidRequest) { + if (!bidRequest.mediaTypes.video) return false; + if (!bidRequest.mediaTypes.video.context) return false; + if (bidRequest.mediaTypes.video.context.toLowerCase() === 'instream') { + return true; + } else { + return false; + } +} + function _getORTBVideo(bidRequest) { // give precedent to mediaTypes.video let video = { ...bidRequest.params.video, ...bidRequest.mediaTypes.video }; @@ -161,8 +175,8 @@ function _getFloor (bid) { if (typeof bid.getFloor === 'function') { const floorInfo = bid.getFloor({ currency: 'USD', - mediaType: 'banner', - size: _sizes(bid.sizes) + mediaType: _isInstreamBidRequest(bid) ? 'video' : 'banner', + size: '*' }); if (typeof floorInfo === 'object' && floorInfo.currency === 'USD' && !isNaN(parseFloat(floorInfo.floor))) { @@ -172,20 +186,79 @@ function _getFloor (bid) { return floor !== null ? floor : bid.params.floor; } -function getUnifiedIdEids(bidRequests) { - return getEids(bidRequests, 'tdid', 'adserver.org', 'TDID'); +function _getGlobalFpd() { + const fpd = {}; + const context = {} + const user = {}; + const ortbData = config.getLegacyFpd(config.getConfig('ortb2')) || {}; + + const fpdContext = Object.assign({}, ortbData.context); + const fpdUser = Object.assign({}, ortbData.user); + + _addEntries(context, fpdContext); + _addEntries(user, fpdUser); + + if (!utils.isEmpty(context)) { + fpd.context = context; + } + if (!utils.isEmpty(user)) { + fpd.user = user; + } + return fpd; } -function getIdentityLinkEids(bidRequests) { - return getEids(bidRequests, 'idl_env', 'liveramp.com', 'idl'); +function _getAdUnitFpd(adUnitFpd) { + const fpd = {}; + const context = {}; + + _addEntries(context, adUnitFpd.ext); + + if (!utils.isEmpty(context)) { + fpd.context = context; + } + + return fpd; } -function getCriteoEids(bidRequests) { - return getEids(bidRequests, 'criteoId', 'criteo.com', 'criteoId'); +function _addEntries(target, source) { + if (!utils.isEmpty(source)) { + Object.keys(source).forEach(key => { + if (source[key] != null) { + target[key] = source[key]; + } + }); + } } -function getEids(bidRequests, type, source, rtiPartner) { - return bidRequests +function _getExt(schain, fpd) { + let ext = {}; + if (!utils.isEmpty(schain)) { + ext.schain = { ...schain }; + } + if (!utils.isEmpty(fpd)) { + ext.fpd = { ...fpd }; + } + return ext; +} + +function getUnifiedIdEids(bidRequest) { + return getEids(bidRequest, 'tdid', 'adserver.org', 'TDID'); +} + +function getIdentityLinkEids(bidRequest) { + return getEids(bidRequest, 'idl_env', 'liveramp.com', 'idl'); +} + +function getCriteoEids(bidRequest) { + return getEids(bidRequest, 'criteoId', 'criteo.com', 'criteoId'); +} + +function getPubCommonEids(bidRequest) { + return getEids(bidRequest, 'pubcid', 'pubcid.org', 'pubcid'); +} + +function getEids(bidRequest, type, source, rtiPartner) { + return bidRequest .map(getUserId(type)) // bids -> userIds of a certain type .filter((x) => !!x) // filter out null userIds .map(formatEid(source, rtiPartner)); // userIds -> eid objects @@ -239,13 +312,30 @@ function _buildResponseObject(bidderRequest, bid) { dealId: dealId, currency: 'USD', ttl: 300, - tl_source: bid.tl_source + tl_source: bid.tl_source, + meta: {} }; - if (breq.mediaTypes.video) { + if (_isInstreamBidRequest(breq)) { bidResponse.vastXml = bid.ad; bidResponse.mediaType = 'video'; }; + + if (bid.advertiser_name) { + bidResponse.meta.advertiserName = bid.advertiser_name; + } + + if (bid.adomain && bid.adomain.length) { + bidResponse.meta.advertiserDomains = bid.adomain; + } + + if (bid.tl_source && bid.tl_source == 'hdx') { + bidResponse.meta.mediaType = 'banner'; + } + + if (bid.tl_source && bid.tl_source == 'tlx') { + bidResponse.meta.mediaType = 'native'; + } }; return bidResponse; } diff --git a/modules/truereachBidAdapter.js b/modules/truereachBidAdapter.js index 2de7edbc04d..92f9cc9951e 100755 --- a/modules/truereachBidAdapter.js +++ b/modules/truereachBidAdapter.js @@ -79,6 +79,26 @@ export const spec = { return bidResponses; }, + getUserSyncs: function(syncOptions, serverResponses, gdprConsent, uspConsent) { + const syncs = [] + + var gdprParams = ''; + if (gdprConsent) { + if (typeof gdprConsent.gdprApplies === 'boolean') { + gdprParams = `?gdpr=${Number(gdprConsent.gdprApplies)}&gdpr_consent=${gdprConsent.consentString}`; + } else { + gdprParams = `?gdpr_consent=${gdprConsent.consentString}`; + } + } + + if (syncOptions.iframeEnabled) { + syncs.push({ + type: 'iframe', + url: 'http://ads.momagic.com/jsp/usersync.jsp' + gdprParams + }); + } + return syncs; + } }; diff --git a/modules/trustxBidAdapter.js b/modules/trustxBidAdapter.js index f412fa21a05..9f8de30a0d4 100644 --- a/modules/trustxBidAdapter.js +++ b/modules/trustxBidAdapter.js @@ -2,9 +2,12 @@ import * as utils from '../src/utils.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; import { Renderer } from '../src/Renderer.js'; import { VIDEO, BANNER } from '../src/mediaTypes.js'; +import { config } from '../src/config.js'; const BIDDER_CODE = 'trustx'; const ENDPOINT_URL = 'https://sofia.trustx.org/hb'; +const NEW_ENDPOINT_URL = 'https://grid.bidswitch.net/hbjson?sp=trustx'; +const ADDITIONAL_SYNC_URL = 'https://x.bidswitch.net/sync?ssp=themediagrid'; const TIME_TO_LIVE = 360; const ADAPTER_SYNC_URL = 'https://sofia.trustx.org/push_sync'; const RENDERER_URL = 'https://acdn.adnxs.com/video/outstream/ANOutstreamVideo.js'; @@ -40,98 +43,30 @@ export const spec = { * @return ServerRequest Info describing the request to the server. */ buildRequests: function(validBidRequests, bidderRequest) { - const auids = []; - const bidsMap = {}; - const slotsMapByUid = {}; - const sizeMap = {}; const bids = validBidRequests || []; - let priceType = 'net'; - let pageKeywords; - let reqId; - + const newFormatBids = []; + const oldFormatBids = []; + const requests = []; bids.forEach(bid => { - if (bid.params.priceType === 'gross') { - priceType = 'gross'; - } - reqId = bid.bidderRequestId; - const {params: {uid}, adUnitCode} = bid; - auids.push(uid); - const sizesId = utils.parseSizesInput(bid.sizes); - - if (!pageKeywords && !utils.isEmpty(bid.params.keywords)) { - const keywords = utils.transformBidderParamKeywords(bid.params.keywords); - - if (keywords.length > 0) { - keywords.forEach(deleteValues); - } - pageKeywords = keywords; - } - - if (!slotsMapByUid[uid]) { - slotsMapByUid[uid] = {}; - } - const slotsMap = slotsMapByUid[uid]; - if (!slotsMap[adUnitCode]) { - slotsMap[adUnitCode] = {adUnitCode, bids: [bid], parents: []}; + if (bid.params.useNewFormat) { + newFormatBids.push(bid); } else { - slotsMap[adUnitCode].bids.push(bid); + oldFormatBids.push(bid); } - const slot = slotsMap[adUnitCode]; - - sizesId.forEach((sizeId) => { - sizeMap[sizeId] = true; - if (!bidsMap[uid]) { - bidsMap[uid] = {}; - } - - if (!bidsMap[uid][sizeId]) { - bidsMap[uid][sizeId] = [slot]; - } else { - bidsMap[uid][sizeId].push(slot); - } - slot.parents.push({parent: bidsMap[uid], key: sizeId, uid}); - }); }); - - const payload = { - pt: priceType, - auids: auids.join(','), - sizes: utils.getKeys(sizeMap).join(','), - r: reqId, - wrapperType: 'Prebid_js', - wrapperVersion: '$prebid.version$' - }; - - if (pageKeywords) { - payload.keywords = JSON.stringify(pageKeywords); - } - - if (bidderRequest) { - if (bidderRequest.refererInfo && bidderRequest.refererInfo.referer) { - payload.u = bidderRequest.refererInfo.referer; + if (newFormatBids.length) { + const newFormatRequests = newFormatRequest(newFormatBids, bidderRequest); + if (newFormatRequests) { + requests.push(newFormatRequests); } - if (bidderRequest.timeout) { - payload.wtimeout = bidderRequest.timeout; - } - if (bidderRequest.gdprConsent) { - if (bidderRequest.gdprConsent.consentString) { - payload.gdpr_consent = bidderRequest.gdprConsent.consentString; - } - payload.gdpr_applies = - (typeof bidderRequest.gdprConsent.gdprApplies === 'boolean') - ? Number(bidderRequest.gdprConsent.gdprApplies) : 1; - } - if (bidderRequest.uspConsent) { - payload.us_privacy = bidderRequest.uspConsent; + } + if (oldFormatBids.length) { + const oldFormatRequests = oldFormatRequest(oldFormatBids, bidderRequest); + if (oldFormatRequests) { + requests.push(oldFormatRequests); } } - - return { - method: 'GET', - url: ENDPOINT_URL, - data: utils.parseQueryStringParameters(payload).replace(/\&$/, ''), - bidsMap: bidsMap, - }; + return requests; }, /** * Unpack the response from the server into a list of bids. @@ -143,8 +78,6 @@ export const spec = { interpretResponse: function(serverResponse, bidRequest, RendererConst = Renderer) { serverResponse = serverResponse && serverResponse.body; const bidResponses = []; - const bidsMap = bidRequest.bidsMap; - const priceType = bidRequest.data.pt; let errorMessage; @@ -155,18 +88,38 @@ export const spec = { if (!errorMessage && serverResponse.seatbid) { serverResponse.seatbid.forEach(respItem => { - _addBidResponse(_getBidFromResponse(respItem), bidsMap, priceType, bidResponses, RendererConst); + _addBidResponse(_getBidFromResponse(respItem), bidRequest, bidResponses, RendererConst); }); } if (errorMessage) utils.logError(errorMessage); return bidResponses; }, - getUserSyncs: function(syncOptions) { + getUserSyncs: function(syncOptions, responses, gdprConsent, uspConsent) { if (syncOptions.pixelEnabled) { - return [{ + const syncsPerBidder = config.getConfig('userSync.syncsPerBidder'); + let params = []; + if (gdprConsent && typeof gdprConsent.consentString === 'string') { + if (typeof gdprConsent.gdprApplies === 'boolean') { + params.push(`gdpr=${Number(gdprConsent.gdprApplies)}&gdpr_consent=${gdprConsent.consentString}`); + } else { + params.push(`gdpr_consent=${gdprConsent.consentString}`); + } + } + if (uspConsent) { + params.push(`us_privacy=${uspConsent}`); + } + const stringParams = params.join('&'); + const syncs = [{ type: 'image', - url: ADAPTER_SYNC_URL + url: ADAPTER_SYNC_URL + (stringParams ? `?${stringParams}` : '') }]; + if (syncsPerBidder > 1) { + syncs.push({ + type: 'image', + url: ADDITIONAL_SYNC_URL + (stringParams ? `&${stringParams}` : '') + }); + } + return syncs; } } } @@ -192,67 +145,75 @@ function _getBidFromResponse(respItem) { return respItem && respItem.bid && respItem.bid[0]; } -function _addBidResponse(serverBid, bidsMap, priceType, bidResponses, RendererConst) { +function _addBidResponse(serverBid, bidRequest, bidResponses, RendererConst) { if (!serverBid) return; let errorMessage; if (!serverBid.auid) errorMessage = LOG_ERROR_MESS.noAuid + JSON.stringify(serverBid); if (!serverBid.adm) errorMessage = LOG_ERROR_MESS.noAdm + JSON.stringify(serverBid); else { - const awaitingBids = bidsMap[serverBid.auid]; - if (awaitingBids) { - const sizeId = `${serverBid.w}x${serverBid.h}`; - if (awaitingBids[sizeId]) { - const slot = awaitingBids[sizeId][0]; - - const bid = slot.bids.shift(); - const bidResponse = { - requestId: bid.bidId, // bid.bidderRequestId, - bidderCode: spec.code, - cpm: serverBid.price, - width: serverBid.w, - height: serverBid.h, - creativeId: serverBid.auid, // bid.bidId, - currency: 'USD', - netRevenue: priceType !== 'gross', - ttl: TIME_TO_LIVE, - dealId: serverBid.dealid - }; - if (serverBid.content_type === 'video') { - bidResponse.vastXml = serverBid.adm; - bidResponse.mediaType = VIDEO; - bidResponse.adResponse = { - content: bidResponse.vastXml - }; - if (!bid.renderer && (!bid.mediaTypes || !bid.mediaTypes.video || bid.mediaTypes.video.context === 'outstream')) { - bidResponse.renderer = createRenderer(bidResponse, { - id: bid.bidId, - url: RENDERER_URL - }, RendererConst); - } - } else { - bidResponse.ad = serverBid.adm; - bidResponse.mediaType = BANNER; + const { bidsMap, priceType, newFormat } = bidRequest; + let bid; + let slot; + if (newFormat) { + bid = bidsMap[serverBid.impid]; + } else { + const awaitingBids = bidsMap[serverBid.auid]; + if (awaitingBids) { + const sizeId = `${serverBid.w}x${serverBid.h}`; + if (awaitingBids[sizeId]) { + slot = awaitingBids[sizeId][0]; + bid = slot.bids.shift(); } + } else { + errorMessage = LOG_ERROR_MESS.noPlacementCode + serverBid.auid; + } + } - bidResponses.push(bidResponse); - - if (!slot.bids.length) { - slot.parents.forEach(({parent, key, uid}) => { - const index = parent[key].indexOf(slot); - if (index > -1) { - parent[key].splice(index, 1); - } - if (!parent[key].length) { - delete parent[key]; - if (!utils.getKeys(parent).length) { - delete bidsMap[uid]; - } - } - }); + if (!errorMessage && bid) { + const bidResponse = { + requestId: bid.bidId, // bid.bidderRequestId, + cpm: serverBid.price, + width: serverBid.w, + height: serverBid.h, + creativeId: serverBid.auid, // bid.bidId, + currency: 'USD', + netRevenue: newFormat ? false : priceType !== 'gross', + ttl: TIME_TO_LIVE, + dealId: serverBid.dealid + }; + if (serverBid.content_type === 'video') { + bidResponse.vastXml = serverBid.adm; + bidResponse.mediaType = VIDEO; + bidResponse.adResponse = { + content: bidResponse.vastXml + }; + if (!bid.renderer && (!bid.mediaTypes || !bid.mediaTypes.video || bid.mediaTypes.video.context === 'outstream')) { + bidResponse.renderer = createRenderer(bidResponse, { + id: bid.bidId, + url: RENDERER_URL + }, RendererConst); } + } else { + bidResponse.ad = serverBid.adm; + bidResponse.mediaType = BANNER; } - } else { - errorMessage = LOG_ERROR_MESS.noPlacementCode + serverBid.auid; + + bidResponses.push(bidResponse); + } + + if (slot && !slot.bids.length) { + slot.parents.forEach(({parent, key, uid}) => { + const index = parent[key].indexOf(slot); + if (index > -1) { + parent[key].splice(index, 1); + } + if (!parent[key].length) { + delete parent[key]; + if (!utils.getKeys(parent).length) { + delete bidsMap[uid]; + } + } + }); } } if (errorMessage) { @@ -285,4 +246,343 @@ function createRenderer (bid, rendererParams, RendererConst) { return rendererInst; } +function createVideoRequest(bid, mediaType) { + const {playerSize, mimes, durationRangeSec, protocols} = mediaType; + const size = (playerSize || bid.sizes || [])[0]; + if (!size) return; + + let result = utils.parseGPTSingleSizeArrayToRtbSize(size); + + if (mimes) { + result.mimes = mimes; + } + + if (durationRangeSec && durationRangeSec.length === 2) { + result.minduration = durationRangeSec[0]; + result.maxduration = durationRangeSec[1]; + } + + if (protocols && protocols.length) { + result.protocols = protocols; + } + + return result; +} + +function createBannerRequest(bid, mediaType) { + const sizes = mediaType.sizes || bid.sizes; + if (!sizes || !sizes.length) return; + + let format = sizes.map((size) => utils.parseGPTSingleSizeArrayToRtbSize(size)); + let result = utils.parseGPTSingleSizeArrayToRtbSize(sizes[0]); + + if (format.length) { + result.format = format + } + return result; +} + +/** + * Gets bidfloor + * @param {Object} mediaTypes + * @param {Object} bid + * @returns {Number} floor + */ +function _getFloor (mediaTypes, bid) { + const curMediaType = mediaTypes.video ? 'video' : 'banner'; + let floor = bid.params.bidFloor || 0; + + if (typeof bid.getFloor === 'function') { + const floorInfo = bid.getFloor({ + currency: 'USD', + mediaType: curMediaType, + size: bid.sizes.map(([w, h]) => ({w, h})) + }); + + if (typeof floorInfo === 'object' && + floorInfo.currency === 'USD' && + !isNaN(parseFloat(floorInfo.floor))) { + floor = Math.max(floor, parseFloat(floorInfo.floor)); + } + } + + return floor; +} + +function newFormatRequest(validBidRequests, bidderRequest) { + if (!validBidRequests.length) { + return null; + } + let pageKeywords = null; + let jwpseg = null; + 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 || {}; + + const referer = refererInfo ? encodeURIComponent(refererInfo.referer) : ''; + const imp = []; + const bidsMap = {}; + + validBidRequests.forEach((bid) => { + if (!bidderRequestId) { + bidderRequestId = bid.bidderRequestId; + } + if (!auctionId) { + auctionId = bid.auctionId; + } + if (!schain) { + schain = bid.schain; + } + 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)) { + pageKeywords = utils.transformBidderParamKeywords(keywords); + } + const bidFloor = _getFloor(mediaTypes || {}, bid); + const jwTargeting = rtd && rtd.jwplayer && rtd.jwplayer.targeting; + if (jwTargeting) { + if (!jwpseg && jwTargeting.segments) { + jwpseg = jwTargeting.segments; + } + if (!content && jwTargeting.content) { + content = jwTargeting.content; + } + } + let impObj = { + id: bidId, + tagid: uid.toString(), + ext: { + divid: adUnitCode + } + }; + + if (bidFloor) { + impObj.bidfloor = bidFloor; + } + + if (!mediaTypes || mediaTypes[BANNER]) { + const banner = createBannerRequest(bid, mediaTypes ? mediaTypes[BANNER] : {}); + if (banner) { + impObj.banner = banner; + } + } + if (mediaTypes && mediaTypes[VIDEO]) { + const video = createVideoRequest(bid, mediaTypes[VIDEO]); + if (video) { + impObj.video = video; + } + } + + if (impObj.banner || impObj.video) { + imp.push(impObj); + } + }); + + const source = { + tid: auctionId, + ext: { + wrapper: 'Prebid_js', + wrapper_version: '$prebid.version$' + } + }; + + if (schain) { + source.ext.schain = schain; + } + + const bidderTimeout = config.getConfig('bidderTimeout') || timeout; + const tmax = timeout ? Math.min(bidderTimeout, timeout) : bidderTimeout; + + let request = { + id: bidderRequestId, + site: { + page: referer + }, + tmax, + source, + imp + }; + + if (content) { + request.site.content = content; + } + + if (jwpseg && jwpseg.length) { + user = { + data: [{ + name: 'iow_labs_pub_data', + segment: jwpseg.map((seg) => { + return {name: 'jwpseg', value: seg}; + }) + }] + }; + } + + if (gdprConsent && gdprConsent.consentString) { + userExt = {consent: gdprConsent.consentString}; + } + + if (userIdAsEids && userIdAsEids.length) { + userExt = userExt || {}; + userExt.eids = [...userIdAsEids]; + } + + if (userExt && Object.keys(userExt).length) { + user = user || {}; + user.ext = userExt; + } + + if (user) { + request.user = user; + } + + const configKeywords = utils.transformBidderParamKeywords({ + 'user': utils.deepAccess(config.getConfig('ortb2.user'), 'keywords') || null, + 'context': utils.deepAccess(config.getConfig('ortb2.site'), 'keywords') || null + }); + + if (configKeywords.length) { + pageKeywords = (pageKeywords || []).concat(configKeywords); + } + + if (pageKeywords && pageKeywords.length > 0) { + pageKeywords.forEach(deleteValues); + } + + if (pageKeywords) { + request.ext = { + keywords: pageKeywords + }; + } + + if (gdprConsent && gdprConsent.gdprApplies) { + request.regs = { + ext: { + gdpr: gdprConsent.gdprApplies ? 1 : 0 + } + } + } + + if (uspConsent) { + if (!request.regs) { + request.regs = {ext: {}}; + } + request.regs.ext.us_privacy = uspConsent; + } + + return { + method: 'POST', + url: NEW_ENDPOINT_URL, + data: JSON.stringify(request), + newFormat: true, + bidsMap + }; +} + +function oldFormatRequest(validBidRequests, bidderRequest) { + const auids = []; + const bidsMap = {}; + const slotsMapByUid = {}; + const sizeMap = {}; + const bids = validBidRequests || []; + let priceType = 'net'; + let pageKeywords; + let reqId; + + bids.forEach(bid => { + if (bid.params.priceType === 'gross') { + priceType = 'gross'; + } + reqId = bid.bidderRequestId; + const {params: {uid}, adUnitCode} = bid; + auids.push(uid); + const sizesId = utils.parseSizesInput(bid.sizes); + + if (!pageKeywords && !utils.isEmpty(bid.params.keywords)) { + const keywords = utils.transformBidderParamKeywords(bid.params.keywords); + + if (keywords.length > 0) { + keywords.forEach(deleteValues); + } + pageKeywords = keywords; + } + + if (!slotsMapByUid[uid]) { + slotsMapByUid[uid] = {}; + } + const slotsMap = slotsMapByUid[uid]; + if (!slotsMap[adUnitCode]) { + slotsMap[adUnitCode] = {adUnitCode, bids: [bid], parents: []}; + } else { + slotsMap[adUnitCode].bids.push(bid); + } + const slot = slotsMap[adUnitCode]; + + sizesId.forEach((sizeId) => { + sizeMap[sizeId] = true; + if (!bidsMap[uid]) { + bidsMap[uid] = {}; + } + + if (!bidsMap[uid][sizeId]) { + bidsMap[uid][sizeId] = [slot]; + } else { + bidsMap[uid][sizeId].push(slot); + } + slot.parents.push({parent: bidsMap[uid], key: sizeId, uid}); + }); + }); + + const payload = { + pt: priceType, + auids: auids.join(','), + sizes: utils.getKeys(sizeMap).join(','), + r: reqId, + wrapperType: 'Prebid_js', + wrapperVersion: '$prebid.version$' + }; + + if (pageKeywords) { + payload.keywords = JSON.stringify(pageKeywords); + } + + if (bidderRequest) { + if (bidderRequest.refererInfo && bidderRequest.refererInfo.referer) { + payload.u = bidderRequest.refererInfo.referer; + } + if (bidderRequest.timeout) { + payload.wtimeout = bidderRequest.timeout; + } + if (bidderRequest.gdprConsent) { + if (bidderRequest.gdprConsent.consentString) { + payload.gdpr_consent = bidderRequest.gdprConsent.consentString; + } + payload.gdpr_applies = + (typeof bidderRequest.gdprConsent.gdprApplies === 'boolean') + ? Number(bidderRequest.gdprConsent.gdprApplies) : 1; + } + if (bidderRequest.uspConsent) { + payload.us_privacy = bidderRequest.uspConsent; + } + } + + return { + method: 'GET', + url: ENDPOINT_URL, + data: utils.parseQueryStringParameters(payload).replace(/\&$/, ''), + bidsMap, + priceType + }; +} + registerBidder(spec); diff --git a/modules/trustxBidAdapter.md b/modules/trustxBidAdapter.md index a72f1ba85aa..e891df8f161 100644 --- a/modules/trustxBidAdapter.md +++ b/modules/trustxBidAdapter.md @@ -52,6 +52,18 @@ TrustX Bid Adapter supports Banner and Video (instream and outstream). } } ] + },{ + code: 'test-div', + sizes: [[300, 250]], + bids: [ + { + bidder: "trustx", + params: { + uid: '58851', + useNewFormat: true + } + } + ] } ]; -``` \ No newline at end of file +``` diff --git a/modules/ucfunnelBidAdapter.js b/modules/ucfunnelBidAdapter.js index f9982edef36..734aba97789 100644 --- a/modules/ucfunnelBidAdapter.js +++ b/modules/ucfunnelBidAdapter.js @@ -1,12 +1,13 @@ import {registerBidder} from '../src/adapters/bidderFactory.js'; import {BANNER, VIDEO, NATIVE} from '../src/mediaTypes.js'; import { getStorageManager } from '../src/storageManager.js'; +import { config } from '../src/config.js'; import * as utils from '../src/utils.js'; const storage = getStorageManager(); const COOKIE_NAME = 'ucf_uid'; const VER = 'ADGENT_PREBID-2018011501'; const BIDDER_CODE = 'ucfunnel'; - +const GVLID = 607; const VIDEO_CONTEXT = { INSTREAM: 0, OUSTREAM: 2 @@ -14,6 +15,7 @@ const VIDEO_CONTEXT = { export const spec = { code: BIDDER_CODE, + gvlid: GVLID, ENDPOINT: 'https://hb.aralego.com/header', supportedMediaTypes: [BANNER, VIDEO, NATIVE], /** @@ -65,11 +67,12 @@ export const spec = { let bid = { requestId: bidRequest.bidId, cpm: ad.cpm || 0, - creativeId: ad.ad_id || bidRequest.params.adid, + creativeId: ad.crid || ad.ad_id || bidRequest.params.adid, dealId: ad.deal || null, - currency: 'USD', + currency: ad.currency || 'USD', netRevenue: true, - ttl: 1800 + ttl: 1800, + meta: {} }; if (bidRequest.params && bidRequest.params.bidfloor && ad.cpm && ad.cpm < bidRequest.params.bidfloor) { @@ -77,6 +80,10 @@ export const spec = { } if (ad.creative_type) { bid.mediaType = ad.creative_type; + bid.meta.mediaType = ad.creative_type; + } + if (ad.adomain) { + bid.meta.advertiserDomains = ad.adomain; } switch (ad.creative_type) { @@ -124,16 +131,19 @@ export const spec = { return [bid]; }, - getUserSyncs: function(syncOptions) { + getUserSyncs: function(syncOptions, serverResponses, gdprConsent = {}, uspConsent) { + let gdprApplies = (gdprConsent && gdprConsent.gdprApplies) ? '1' : ''; + let apiVersion = (gdprConsent) ? gdprConsent.apiVersion : ''; + let consentString = (gdprConsent) ? gdprConsent.consentString : ''; if (syncOptions.iframeEnabled) { return [{ type: 'iframe', - url: 'https://cdn.aralego.net/ucfad/cookie/sync.html' + url: 'https://cdn.aralego.net/ucfad/cookie/sync.html' + getCookieSyncParameter(gdprApplies, apiVersion, consentString, uspConsent) }]; } else if (syncOptions.pixelEnabled) { return [{ type: 'image', - url: 'https://sync.aralego.com/idSync' + url: 'https://sync.aralego.com/idSync' + getCookieSyncParameter(gdprApplies, apiVersion, consentString, uspConsent) }]; } } @@ -146,6 +156,22 @@ function transformSizes(requestSizes) { } } +function getCookieSyncParameter(gdprApplies, apiVersion, consentString, uspConsent) { + let param = '?'; + if (gdprApplies == '1') { + param = param + 'gdpr=1&'; + } + if (apiVersion == 1) { + param = param + 'euconsent=' + consentString + '&'; + } else if (apiVersion == 2) { + param = param + 'euconsent-v2=' + consentString + '&'; + } + if (uspConsent) { + param = param + 'usprivacy=' + uspConsent; + } + return (param == '?') ? '' : param; +} + function parseSizes(bid) { let params = bid.params; if (bid.mediaType === VIDEO) { @@ -202,14 +228,14 @@ function getRequestData(bid, bidderRequest) { schain: supplyChain, fp: bid.params.bidfloor }; - + addUserId(bidData, bid.userId); try { bidData.host = window.top.location.hostname; - bidData.u = window.top.location.href; + bidData.u = config.getConfig('publisherDomain') || window.top.location.href; bidData.xr = 0; } catch (e) { bidData.host = window.location.hostname; - bidData.u = document.referrer || window.location.href; + bidData.u = config.getConfig('publisherDomain') || bidderRequest.refererInfo.referrer || document.referrer || window.location.href; bidData.xr = 1; } @@ -253,11 +279,83 @@ function getRequestData(bid, bidderRequest) { } if (bidderRequest && bidderRequest.gdprConsent) { - Object.assign(bidData, { - gdpr: bidderRequest.gdprConsent.gdprApplies ? 1 : 0, - euconsent: bidderRequest.gdprConsent.consentString - }); + if (bidderRequest.gdprConsent.apiVersion == 1) { + Object.assign(bidData, { + gdpr: bidderRequest.gdprConsent.gdprApplies ? 1 : 0, + euconsent: bidderRequest.gdprConsent.consentString + }); + } else if (bidderRequest.gdprConsent.apiVersion == 2) { + Object.assign(bidData, { + gdpr: bidderRequest.gdprConsent.gdprApplies ? 1 : 0, + 'euconsent-v2': bidderRequest.gdprConsent.consentString + }); + } + } + + if (config.getConfig('coppa')) { + bidData.coppa = true; } return bidData; } + +function addUserId(bidData, userId) { + bidData['eids'] = ''; + utils._each(userId, (userIdObjectOrValue, userIdProviderKey) => { + switch (userIdProviderKey) { + case 'sharedid': + if (userIdObjectOrValue.id) { + bidData[userIdProviderKey + '_id'] = userIdObjectOrValue.id; + } + if (userIdObjectOrValue.third) { + bidData[userIdProviderKey + '_third'] = userIdObjectOrValue.third; + } + break; + case 'haloId': + if (userIdObjectOrValue.haloId) { + bidData[userIdProviderKey + 'haloId'] = userIdObjectOrValue.haloId; + } + if (userIdObjectOrValue.auSeg) { + bidData[userIdProviderKey + '_auSeg'] = userIdObjectOrValue.auSeg; + } + break; + case 'parrableId': + if (userIdObjectOrValue.eid) { + bidData[userIdProviderKey + '_eid'] = userIdObjectOrValue.eid; + } + break; + case 'id5id': + if (userIdObjectOrValue.uid) { + bidData[userIdProviderKey + '_uid'] = userIdObjectOrValue.uid; + } + if (userIdObjectOrValue.ext && userIdObjectOrValue.ext.linkType) { + bidData[userIdProviderKey + '_linkType'] = userIdObjectOrValue.ext.linkType; + } + break; + case 'uid2': + if (userIdObjectOrValue.id) { + bidData['eids'] = (bidData['eids'].length > 0) + ? (bidData['eids'] + '!' + userIdProviderKey + ',' + userIdObjectOrValue.id) + : (userIdProviderKey + ',' + userIdObjectOrValue.id); + } + break; + case 'connectid': + if (userIdObjectOrValue) { + bidData['eids'] = (bidData['eids'].length > 0) + ? (bidData['eids'] + '!verizonMediaId,' + userIdObjectOrValue) + : ('verizonMediaId,' + userIdObjectOrValue); + } + break; + case 'flocId': + if (userIdObjectOrValue.id) { + bidData['cid'] = userIdObjectOrValue.id; + } + break; + default: + bidData[userIdProviderKey] = userIdObjectOrValue; + break; + } + }); + + return bidData; +} diff --git a/modules/uid2IdSystem.js b/modules/uid2IdSystem.js new file mode 100644 index 00000000000..053b57cb76d --- /dev/null +++ b/modules/uid2IdSystem.js @@ -0,0 +1,97 @@ +/** + * This module adds uid2 ID support to the User ID module + * The {@link module:modules/userId} module is required. + * @module modules/uid2IdSystem + * @requires module:modules/userId + */ + +import * as utils from '../src/utils.js' +import {submodule} from '../src/hook.js'; +import { getStorageManager } from '../src/storageManager.js'; + +const MODULE_NAME = 'uid2'; +const GVLID = 887; +const LOG_PRE_FIX = 'UID2: '; +const ADVERTISING_COOKIE = '__uid2_advertising_token'; + +function readCookie() { + return storage.cookiesAreEnabled() ? storage.getCookie(ADVERTISING_COOKIE) : null; +} + +function readFromLocalStorage() { + return storage.localStorageIsEnabled() ? storage.getDataFromLocalStorage(ADVERTISING_COOKIE) : null; +} + +function getStorage() { + return getStorageManager(GVLID, MODULE_NAME); +} + +const storage = getStorage(); + +const logInfo = createLogInfo(LOG_PRE_FIX); + +function createLogInfo(prefix) { + return function (...strings) { + utils.logInfo(prefix + ' ', ...strings); + } +} + +/** + * Encode the id + * @param value + * @returns {string|*} + */ +function encodeId(value) { + const result = {}; + if (value) { + const bidIds = { + id: value + } + result.uid2 = bidIds; + logInfo('Decoded value ' + JSON.stringify(result)); + return result; + } + return undefined; +} + +/** @type {Submodule} */ +export const uid2IdSubmodule = { + /** + * used to link submodule with config + * @type {string} + */ + name: MODULE_NAME, + + /** + * Vendor id of Prebid + * @type {Number} + */ + gvlid: GVLID, + /** + * decode the stored id value for passing to bid requests + * @function + * @param {string} value + * @returns {{uid2:{ id: string }} or undefined if value doesn't exists + */ + decode(value) { + return (value) ? encodeId(value) : undefined; + }, + + /** + * performs action to obtain id and return a value. + * @function + * @param {SubmoduleConfig} [config] + * @param {ConsentData|undefined} consentData + * @returns {uid2Id} + */ + getId(config, consentData) { + logInfo('Creating UID 2.0'); + let value = readCookie() || readFromLocalStorage(); + logInfo('The advertising token: ' + value); + return {id: value} + }, + +}; + +// Register submodule for userId +submodule('userId', uid2IdSubmodule); diff --git a/modules/uid2IdSystem.md b/modules/uid2IdSystem.md new file mode 100644 index 00000000000..fa596b17584 --- /dev/null +++ b/modules/uid2IdSystem.md @@ -0,0 +1,24 @@ +## UID 2.0 User ID Submodule + +UID 2.0 ID Module. + +### Prebid Params + +Individual params may be set for the UID 2.0 Submodule. At least one identifier must be set in the params. + +``` +pbjs.setConfig({ + userSync: { + userIds: [{ + name: 'uid2' + }] + } +}); +``` +## Parameter Descriptions for the `usersync` Configuration Section +The below parameters apply only to the UID 2.0 User ID Module integration. + +| Param under userSync.userIds[] | Scope | Type | Description | Example | +| --- | --- | --- | --- | --- | +| name | Required | String | ID value for the UID20 module - `"uid2"` | `"uid2"` | +| value | Optional | Object | Used only if the page has a separate mechanism for storing the UID 2.0 ID. The value is an object containing the values to be sent to the adapters. In this scenario, no URL is called and nothing is added to local storage | `{"uid2": { "id": "eb33b0cb-8d35-4722-b9c0-1a31d4064888"}}` | diff --git a/modules/undertoneBidAdapter.js b/modules/undertoneBidAdapter.js index 743cb07b21e..14a765206b6 100644 --- a/modules/undertoneBidAdapter.js +++ b/modules/undertoneBidAdapter.js @@ -158,7 +158,8 @@ export const spec = { creativeId: bidRes.adId, currency: bidRes.currency, netRevenue: bidRes.netRevenue, - ttl: bidRes.ttl || 360 + ttl: bidRes.ttl || 360, + meta: { advertiserDomains: bidRes.adomain ? bidRes.adomain : [] } }; if (bidRes.mediaType && bidRes.mediaType === 'video') { bid.vastXml = bidRes.ad; diff --git a/modules/unicornBidAdapter.js b/modules/unicornBidAdapter.js index 33e60ad2789..2d4b5b53966 100644 --- a/modules/unicornBidAdapter.js +++ b/modules/unicornBidAdapter.js @@ -8,6 +8,7 @@ const BIDDER_CODE = 'unicorn'; const UNICORN_ENDPOINT = 'https://ds.uncn.jp/pb/0/bid.json'; const UNICORN_DEFAULT_CURRENCY = 'JPY'; const UNICORN_PB_COOKIE_KEY = '__pb_unicorn_aud'; +const UNICORN_PB_VERSION = '1.0'; /** * Placement ID and Account ID are required. @@ -47,12 +48,12 @@ function buildOpenRtbBidRequestPayload(validBidRequests, bidderRequest) { bidderRequest ); const imp = validBidRequests.map(br => { - const sizes = utils.parseSizesInput(br.sizes)[0]; return { id: br.bidId, banner: { - w: sizes.split('x')[0], - h: sizes.split('x')[1] + format: makeFormat(br.sizes), + w: br.sizes[0][0], + h: br.sizes[0][1], }, tagid: utils.deepAccess(br, 'params.placementId') || br.adUnitCode, secure: 1, @@ -84,7 +85,8 @@ function buildOpenRtbBidRequestPayload(validBidRequests, bidderRequest) { source: { ext: { stype: 'prebid_uncn', - bidder: BIDDER_CODE + bidder: BIDDER_CODE, + prebid_version: UNICORN_PB_VERSION } }, ext: { @@ -138,6 +140,14 @@ const getUid = () => { } }; +/** + * Make imp.banner.format + * @param {Array} arr + */ +const makeFormat = arr => arr.map((s) => { + return { w: s[0], h: s[1] }; +}); + export const spec = { code: BIDDER_CODE, aliases: ['uncn'], diff --git a/modules/unifiedIdSystem.js b/modules/unifiedIdSystem.js index f916030d643..bc033f37992 100644 --- a/modules/unifiedIdSystem.js +++ b/modules/unifiedIdSystem.js @@ -18,6 +18,10 @@ export const unifiedIdSubmodule = { * @type {string} */ name: MODULE_NAME, + /** + * required for the gdpr enforcement module + */ + gvlid: 21, /** * decode the stored id value for passing to bid requests * @function @@ -30,10 +34,11 @@ export const unifiedIdSubmodule = { /** * performs action to obtain id and return a value in the callback's response argument * @function - * @param {SubmoduleParams} [configParams] + * @param {SubmoduleConfig} [config] * @returns {IdResponse|undefined} */ - getId(configParams) { + getId(config) { + const configParams = (config && config.params) || {}; if (!configParams || (typeof configParams.partner !== 'string' && typeof configParams.url !== 'string')) { utils.logError('User ID - unifiedId submodule requires either partner or url to be defined'); return; diff --git a/modules/unrulyBidAdapter.js b/modules/unrulyBidAdapter.js index 15fe0fefe8b..36a156d5c66 100644 --- a/modules/unrulyBidAdapter.js +++ b/modules/unrulyBidAdapter.js @@ -31,6 +31,7 @@ const serverResponseToBid = (bid, rendererInstance) => ({ netRevenue: true, creativeId: bid.bidId, ttl: 360, + meta: { advertiserDomains: bid && bid.adomain ? bid.adomain : [] }, currency: 'USD', renderer: rendererInstance, mediaType: VIDEO diff --git a/modules/userId/eids.js b/modules/userId/eids.js index 72be98eca50..4012079ab6e 100644 --- a/modules/userId/eids.js +++ b/modules/userId/eids.js @@ -30,8 +30,16 @@ const USER_IDS_CONFIG = { // id5Id 'id5id': { + getValue: function(data) { + return data.uid + }, source: 'id5-sync.com', - atype: 1 + atype: 1, + getUidExt: function(data) { + if (data.ext) { + return data.ext; + } + } }, // parrableId @@ -63,7 +71,7 @@ const USER_IDS_CONFIG = { // identityLink 'idl_env': { source: 'liveramp.com', - atype: 1 + atype: 3 }, // liveIntentId @@ -72,7 +80,7 @@ const USER_IDS_CONFIG = { return data.lipbid; }, source: 'liveintent.com', - atype: 1, + atype: 3, getEidExt: function(data) { if (Array.isArray(data.segments) && data.segments.length) { return { @@ -85,7 +93,13 @@ const USER_IDS_CONFIG = { // britepoolId 'britepoolid': { source: 'britepool.com', - atype: 1 + atype: 3 + }, + + // dmdId + 'dmdId': { + source: 'hcn.health', + atype: 3 }, // lotamePanoramaId @@ -100,11 +114,26 @@ const USER_IDS_CONFIG = { atype: 1 }, + // merkleId + 'merkleId': { + source: 'merkleinc.com', + atype: 3, + getValue: function(data) { + return data.id; + }, + getUidExt: function(data) { + return (data && data.keyID) ? { + keyID: data.keyID + } : undefined; + } + }, + // NetId 'netId': { source: 'netid.de', atype: 1 }, + // sharedid 'sharedid': { source: 'sharedid.org', @@ -117,6 +146,81 @@ const USER_IDS_CONFIG = { third: data.third } : undefined; } + }, + + // zeotapIdPlus + 'IDP': { + source: 'zeotap.com', + atype: 1 + }, + + // haloId + 'haloId': { + source: 'audigent.com', + atype: 1 + }, + + // quantcastId + 'quantcastId': { + source: 'quantcast.com', + atype: 1 + }, + + // nextroll + 'nextrollId': { + source: 'nextroll.com', + atype: 1 + }, + + // IDx + 'idx': { + source: 'idx.lat', + atype: 1 + }, + + // Verizon Media ConnectID + 'connectid': { + source: 'verizonmedia.com', + atype: 3 + }, + + // Neustar Fabrick + 'fabrickId': { + source: 'neustar.biz', + atype: 1 + }, + // MediaWallah OpenLink + 'mwOpenLinkId': { + source: 'mediawallahscript.com', + atype: 1 + }, + 'tapadId': { + source: 'tapad.com', + atype: 1 + }, + // Novatiq Snowflake + 'novatiq': { + getValue: function(data) { + return data.snowflake + }, + source: 'novatiq.com', + atype: 1 + }, + 'uid2': { + source: 'uidapi.com', + atype: 3, + getValue: function(data) { + return data.id; + } + }, + 'deepintentId': { + source: 'deepintent.com', + atype: 3 + }, + // Admixer Id + 'admixerId': { + source: 'admixer.net', + atype: 3 } }; @@ -157,11 +261,37 @@ export function createEidsArray(bidRequestUserId) { let eids = []; for (const subModuleKey in bidRequestUserId) { if (bidRequestUserId.hasOwnProperty(subModuleKey)) { - const eid = createEidObject(bidRequestUserId[subModuleKey], subModuleKey); - if (eid) { - eids.push(eid); + if (subModuleKey === 'pubProvidedId') { + eids = eids.concat(bidRequestUserId['pubProvidedId']); + } else { + const eid = createEidObject(bidRequestUserId[subModuleKey], subModuleKey); + if (eid) { + eids.push(eid); + } } } } return eids; } + +/** + * @param {SubmoduleContainer[]} submodules + */ +export function buildEidPermissions(submodules) { + let eidPermissions = []; + submodules.filter(i => utils.isPlainObject(i.idObj) && Object.keys(i.idObj).length) + .forEach(i => { + Object.keys(i.idObj).forEach(key => { + if (utils.deepAccess(i, 'config.bidders') && Array.isArray(i.config.bidders) && + utils.deepAccess(USER_IDS_CONFIG, key + '.source')) { + eidPermissions.push( + { + source: USER_IDS_CONFIG[key].source, + bidders: i.config.bidders + } + ); + } + }); + }); + return eidPermissions; +} diff --git a/modules/userId/eids.md b/modules/userId/eids.md index f42b7e61a52..32aa6b3c0d9 100644 --- a/modules/userId/eids.md +++ b/modules/userId/eids.md @@ -1,7 +1,8 @@ ## Example of eids array generated by UserId Module. + ``` userIdAsEids = [ - { + { source: 'pubcid.org', uids: [{ id: 'some-random-id-value', @@ -21,13 +22,25 @@ userIdAsEids = [ }, { - source: 'id5-sync.com', + source: 'neustar.biz', uids: [{ id: 'some-random-id-value', atype: 1 }] }, + { + source: 'id5-sync.com', + uids: [{ + id: 'some-random-id-value', + atype: 1, + ext: { + linkType: 2, + abTestingControlGroup: false + } + }] + }, + { source: 'parrable.com', uids: [{ @@ -55,6 +68,14 @@ userIdAsEids = [ } }, + { + source: 'merkleinc.com', + uids: [{ + id: 'some-random-id-value', + atype: 1 + }] + }, + { source: 'britepool.com', uids: [{ @@ -63,6 +84,14 @@ userIdAsEids = [ }] }, + { + source: 'hcn.health', + uids: [{ + id: 'some-random-id-value', + atype: 3 + }] + }, + { source: 'criteo.com', uids: [{ @@ -78,15 +107,98 @@ userIdAsEids = [ atype: 1 }] }, + { source: 'sharedid.org', uids: [{ id: 'some-random-id-value', atype: 1, - ext: { + ext: { third: 'some-random-id-value' } }] + }, + + { + source: 'zeotap.com', + uids: [{ + id: 'some-random-id-value', + atype: 1 + }] + }, + + { + source: 'nextroll.com', + uids: [{ + id: 'some-random-id-value', + atype: 1 + }] + }, + + { + source: 'audigent.com', + uids: [{ + id: 'some-random-id-value', + atype: 1 + }] + }, + + { + source: 'quantcast.com', + uids: [{ + id: 'some-random-id-value', + atype: 1 + }] + }, + + { + source: 'verizonmedia.com', + uids: [{ + id: 'some-random-id-value', + atype: 1 + }] + }, + { + source: 'mediawallahscript.com', + uids: [{ + id: 'some-random-id-value', + atype: 1 + }] + }, + { + source: 'tapad.com', + uids: [{ + id: 'some-random-id-value', + atype: 1 + }] + }, + { + source: 'novatiq.com', + uids: [{ + id: 'some-random-id-value', + atype: 1 + }] + }, + { + source: 'uidapi.com', + uids: [{ + id: 'some-random-id-value', + atype: 3 + }] + }, + { + source: 'admixer.net', + uids: [{ + id: 'some-random-id-value', + atype: 3 + }] + }, + { + source: 'deepintent.com', + uids: [{ + id: 'some-random-id-value', + atype: 3 + }] } ] ``` diff --git a/modules/userId/index.js b/modules/userId/index.js index afdd93a57ba..199934f4cdb 100644 --- a/modules/userId/index.js +++ b/modules/userId/index.js @@ -14,7 +14,7 @@ * If IdResponse#callback is defined, then it'll called at the end of auction. * It's permissible to return neither, one, or both fields. * @name Submodule#getId - * @param {SubmoduleParams} configParams + * @param {SubmoduleConfig} config * @param {ConsentData|undefined} consentData * @param {(Object|undefined)} cacheIdObj * @return {(IdResponse|undefined)} A response object that contains id and/or callback. @@ -27,7 +27,8 @@ * If IdResponse#callback is defined, then it'll called at the end of auction. * It's permissible to return neither, one, or both fields. * @name Submodule#extendId - * @param {SubmoduleParams} configParams + * @param {SubmoduleConfig} config + * @param {ConsentData|undefined} consentData * @param {Object} storedId - existing id, if any * @return {(IdResponse|function(callback:function))} A response object that contains id and/or callback. */ @@ -37,7 +38,7 @@ * @summary decode a stored value for passing to bid requests * @name Submodule#decode * @param {Object|string} value - * @param {SubmoduleParams|undefined} configParams + * @param {SubmoduleConfig|undefined} config * @return {(Object|undefined)} */ @@ -48,6 +49,20 @@ * @type {string} */ +/** + * @property + * @summary use a predefined domain override for cookies or provide your own + * @name Submodule#domainOverride + * @type {(undefined|function)} + */ + +/** + * @function + * @summary Returns the root domain + * @name Submodule#findRootDomain + * @returns {string} + */ + /** * @typedef {Object} SubmoduleConfig * @property {string} name - the User ID submodule name (used to link submodule with config) @@ -83,8 +98,10 @@ * @property {(string|undefined)} publisherId - the unique identifier of the publisher in question * @property {(string|undefined)} ajaxTimeout - the number of milliseconds a resolution request can take before automatically being terminated * @property {(array|undefined)} identifiersToResolve - the identifiers from either ls|cookie to be attached to the getId query - * @property {(string|undefined)} providedIdentifierName - defines the name of an identifier that can be found in local storage or in the cookie jar that can be sent along with the getId request. This parameter should be used whenever a customer is able to provide the most stable identifier possible * @property {(LiveIntentCollectConfig|undefined)} liCollectConfig - the config for LiveIntent's collect requests + * @property {(string|undefined)} pd - publisher provided data for reconciling ID5 IDs + * @property {(string|undefined)} emailHash - if provided, the hashed email address of a user + * @property {(string|undefined)} notUse3P - use to retrieve envelope from 3p endpoint */ /** @@ -108,16 +125,23 @@ * @property {(function|undefined)} callback - function that will return an id */ +/** + * @typedef {Object} RefreshUserIdsOptions + * @property {(string[]|undefined)} submoduleNames - submodules to refresh + */ + import find from 'core-js-pure/features/array/find.js'; -import {config} from '../../src/config.js'; +import { config } from '../../src/config.js'; import events from '../../src/events.js'; import * as utils from '../../src/utils.js'; -import {getGlobal} from '../../src/prebidGlobal.js'; -import {gdprDataHandler} from '../../src/adapterManager.js'; +import { getGlobal } from '../../src/prebidGlobal.js'; +import { gdprDataHandler } from '../../src/adapterManager.js'; import CONSTANTS from '../../src/constants.json'; -import {module, hook} from '../../src/hook.js'; -import {createEidsArray} from './eids.js'; +import { module, hook } from '../../src/hook.js'; +import { createEidsArray, buildEidPermissions } from './eids.js'; import { getCoreStorageManager } from '../../src/storageManager.js'; +import {getPrebidInternal} from '../../src/utils.js'; +import includes from 'core-js-pure/features/array/includes.js'; const MODULE_NAME = 'User ID'; const COOKIE = 'cookie'; @@ -128,6 +152,7 @@ const CONSENT_DATA_COOKIE_STORAGE_CONFIG = { name: '_pbjs_userid_consent_data', expires: 30 // 30 days expiration, which should match how often consent is refreshed by CMPs }; +export const PBJS_USER_ID_OPTOUT_NAME = '_pbjs_id_optout'; export const coreStorage = getCoreStorageManager('userid'); /** @type {string[]} */ @@ -193,6 +218,14 @@ export function setStoredValue(submodule, value) { } } +function setPrebidServerEidPermissions(initializedSubmodules) { + let setEidPermissions = getPrebidInternal().setEidPermissions; + if (typeof setEidPermissions === 'function' && utils.isArray(initializedSubmodules)) { + setEidPermissions(buildEidPermissions(initializedSubmodules)); + } +} + +/** /** * @param {SubmoduleStorage} storage * @param {String|undefined} key optional key of the value @@ -216,7 +249,7 @@ function getStoredValue(storage, key = undefined) { } } // support storing a string or a stringified object - if (typeof storedValue === 'string' && storedValue.charAt(0) === '{') { + if (typeof storedValue === 'string' && storedValue.trim().charAt(0) === '{') { storedValue = JSON.parse(storedValue); } } catch (e) { @@ -232,7 +265,7 @@ function getStoredValue(storage, key = undefined) { * @param consentData * @returns {{apiVersion: number, gdprApplies: boolean, consentString: string}} */ -function makeStoredConsentDataObject(consentData) { +function makeStoredConsentDataHash(consentData) { const storedConsentData = { consentString: '', gdprApplies: false, @@ -244,8 +277,7 @@ function makeStoredConsentDataObject(consentData) { storedConsentData.gdprApplies = consentData.gdprApplies; storedConsentData.apiVersion = consentData.apiVersion; } - - return storedConsentData; + return utils.cyrb53Hash(JSON.stringify(storedConsentData)); } /** @@ -255,7 +287,7 @@ function makeStoredConsentDataObject(consentData) { export function setStoredConsentData(consentData) { try { const expiresStr = (new Date(Date.now() + (CONSENT_DATA_COOKIE_STORAGE_CONFIG.expires * (60 * 60 * 24 * 1000)))).toUTCString(); - coreStorage.setCookie(CONSENT_DATA_COOKIE_STORAGE_CONFIG.name, JSON.stringify(makeStoredConsentDataObject(consentData)), expiresStr, 'Lax'); + coreStorage.setCookie(CONSENT_DATA_COOKIE_STORAGE_CONFIG.name, makeStoredConsentDataHash(consentData), expiresStr, 'Lax'); } catch (error) { utils.logError(error); } @@ -266,13 +298,11 @@ export function setStoredConsentData(consentData) { * @returns {string} */ function getStoredConsentData() { - let storedValue; try { - storedValue = JSON.parse(coreStorage.getCookie(CONSENT_DATA_COOKIE_STORAGE_CONFIG.name)); + return coreStorage.getCookie(CONSENT_DATA_COOKIE_STORAGE_CONFIG.name); } catch (e) { utils.logError(e); } - return storedValue; } /** @@ -287,7 +317,7 @@ function storedConsentDataMatchesConsentData(storedConsentData, consentData) { return ( typeof storedConsentData === 'undefined' || storedConsentData === null || - utils.deepEqual(storedConsentData, makeStoredConsentDataObject(consentData)) + storedConsentData === makeStoredConsentDataHash(consentData) ); } @@ -311,13 +341,73 @@ function hasGDPRConsent(consentData) { return true; } +/** + * Find the root domain + * @param {string|undefined} fullDomain + * @return {string} + */ +export function findRootDomain(fullDomain = window.location.hostname) { + if (!coreStorage.cookiesAreEnabled()) { + return fullDomain; + } + + const domainParts = fullDomain.split('.'); + if (domainParts.length == 2) { + return fullDomain; + } + let rootDomain; + let continueSearching; + let startIndex = -2; + const TEST_COOKIE_NAME = `_rdc${Date.now()}`; + const TEST_COOKIE_VALUE = 'writeable'; + do { + rootDomain = domainParts.slice(startIndex).join('.'); + let expirationDate = new Date(utils.timestamp() + 10 * 1000).toUTCString(); + + // Write a test cookie + coreStorage.setCookie( + TEST_COOKIE_NAME, + TEST_COOKIE_VALUE, + expirationDate, + 'Lax', + rootDomain, + undefined + ); + + // See if the write was successful + const value = coreStorage.getCookie(TEST_COOKIE_NAME, undefined); + if (value === TEST_COOKIE_VALUE) { + continueSearching = false; + // Delete our test cookie + coreStorage.setCookie( + TEST_COOKIE_NAME, + '', + 'Thu, 01 Jan 1970 00:00:01 GMT', + undefined, + rootDomain, + undefined + ); + } else { + startIndex += -1; + continueSearching = Math.abs(startIndex) <= domainParts.length; + } + } while (continueSearching); + return rootDomain; +} + /** * @param {SubmoduleContainer[]} submodules * @param {function} cb - callback for after processing is done. */ function processSubmoduleCallbacks(submodules, cb) { - const done = cb ? utils.delayExecution(cb, submodules.length) : function() { }; - submodules.forEach(function(submodule) { + let done = () => {}; + if (cb) { + done = utils.delayExecution(() => { + clearTimeout(timeoutID); + cb(); + }, submodules.length); + } + submodules.forEach(function (submodule) { submodule.callback(function callbackCompleted(idObj) { // if valid, id data should be saved to cookie/html storage if (idObj) { @@ -325,7 +415,7 @@ function processSubmoduleCallbacks(submodules, cb) { setStoredValue(submodule, idObj); } // cache decoded value (this is copied to every adUnit bid) - submodule.idObj = submodule.submodule.decode(idObj); + submodule.idObj = submodule.submodule.decode(idObj, submodule.config); } else { utils.logInfo(`${MODULE_NAME}: ${submodule.submodule.name} - request id responded with an empty value`); } @@ -335,7 +425,6 @@ function processSubmoduleCallbacks(submodules, cb) { // clear callback, this prop is used to test if all submodule callbacks are complete below submodule.callback = undefined; }); - clearTimeout(timeoutID); } /** @@ -356,6 +445,26 @@ function getCombinedSubmoduleIds(submodules) { return combinedSubmoduleIds; } +/** + * This function will create a combined object for bidder with allowed subModule Ids + * @param {SubmoduleContainer[]} submodules + * @param {string} bidder + */ +function getCombinedSubmoduleIdsForBidder(submodules, bidder) { + if (!Array.isArray(submodules) || !submodules.length || !bidder) { + return {}; + } + return submodules + .filter(i => !i.config.bidders || !utils.isArray(i.config.bidders) || includes(i.config.bidders, bidder)) + .filter(i => utils.isPlainObject(i.idObj) && Object.keys(i.idObj).length) + .reduce((carry, i) => { + Object.keys(i.idObj).forEach(key => { + carry[key] = i.idObj[key]; + }); + return carry; + }, {}); +} + /** * @param {AdUnit[]} adUnits * @param {SubmoduleContainer[]} submodules @@ -364,17 +473,18 @@ function addIdDataToAdUnitBids(adUnits, submodules) { if ([adUnits].some(i => !Array.isArray(i) || !i.length)) { return; } - const combinedSubmoduleIds = getCombinedSubmoduleIds(submodules); - const combinedSubmoduleIdsAsEids = createEidsArray(combinedSubmoduleIds); - if (Object.keys(combinedSubmoduleIds).length) { - adUnits.forEach(adUnit => { + adUnits.forEach(adUnit => { + if (adUnit.bids && utils.isArray(adUnit.bids)) { adUnit.bids.forEach(bid => { - // create a User ID object on the bid, - bid.userId = combinedSubmoduleIds; - bid.userIdAsEids = combinedSubmoduleIdsAsEids; + const combinedSubmoduleIds = getCombinedSubmoduleIdsForBidder(submodules, bid.bidder); + if (Object.keys(combinedSubmoduleIds).length) { + // create a User ID object on the bid, + bid.userId = combinedSubmoduleIds; + bid.userIdAsEids = createEidsArray(combinedSubmoduleIds); + } }); - }); - } + } + }); } /** @@ -387,6 +497,7 @@ function initializeSubmodulesAndExecuteCallbacks(continueAuction) { if (typeof initializedSubmodules === 'undefined') { initializedSubmodules = initSubmodules(submodules, gdprDataHandler.getConsentData()); if (initializedSubmodules.length) { + setPrebidServerEidPermissions(initializedSubmodules); // list of submodules that have callbacks that need to be executed const submodulesWithCallbacks = initializedSubmodules.filter(item => utils.isFn(item.callback)); @@ -395,7 +506,7 @@ function initializeSubmodulesAndExecuteCallbacks(continueAuction) { // delay auction until ids are available delayed = true; let continued = false; - const continueCallback = function() { + const continueCallback = function () { if (!continued) { continued = true; continueAuction(); @@ -412,7 +523,7 @@ function initializeSubmodulesAndExecuteCallbacks(continueAuction) { // when syncDelay is zero, process callbacks now, otherwise delay process with a setTimeout if (syncDelay > 0) { - setTimeout(function() { + setTimeout(function () { processSubmoduleCallbacks(submodulesWithCallbacks); }, syncDelay); } else { @@ -440,7 +551,7 @@ function initializeSubmodulesAndExecuteCallbacks(continueAuction) { */ export function requestBidsHook(fn, reqBidsConfigObj) { // initialize submodules only when undefined - initializeSubmodulesAndExecuteCallbacks(function() { + initializeSubmodulesAndExecuteCallbacks(function () { // pass available user id data to bid adapters addIdDataToAdUnitBids(reqBidsConfigObj.adUnits || getGlobal().adUnits, initializedSubmodules); // calling fn allows prebid to continue processing @@ -468,79 +579,132 @@ function getUserIdsAsEids() { return createEidsArray(getCombinedSubmoduleIds(initializedSubmodules)); } +/** +* This function will be exposed in the global-name-space so that userIds can be refreshed after initialization. +* @param {RefreshUserIdsOptions} options +*/ +function refreshUserIds(options, callback) { + let submoduleNames = options ? options.submoduleNames : null; + if (!submoduleNames) { + submoduleNames = []; + } + + initializeSubmodulesAndExecuteCallbacks(function() { + let consentData = gdprDataHandler.getConsentData() + + // gdpr consent with purpose one is required, otherwise exit immediately + let {userIdModules, hasValidated} = validateGdprEnforcement(submodules, consentData); + if (!hasValidated && !hasGDPRConsent(consentData)) { + utils.logWarn(`${MODULE_NAME} - gdpr permission not valid for local storage or cookies, exit module`); + return; + } + + // we always want the latest consentData stored, even if we don't execute any submodules + const storedConsentData = getStoredConsentData(); + setStoredConsentData(consentData); + + let callbackSubmodules = []; + for (let submodule of userIdModules) { + if (submoduleNames.length > 0 && + submoduleNames.indexOf(submodule.submodule.name) === -1) { + continue; + } + + utils.logInfo(`${MODULE_NAME} - refreshing ${submodule.submodule.name}`); + populateSubmoduleId(submodule, consentData, storedConsentData, true); + + if (utils.isFn(submodule.callback)) { + callbackSubmodules.push(submodule); + } + } + + if (callbackSubmodules.length > 0) { + processSubmoduleCallbacks(callbackSubmodules); + } + + if (callback) { + callback(); + } + }); +} + /** * This hook returns updated list of submodules which are allowed to do get user id based on TCF 2 enforcement rules configured */ export const validateGdprEnforcement = hook('sync', function (submodules, consentData) { - return {userIdModules: submodules, hasValidated: consentData && consentData.hasValidated}; + return { userIdModules: submodules, hasValidated: consentData && consentData.hasValidated }; }, 'validateGdprEnforcement'); +function populateSubmoduleId(submodule, consentData, storedConsentData, forceRefresh) { + // There are two submodule configuration types to handle: storage or value + // 1. storage: retrieve user id data from cookie/html storage or with the submodule's getId method + // 2. value: pass directly to bids + if (submodule.config.storage) { + let storedId = getStoredValue(submodule.config.storage); + let response; + + let refreshNeeded = false; + if (typeof submodule.config.storage.refreshInSeconds === 'number') { + const storedDate = new Date(getStoredValue(submodule.config.storage, 'last')); + refreshNeeded = storedDate && (Date.now() - storedDate.getTime() > submodule.config.storage.refreshInSeconds * 1000); + } + + if (!storedId || refreshNeeded || forceRefresh || !storedConsentDataMatchesConsentData(storedConsentData, consentData)) { + // No id previously saved, or a refresh is needed, or consent has changed. Request a new id from the submodule. + response = submodule.submodule.getId(submodule.config, consentData, storedId); + } else if (typeof submodule.submodule.extendId === 'function') { + // If the id exists already, give submodule a chance to decide additional actions that need to be taken + response = submodule.submodule.extendId(submodule.config, consentData, storedId); + } + + if (utils.isPlainObject(response)) { + if (response.id) { + // A getId/extendId result assumed to be valid user id data, which should be saved to users local storage or cookies + setStoredValue(submodule, response.id); + storedId = response.id; + } + + if (typeof response.callback === 'function') { + // Save async callback to be invoked after auction + submodule.callback = response.callback; + } + } + + if (storedId) { + // cache decoded value (this is copied to every adUnit bid) + submodule.idObj = submodule.submodule.decode(storedId, submodule.config); + } + } else if (submodule.config.value) { + // cache decoded value (this is copied to every adUnit bid) + submodule.idObj = submodule.config.value; + } else { + const response = submodule.submodule.getId(submodule.config, consentData, undefined); + if (utils.isPlainObject(response)) { + if (typeof response.callback === 'function') { submodule.callback = response.callback; } + if (response.id) { submodule.idObj = submodule.submodule.decode(response.id, submodule.config); } + } + } +} + /** * @param {SubmoduleContainer[]} submodules * @param {ConsentData} consentData * @returns {SubmoduleContainer[]} initialized submodules */ function initSubmodules(submodules, consentData) { - // we always want the latest consentData stored, even if we don't execute any submodules - const storedConsentData = getStoredConsentData(); - setStoredConsentData(consentData); - // gdpr consent with purpose one is required, otherwise exit immediately - let {userIdModules, hasValidated} = validateGdprEnforcement(submodules, consentData); + let { userIdModules, hasValidated } = validateGdprEnforcement(submodules, consentData); if (!hasValidated && !hasGDPRConsent(consentData)) { utils.logWarn(`${MODULE_NAME} - gdpr permission not valid for local storage or cookies, exit module`); return []; } - return userIdModules.reduce((carry, submodule) => { - // There are two submodule configuration types to handle: storage or value - // 1. storage: retrieve user id data from cookie/html storage or with the submodule's getId method - // 2. value: pass directly to bids - if (submodule.config.storage) { - let storedId = getStoredValue(submodule.config.storage); - let response; - - let refreshNeeded = false; - if (typeof submodule.config.storage.refreshInSeconds === 'number') { - const storedDate = new Date(getStoredValue(submodule.config.storage, 'last')); - refreshNeeded = storedDate && (Date.now() - storedDate.getTime() > submodule.config.storage.refreshInSeconds * 1000); - } - - if (!storedId || refreshNeeded || !storedConsentDataMatchesConsentData(storedConsentData, consentData)) { - // No id previously saved, or a refresh is needed, or consent has changed. Request a new id from the submodule. - response = submodule.submodule.getId(submodule.config.params, consentData, storedId); - } else if (typeof submodule.submodule.extendId === 'function') { - // If the id exists already, give submodule a chance to decide additional actions that need to be taken - response = submodule.submodule.extendId(submodule.config.params, storedId); - } - - if (utils.isPlainObject(response)) { - if (response.id) { - // A getId/extendId result assumed to be valid user id data, which should be saved to users local storage or cookies - setStoredValue(submodule, response.id); - storedId = response.id; - } - - if (typeof response.callback === 'function') { - // Save async callback to be invoked after auction - submodule.callback = response.callback; - } - } + // we always want the latest consentData stored, even if we don't execute any submodules + const storedConsentData = getStoredConsentData(); + setStoredConsentData(consentData); - if (storedId) { - // cache decoded value (this is copied to every adUnit bid) - submodule.idObj = submodule.submodule.decode(storedId, submodule.config.params); - } - } else if (submodule.config.value) { - // cache decoded value (this is copied to every adUnit bid) - submodule.idObj = submodule.config.value; - } else { - const response = submodule.submodule.getId(submodule.config.params, consentData, undefined); - if (utils.isPlainObject(response)) { - if (typeof response.callback === 'function') { submodule.callback = response.callback; } - if (response.id) { submodule.idObj = submodule.submodule.decode(response.id, submodule.config.params); } - } - } + return userIdModules.reduce((carry, submodule) => { + populateSubmoduleId(submodule, consentData, storedConsentData, false); carry.push(submodule); return carry; }, []); @@ -593,7 +757,9 @@ function updateSubmodules() { // find submodule and the matching configuration, if found create and append a SubmoduleContainer submodules = addedSubmodules.map(i => { - const submoduleConfig = find(configs, j => j.name === i.name); + const submoduleConfig = find(configs, j => j.name && j.name.toLowerCase() === i.name.toLowerCase()); + if (submoduleConfig && i.name !== submoduleConfig.name) submoduleConfig.name = i.name; + i.findRootDomain = findRootDomain; return submoduleConfig ? { submodule: i, config: submoduleConfig, @@ -605,7 +771,7 @@ function updateSubmodules() { if (!addedUserIdHook && submodules.length) { // priority value 40 will load after consentManagement with a priority of 50 getGlobal().requestBids.before(requestBidsHook, 40); - utils.logInfo(`${MODULE_NAME} - usersync config updated for ${submodules.length} submodules`); + utils.logInfo(`${MODULE_NAME} - usersync config updated for ${submodules.length} submodules: `, submodules.map(a => a.submodule.name)); addedUserIdHook = true; } } @@ -639,15 +805,15 @@ export function init(config) { ].filter(i => i !== null); // exit immediately if opt out cookie or local storage keys exists. - if (validStorageTypes.indexOf(COOKIE) !== -1 && (coreStorage.getCookie('_pbjs_id_optout') || coreStorage.getCookie('_pubcid_optout'))) { + if (validStorageTypes.indexOf(COOKIE) !== -1 && coreStorage.getCookie(PBJS_USER_ID_OPTOUT_NAME)) { utils.logInfo(`${MODULE_NAME} - opt-out cookie found, exit module`); return; } - // _pubcid_optout is checked for compatibility with pubCommonId - if (validStorageTypes.indexOf(LOCAL_STORAGE) !== -1 && (coreStorage.getDataFromLocalStorage('_pbjs_id_optout') || coreStorage.getDataFromLocalStorage('_pubcid_optout'))) { + if (validStorageTypes.indexOf(LOCAL_STORAGE) !== -1 && coreStorage.getDataFromLocalStorage(PBJS_USER_ID_OPTOUT_NAME)) { utils.logInfo(`${MODULE_NAME} - opt-out localStorage found, exit module`); return; } + // listen for config userSyncs to be set config.getConfig(conf => { // Note: support for 'usersync' was dropped as part of Prebid.js 4.0 @@ -663,6 +829,7 @@ export function init(config) { // exposing getUserIds function in global-name-space so that userIds stored in Prebid can be used by external codes. (getGlobal()).getUserIds = getUserIds; (getGlobal()).getUserIdsAsEids = getUserIdsAsEids; + (getGlobal()).refreshUserIds = refreshUserIds; } // init config update listener to start the application diff --git a/modules/userId/userId.md b/modules/userId/userId.md index a47ecd9f08c..5d78a447572 100644 --- a/modules/userId/userId.md +++ b/modules/userId/userId.md @@ -11,6 +11,16 @@ pbjs.setConfig({ name: "_pubcid", expires: 60 } + }, { + name: 'dmdId', + storage: { + name: 'dmd-dgid', + type: 'cookie', + expires: 30 + }, + params: { + api_key: '3fdbe297-3690-4f5c-9e11-ee9186a6d77c', // provided by DMD + } }, { name: "unifiedId", params: { @@ -26,12 +36,12 @@ pbjs.setConfig({ name: "id5Id", params: { partner: 173, // Set your real ID5 partner ID here for production, please ask for one at https://id5.io/universal-id - pd: "some-pd-string" // See https://wiki.id5.io/display/PD/Prebid.js+UserId+Module for details + pd: "some-pd-string" // See https://wiki.id5.io/x/BIAZ for details }, storage: { - type: "cookie", - name: "id5id.1st", - expires: 90, // Expiration of cookies in days + type: "html5", // ID5 requires html5 + name: "id5id", + expires: 90, // Expiration in days refreshInSeconds: 8*3600 // User Id cache lifetime in seconds, defaulting to 'expires' }, }, { @@ -43,7 +53,8 @@ pbjs.setConfig({ }, { name: 'identityLink', params: { - pid: '999' // Set your real identityLink placement ID here + pid: '999', // Set your real identityLink placement ID here + // notUse3P: true // true/false - If you do not want to use 3P endpoint to retrieve envelope. If you do not set this property to true, 3p endpoint will be fired. By default this property is undefined and 3p request will be fired. }, storage: { type: 'cookie', @@ -53,7 +64,7 @@ pbjs.setConfig({ }, { name: 'liveIntentId', params: { - publisherId: '7798696' // Set an identifier of a publisher know to your systems + publisherId: '7798696' // Set an identifier of a publisher know to your systems }, storage: { type: 'cookie', @@ -70,6 +81,53 @@ pbjs.setConfig({ name: 'sharedid', expires: 28 } + }, { + name: 'criteo', + storage: { // It is best not to specify this parameter since the module needs to be called as many times as possible + type: 'cookie', + name: '_criteoId', + expires: 1 + } + }, { + name: 'mwOpenLinkId', + params: { + accountId: 0000, + partnerId: 0000, + uid: '12345xyz' + } + },{ + name: "merkleId", + params: { + vendor:'sdfg', + sv_cid:'dfg', + sv_pubid:'xcv', + sv_domain:'zxv' + }, + storage: { + type: "cookie", + name: "merkleId", + expires: 30 + } + },{ + name: 'uid2' + } + }, { + name: 'admixerId', + params: { + pid: "4D393FAC-B6BB-4E19-8396-0A4813607316", // example id + e: "3d400b57e069c993babea0bd9efa79e5dc698e16c042686569faae20391fd7ea", // example hashed email (sha256) + p: "05de6c07eb3ea4bce45adca4e0182e771d80fbb99e12401416ca84ddf94c3eb9" //example hashed phone (sha256) + }, + storage: { + type: 'cookie', + name: '__adm__admixer', + expires: 30 + } + },{ + name: 'flocId', + params: { + token: "Registered token or default sharedid.org token" // Default sharedid.org token: "A3dHTSoNUMjjERBLlrvJSelNnwWUCwVQhZ5tNQ+sll7y+LkPPVZXtB77u2y7CweRIxiYaGwGXNlW1/dFp8VMEgIAAAB+eyJvcmlnaW4iOiJodHRwczovL3NoYXJlZGlkLm9yZzo0NDMiLCJmZWF0dXJlIjoiSW50ZXJlc3RDb2hvcnRBUEkiLCJleHBpcnkiOjE2MjYyMjA3OTksImlzU3ViZG9tYWluIjp0cnVlLCJpc1RoaXJkUGFydHkiOnRydWV9" + } }], syncDelay: 5000, auctionDelay: 1000 @@ -102,7 +160,8 @@ pbjs.setConfig({ }, { name: 'identityLink', params: { - pid: '999' // Set your real identityLink placement ID here + pid: '999', // Set your real identityLink placement ID here + // notUse3P: true // true/false - If you do not want to use 3P endpoint to retrieve envelope. If you do not set this property to true, 3p endpoint will be fired. By default this property is undefined and 3p request will be fired. }, storage: { type: 'html5', @@ -110,25 +169,93 @@ pbjs.setConfig({ expires: 30 } }, { - name: 'liveIntentId', - params: { - publisherId: '7798696' // Set an identifier of a publisher know to your systems - }, - storage: { - type: 'html5', - name: '_li_pbid', - expires: 60 - } + name: 'liveIntentId', + params: { + publisherId: '7798696' // Set an identifier of a publisher know to your systems + }, + storage: { + type: 'html5', + name: '_li_pbid', + expires: 60 + } }, { - name: 'sharedId', + name: 'sharedId', params: { syncTime: 60 // in seconds, default is 24 hours }, storage: { - type: 'cookie', + type: 'html5', name: 'sharedid', expires: 28 } + }, { + name: 'id5Id', + params: { + partner: 173, // Set your real ID5 partner ID here for production, please ask for one at https://id5.io/universal-id + pd: 'some-pd-string' // See https://wiki.id5.io/x/BIAZ for details + }, + storage: { + type: 'html5', + name: 'id5id', + expires: 90, // Expiration in days + refreshInSeconds: 8*3600 // User Id cache lifetime in seconds, defaulting to 'expires' + }, + }, { + name: 'nextrollId', + params: { + partnerId: "1009", // Set your real NextRoll partner ID here for production + } + }, { + name: 'criteo', + storage: { // It is best not to specify this parameter since the module needs to be called as many times as possible + type: 'html5', + name: '_criteoId', + expires: 1 + } + },{ + name: "merkleId", + params: { + vendor:'sdfg', + sv_cid:'dfg', + sv_pubid:'xcv', + sv_domain:'zxv' + }, + storage: { + type: "html5", + name: "merkleId", + expires: 30 + } + }, { + name: 'admixerId', + params: { + pid: "4D393FAC-B6BB-4E19-8396-0A4813607316", // example id + e: "3d400b57e069c993babea0bd9efa79e5dc698e16c042686569faae20391fd7ea", // example hashed email (sha256) + p: "05de6c07eb3ea4bce45adca4e0182e771d80fbb99e12401416ca84ddf94c3eb9" //example hashed phone (sha256) + }, + storage: { + type: 'html5', + name: 'admixerId', + expires: 30 + } + },{ + name: 'flocId', + params: { + token: "Registered token or default sharedid.org token" // Default sharedid.org token: "A3dHTSoNUMjjERBLlrvJSelNnwWUCwVQhZ5tNQ+sll7y+LkPPVZXtB77u2y7CweRIxiYaGwGXNlW1/dFp8VMEgIAAAB+eyJvcmlnaW4iOiJodHRwczovL3NoYXJlZGlkLm9yZzo0NDMiLCJmZWF0dXJlIjoiSW50ZXJlc3RDb2hvcnRBUEkiLCJleHBpcnkiOjE2MjYyMjA3OTksImlzU3ViZG9tYWluIjp0cnVlLCJpc1RoaXJkUGFydHkiOnRydWV9" + } + },{ + name: "deepintentId", + storage: { + type: "html5", + name: "_dpes_id", + expires: 90 + } + },{ + name: "deepintentId", + storage: { + type: "cookie", + name: "_dpes_id", + expires: 90 + } }], syncDelay: 5000 } @@ -152,6 +279,14 @@ pbjs.setConfig({ { name: "netId", value: { "netId": "fH5A3n2O8_CZZyPoJVD-eabc6ECb7jhxCicsds7qSg" } + }, + { + name: "criteo", + value: { "criteoId": "wK-fkF8zaEIlMkZMbHl3eFo4NEtoNmZaeXJtYkFjZlVuWjBhcjJMaTRYd3pZNSUyQnlKRHNGRXlpdzdjd3pjVzhjcSUyQmY4eTFzN3VSZjV1ZyUyRlA0U2ZiR0UwN2I4bDZRJTNEJTNE" } + }, + { + name: "novatiq", + value: { "snowflake": "81b001ec-8914-488c-a96e-8c220d4ee08895ef" } }], syncDelay: 5000 } diff --git a/modules/userIdTargeting.js b/modules/userIdTargeting.js index 3ed8b2a14b5..e15c9ddaca2 100644 --- a/modules/userIdTargeting.js +++ b/modules/userIdTargeting.js @@ -20,14 +20,16 @@ export function userIdTargeting(userIds, config) { if (!SHARE_WITH_GAM) { logInfo(MODULE_NAME + ': Not enabled for ' + GAM); - } - - if (window.googletag && isFn(window.googletag.pubads) && hasOwn(window.googletag.pubads(), 'setTargeting') && isFn(window.googletag.pubads().setTargeting)) { + } else if (window.googletag && isFn(window.googletag.pubads) && hasOwn(window.googletag.pubads(), 'setTargeting') && isFn(window.googletag.pubads().setTargeting)) { GAM_API = window.googletag.pubads().setTargeting; } else { - SHARE_WITH_GAM = false; - logInfo(MODULE_NAME + ': Could not find googletag.pubads().setTargeting API. Not adding User Ids in targeting.') - return; + window.googletag = window.googletag || {}; + window.googletag.cmd = window.googletag.cmd || []; + GAM_API = function (key, value) { + window.googletag.cmd.push(function () { + window.googletag.pubads().setTargeting(key, value); + }); + }; } Object.keys(userIds).forEach(function(key) { diff --git a/modules/validationFpdModule/config.js b/modules/validationFpdModule/config.js new file mode 100644 index 00000000000..f6adfea70eb --- /dev/null +++ b/modules/validationFpdModule/config.js @@ -0,0 +1,125 @@ +/** + * Data type map + */ +const TYPES = { + string: 'string', + object: 'object', + number: 'number', +}; + +/** + * Template to define what ortb2 attributes should be validated + * Accepted fields: + * -- invalid - {Boolean} if true, field is not valid + * -- type - {String} valid data type of field + * -- isArray - {Boolean} if true, field must be an array + * -- childType - {String} used in conjuction with isArray: true, defines valid type of array indices + * -- children - {Object} defines child properties needed to be validated (used only if type: object) + * -- required - {Array} array of strings defining any required properties for object (used only if type: object) + * -- optoutApplies - {Boolean} if true, optout logic will filter if applicable (currently only applies to user object) + */ +export const ORTB_MAP = { + imp: { + invalid: true + }, + cur: { + type: TYPES.object, + isArray: true, + childType: TYPES.string + }, + device: { + type: TYPES.object, + children: { + w: { type: TYPES.number }, + h: { type: TYPES.number } + } + }, + site: { + type: TYPES.object, + children: { + name: { type: TYPES.string }, + domain: { type: TYPES.string }, + page: { type: TYPES.string }, + ref: { type: TYPES.string }, + keywords: { type: TYPES.string }, + search: { type: TYPES.string }, + cat: { + type: TYPES.object, + isArray: true, + childType: TYPES.string + }, + sectioncat: { + type: TYPES.object, + isArray: true, + childType: TYPES.string + }, + pagecat: { + type: TYPES.object, + isArray: true, + childType: TYPES.string + }, + content: { + type: TYPES.object, + isArray: false, + children: { + data: { + type: TYPES.object, + isArray: true, + childType: TYPES.object, + required: ['name', 'segment'], + children: { + segment: { + type: TYPES.object, + isArray: true, + childType: TYPES.object, + required: ['id'], + children: { + id: { type: TYPES.string } + } + }, + name: { type: TYPES.string }, + ext: { type: TYPES.object }, + } + } + } + }, + publisher: { + type: TYPES.object, + isArray: false + }, + } + }, + user: { + type: TYPES.object, + children: { + yob: { + type: TYPES.number, + optoutApplies: true + }, + gender: { + type: TYPES.string, + optoutApplies: true + }, + keywords: { type: TYPES.string }, + data: { + type: TYPES.object, + isArray: true, + childType: TYPES.object, + required: ['name', 'segment'], + children: { + segment: { + type: TYPES.object, + isArray: true, + childType: TYPES.object, + required: ['id'], + children: { + id: { type: TYPES.string } + } + }, + name: { type: TYPES.string }, + ext: { type: TYPES.object }, + } + } + } + } +} diff --git a/modules/validationFpdModule/index.js b/modules/validationFpdModule/index.js new file mode 100644 index 00000000000..c23f7e09316 --- /dev/null +++ b/modules/validationFpdModule/index.js @@ -0,0 +1,232 @@ +/** + * This module sets default values and validates ortb2 first part data + * @module modules/firstPartyData + */ +import { config } from '../../src/config.js'; +import * as utils from '../../src/utils.js'; +import { ORTB_MAP } from './config.js'; +import { submodule } from '../../src/hook.js'; +import { getStorageManager } from '../../src/storageManager.js'; + +const STORAGE = getStorageManager(); +let optout; + +/** + * Check if data passed is empty + * @param {*} value to test against + * @returns {Boolean} is value empty + */ +function isEmptyData(data) { + let check = true; + + if (typeof data === 'object' && !utils.isEmpty(data)) { + check = false; + } else if (typeof data !== 'object' && (utils.isNumber(data) || data)) { + check = false; + } + + return check; +} + +/** + * Check if required keys exist in data object + * @param {Object} data object + * @param {Array} array of required keys + * @param {String} object path (for printing warning) + * @param {Number} index of object value in the data array (for printing warning) + * @returns {Boolean} is requirements fulfilled + */ +function getRequiredData(obj, required, parent, i) { + let check = true; + + required.forEach(key => { + if (!obj[key] || isEmptyData(obj[key])) { + check = false; + utils.logWarn(`Filtered ${parent}[] value at index ${i} in ortb2 data: missing required property ${key}`); + } + }); + + return check; +} + +/** + * Check if data type is valid + * @param {*} value to test against + * @param {Object} object containing type definition and if should be array bool + * @returns {Boolean} is type fulfilled + */ +function typeValidation(data, mapping) { + let check = false; + + switch (mapping.type) { + case 'string': + if (typeof data === 'string') check = true; + break; + case 'number': + if (typeof data === 'number' && isFinite(data)) check = true; + break; + case 'object': + if (typeof data === 'object') { + if ((Array.isArray(data) && mapping.isArray) || (!Array.isArray(data) && !mapping.isArray)) check = true; + } + break; + } + + return check; +} + +/** + * Validates ortb2 data arrays and filters out invalid data + * @param {Array} ortb2 data array + * @param {Object} object defining child type and if array + * @param {String} config path of data array + * @param {String} parent path for logging warnings + * @returns {Array} validated/filtered data + */ +export function filterArrayData(arr, child, path, parent) { + arr = arr.filter((index, i) => { + let check = typeValidation(index, {type: child.type, isArray: child.isArray}); + + if (check && Array.isArray(index) === Boolean(child.isArray)) { + return true; + } + + utils.logWarn(`Filtered ${parent}[] value at index ${i} in ortb2 data: expected type ${child.type}`); + }).filter((index, i) => { + let requiredCheck = true; + let mapping = utils.deepAccess(ORTB_MAP, path); + + if (mapping && mapping.required) requiredCheck = getRequiredData(index, mapping.required, parent, i); + + if (requiredCheck) return true; + }).reduce((result, value, i) => { + let typeBool = false; + let mapping = utils.deepAccess(ORTB_MAP, path); + + switch (child.type) { + case 'string': + result.push(value); + break; + case 'object': + if (mapping && mapping.children) { + let validObject = validateFpd(value, path + '.children.', parent + '.'); + if (Object.keys(validObject).length) { + let requiredCheck = getRequiredData(validObject, mapping.required, parent, i); + + if (requiredCheck) { + result.push(validObject); + typeBool = true; + } + } + } else { + result.push(value); + typeBool = true; + } + break; + } + + if (!typeBool) utils.logWarn(`Filtered ${parent}[] value at index ${i} in ortb2 data: expected type ${child.type}`); + + return result; + }, []); + + return arr; +} + +/** + * Validates ortb2 object and filters out invalid data + * @param {Object} ortb2 object + * @param {String} config path of data array + * @param {String} parent path for logging warnings + * @returns {Object} validated/filtered data + */ +export function validateFpd(fpd, path = '', parent = '') { + if (!fpd) return {}; + + // Filter out imp property if exists + let validObject = Object.assign({}, Object.keys(fpd).filter(key => { + let mapping = utils.deepAccess(ORTB_MAP, path + key); + + if (!mapping || !mapping.invalid) return key; + + utils.logWarn(`Filtered ${parent}${key} property in ortb2 data: invalid property`); + }).filter(key => { + let mapping = utils.deepAccess(ORTB_MAP, path + key); + // let typeBool = false; + let typeBool = (mapping) ? typeValidation(fpd[key], {type: mapping.type, isArray: mapping.isArray}) : true; + + if (typeBool || !mapping) return key; + + utils.logWarn(`Filtered ${parent}${key} property in ortb2 data: expected type ${(mapping.isArray) ? 'array' : mapping.type}`); + }).reduce((result, key) => { + let mapping = utils.deepAccess(ORTB_MAP, path + key); + let modified = {}; + + if (mapping) { + if (mapping.optoutApplies && optout) { + utils.logWarn(`Filtered ${parent}${key} data: pubcid optout found`); + return result; + } + + modified = (mapping.type === 'object' && !mapping.isArray) + ? validateFpd(fpd[key], path + key + '.children.', parent + key + '.') + : (mapping.isArray && mapping.childType) + ? filterArrayData(fpd[key], { type: mapping.childType, isArray: mapping.childisArray }, path + key, parent + key) : fpd[key]; + + // Check if modified data has data and return + (!isEmptyData(modified)) ? result[key] = modified + : utils.logWarn(`Filtered ${parent}${key} property in ortb2 data: empty data found`); + } else { + result[key] = fpd[key]; + } + + return result; + }, {})); + + // Return validated data + return validObject; +} + +/** + * Run validation on global and bidder config data for ortb2 + */ +function runValidations(data) { + let conf = validateFpd(data); + + let bidderDuplicate = { ...config.getBidderConfig() }; + + Object.keys(bidderDuplicate).forEach(bidder => { + let modConf = Object.keys(bidderDuplicate[bidder]).reduce((res, key) => { + let valid = (key !== 'ortb2') ? bidderDuplicate[bidder][key] : validateFpd(bidderDuplicate[bidder][key]); + + if (valid) res[key] = valid; + + return res; + }, {}); + + if (Object.keys(modConf).length) config.setBidderConfig({ bidders: [bidder], config: modConf }); + }); + + return conf; +} + +/** + * Sets default values to ortb2 if exists and adds currency and ortb2 setConfig callbacks on init + */ +export function initSubmodule(fpdConf, data) { + // Checks for existsnece of pubcid optout cookie/storage + // if exists, filters user data out + optout = (STORAGE.cookiesAreEnabled() && STORAGE.getCookie('_pubcid_optout')) || + (STORAGE.hasLocalStorage() && STORAGE.getDataFromLocalStorage('_pubcid_optout')); + + return (!fpdConf.skipValidations) ? runValidations(data) : data; +} + +/** @type {firstPartyDataSubmodule} */ +export const validationSubmodule = { + name: 'validation', + queue: 1, + init: initSubmodule +} + +submodule('firstPartyData', validationSubmodule) diff --git a/modules/vdoaiBidAdapter.js b/modules/vdoaiBidAdapter.js index 395953fb737..92d665887a5 100644 --- a/modules/vdoaiBidAdapter.js +++ b/modules/vdoaiBidAdapter.js @@ -1,14 +1,14 @@ import * as utils from '../src/utils.js'; import {config} from '../src/config.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; -import {BANNER} from '../src/mediaTypes.js'; +import {BANNER, VIDEO} from '../src/mediaTypes.js'; -const BIDDER_CODE = 'vdo.ai'; +const BIDDER_CODE = 'vdoai'; const ENDPOINT_URL = 'https://prebid.vdo.ai/auction'; export const spec = { code: BIDDER_CODE, - supportedMediaTypes: [BANNER], + supportedMediaTypes: [BANNER, VIDEO], /** * Determines whether or not the given bid request is valid. * @@ -30,17 +30,16 @@ export const spec = { if (validBidRequests.length === 0) { return []; } + return validBidRequests.map(bidRequest => { - const sizes = utils.parseSizesInput(bidRequest.params.size || bidRequest.sizes)[0]; - const width = sizes.split('x')[0]; - const height = sizes.split('x')[1]; + const sizes = utils.getAdUnitSizes(bidRequest); const payload = { placementId: bidRequest.params.placementId, - width: width, - height: height, + sizes: sizes, bidId: bidRequest.bidId, referer: bidderRequest.refererInfo.referer, - id: bidRequest.auctionId + id: bidRequest.auctionId, + mediaType: bidRequest.mediaTypes.video ? 'video' : 'banner' }; bidRequest.params.bidFloor && (payload['bidFloor'] = bidRequest.params.bidFloor); return { @@ -63,9 +62,9 @@ export const spec = { const response = serverResponse.body; const creativeId = response.adid || 0; // const width = response.w || 0; - const width = bidRequest.data.width; + const width = response.width; // const height = response.h || 0; - const height = bidRequest.data.height; + const height = response.height; const cpm = response.price || 0; response.rWidth = width; @@ -90,8 +89,15 @@ export const spec = { ttl: config.getConfig('_bidderTimeout'), // referrer: referrer, // ad: response.adm - ad: adCreative + // ad: adCreative, + mediaType: response.mediaType }; + + if (response.mediaType == 'video') { + bidResponse.vastXml = adCreative; + } else { + bidResponse.ad = adCreative; + } bidResponses.push(bidResponse); } return bidResponses; diff --git a/modules/vdoaiBidAdapter.md b/modules/vdoaiBidAdapter.md index 81bd8e69c1d..04200cc9be0 100644 --- a/modules/vdoaiBidAdapter.md +++ b/modules/vdoaiBidAdapter.md @@ -22,13 +22,36 @@ Module that connects to VDO.AI's demand sources }, bids: [ { - bidder: "vdo.ai", + bidder: "vdoai", params: { - placement: 'newsdv77', + placementId: 'newsdv77', bidFloor: 0.01 // Optional } } ] } ]; +``` + + +# Video Test Parameters +``` +var videoAdUnit = { + code: 'test-div', + sizes: [[640, 480]], + mediaTypes: { + video: { + playerSize: [[640, 480]], + context: 'instream' + }, + }, + bids: [ + { + bidder: "vdoai", + params: { + placementId: 'newsdv77' + } + } + ] +}; ``` \ No newline at end of file diff --git a/modules/verizonMediaIdSystem.js b/modules/verizonMediaIdSystem.js new file mode 100644 index 00000000000..ca395087d2d --- /dev/null +++ b/modules/verizonMediaIdSystem.js @@ -0,0 +1,105 @@ +/** + * This module adds verizonMediaId to the User ID module + * The {@link module:modules/userId} module is required + * @module modules/verizonMediaIdSystem + * @requires module:modules/userId + */ + +import {ajax} from '../src/ajax.js'; +import {submodule} from '../src/hook.js'; +import * as utils from '../src/utils.js'; +import includes from 'core-js-pure/features/array/includes.js'; + +const MODULE_NAME = 'verizonMediaId'; +const VENDOR_ID = 25; +const PLACEHOLDER = '__PIXEL_ID__'; +const VMCID_ENDPOINT = `https://ups.analytics.yahoo.com/ups/${PLACEHOLDER}/fed`; + +function isEUConsentRequired(consentData) { + return !!(consentData && consentData.gdpr && consentData.gdpr.gdprApplies); +} + +/** @type {Submodule} */ +export const verizonMediaIdSubmodule = { + /** + * used to link submodule with config + * @type {string} + */ + name: MODULE_NAME, + /** + * Vendor id of Verizon Media EMEA Limited + * @type {Number} + */ + gvlid: VENDOR_ID, + /** + * decode the stored id value for passing to bid requests + * @function + * @returns {{connectid: string} | undefined} + */ + decode(value) { + return (typeof value === 'object' && (value.connectid || value.vmuid)) + ? {connectid: value.connectid || value.vmuid} : undefined; + }, + /** + * Gets the Verizon Media Connect ID + * @function + * @param {SubmoduleConfig} [config] + * @param {ConsentData} [consentData] + * @returns {IdResponse|undefined} + */ + getId(config, consentData) { + const params = config.params || {}; + if (!params || typeof params.he !== 'string' || + (typeof params.pixelId === 'undefined' && typeof params.endpoint === 'undefined')) { + utils.logError('The verizonMediaId submodule requires the \'he\' and \'pixelId\' parameters to be defined.'); + return; + } + + const data = { + '1p': includes([1, '1', true], params['1p']) ? '1' : '0', + he: params.he, + gdpr: isEUConsentRequired(consentData) ? '1' : '0', + gdpr_consent: isEUConsentRequired(consentData) ? consentData.gdpr.consentString : '', + us_privacy: consentData && consentData.uspConsent ? consentData.uspConsent : '' + }; + + if (params.pixelId) { + data.pixelId = params.pixelId + } + + const resp = function (callback) { + const callbacks = { + success: response => { + let responseObj; + if (response) { + try { + responseObj = JSON.parse(response); + } catch (error) { + utils.logError(error); + } + } + callback(responseObj); + }, + error: error => { + utils.logError(`${MODULE_NAME}: ID fetch encountered an error`, error); + callback(); + } + }; + const endpoint = VMCID_ENDPOINT.replace(PLACEHOLDER, params.pixelId); + let url = `${params.endpoint || endpoint}?${utils.formatQS(data)}`; + verizonMediaIdSubmodule.getAjaxFn()(url, callbacks, null, {method: 'GET', withCredentials: true}); + }; + return {callback: resp}; + }, + + /** + * Return the function used to perform XHR calls. + * Utilised for each of testing. + * @returns {Function} + */ + getAjaxFn() { + return ajax; + } +}; + +submodule('userId', verizonMediaIdSubmodule); diff --git a/modules/verizonMediaSystemId.md b/modules/verizonMediaSystemId.md new file mode 100644 index 00000000000..c0d315dc754 --- /dev/null +++ b/modules/verizonMediaSystemId.md @@ -0,0 +1,33 @@ +## Verizon Media User ID Submodule + +Verizon Media User ID Module. + +### Prebid Params + +``` +pbjs.setConfig({ + userSync: { + userIds: [{ + name: 'verizonMediaId', + storage: { + name: 'vmcid', + type: 'html5', + expires: 15 + }, + params: { + pixelId: 58776, + he: '0bef996248d63cea1529cb86de31e9547a712d9f380146e98bbd39beec70355a' + } + }] + } +}); +``` +## Parameter Descriptions for the `usersync` Configuration Section +The below parameters apply only to the Verizon Media User ID Module integration. + +| Param under usersync.userIds[] | Scope | Type | Description | Example | +| --- | --- | --- | --- | --- | +| name | Required | String | ID value for the Verizon Media module - `"verizonMediaId"` | `"verizonMediaId"` | +| params | Required | Object | Data for Verizon Media ID initialization. | | +| params.pixelId | Required | Number | The Verizon Media supplied publisher specific pixel Id | `8976` | +| params.he | Required | String | The SHA-256 hashed user email address | `"529cb86de31e9547a712d9f380146e98bbd39beec"` | diff --git a/modules/vidazooBidAdapter.js b/modules/vidazooBidAdapter.js index 382833fbca9..7fc6e3a5395 100644 --- a/modules/vidazooBidAdapter.js +++ b/modules/vidazooBidAdapter.js @@ -3,7 +3,7 @@ import { registerBidder } from '../src/adapters/bidderFactory.js'; import { BANNER } from '../src/mediaTypes.js'; import { getStorageManager } from '../src/storageManager.js'; -const GLVID = 744; +const GVLID = 744; const DEFAULT_SUB_DOMAIN = 'prebid'; const BIDDER_CODE = 'vidazoo'; const BIDDER_VERSION = '1.0.0'; @@ -12,14 +12,6 @@ const TTL_SECONDS = 60 * 5; const DEAL_ID_EXPIRY = 1000 * 60 * 15; const UNIQUE_DEAL_ID_EXPIRY = 1000 * 60 * 15; const SESSION_ID_KEY = 'vidSid'; -const INTERNAL_SYNC_TYPE = { - IFRAME: 'iframe', - IMAGE: 'img' -}; -const EXTERNAL_SYNC_TYPE = { - IFRAME: 'iframe', - IMAGE: 'image' -}; export const SUPPORTED_ID_SYSTEMS = { 'britepoolid': 1, 'criteoId': 1, @@ -32,7 +24,7 @@ export const SUPPORTED_ID_SYSTEMS = { 'pubcid': 1, 'tdid': 1, }; -const storage = getStorageManager(GLVID); +const storage = getStorageManager(GVLID); export function createDomain(subDomain = DEFAULT_SUB_DOMAIN) { return `https://${subDomain}.cootlogix.com`; @@ -125,6 +117,9 @@ function appendUserIdsToRequestPayload(payloadRef, userIds) { case 'parrableId': payloadRef[key] = userId.eid; break; + case 'id5id': + payloadRef[key] = userId.uid; + break; default: payloadRef[key] = userId; } @@ -176,39 +171,24 @@ function interpretResponse(serverResponse, request) { } } -function getUserSyncs(syncOptions, responses) { +function getUserSyncs(syncOptions, responses, gdprConsent = {}, uspConsent = '') { + let syncs = []; const { iframeEnabled, pixelEnabled } = syncOptions; - + const { gdprApplies, consentString = '' } = gdprConsent; + const params = `?gdpr=${gdprApplies ? 1 : 0}&gdpr_consent=${encodeURIComponent(consentString || '')}&us_privacy=${encodeURIComponent(uspConsent || '')}` if (iframeEnabled) { - return [{ + syncs.push({ type: 'iframe', - url: 'https://static.cootlogix.com/basev/sync/user_sync.html' - }]; + url: `https://prebid.cootlogix.com/api/sync/iframe/${params}` + }); } - if (pixelEnabled) { - const lookup = {}; - const syncs = []; - responses.forEach(response => { - const { body } = response; - const results = body ? body.results || [] : []; - results.forEach(result => { - (result.cookies || []).forEach(cookie => { - if (cookie.type === INTERNAL_SYNC_TYPE.IMAGE) { - if (pixelEnabled && !lookup[cookie.src]) { - syncs.push({ - type: EXTERNAL_SYNC_TYPE.IMAGE, - url: cookie.src - }); - } - } - }); - }); + syncs.push({ + type: 'image', + url: `https://prebid.cootlogix.com/api/sync/image/${params}` }); - return syncs; } - - return []; + return syncs; } export function hashCode(s, prefix = '_') { @@ -286,6 +266,7 @@ export function tryParseJSON(value) { export const spec = { code: BIDDER_CODE, + gvlid: GVLID, version: BIDDER_VERSION, supportedMediaTypes: [BANNER], isBidRequestValid, diff --git a/modules/viewdeosDXBidAdapter.js b/modules/viewdeosDXBidAdapter.js index 52894ef2dd9..22c16ab5b34 100644 --- a/modules/viewdeosDXBidAdapter.js +++ b/modules/viewdeosDXBidAdapter.js @@ -13,6 +13,7 @@ const DISPLAY = 'display'; export const spec = { code: BIDDER_CODE, aliases: ['viewdeos'], + gvlid: 924, supportedMediaTypes: [VIDEO, BANNER], isBidRequestValid: function (bid) { return !!utils.deepAccess(bid, 'params.aid'); diff --git a/modules/visxBidAdapter.js b/modules/visxBidAdapter.js index 511e658c947..63f4121724e 100644 --- a/modules/visxBidAdapter.js +++ b/modules/visxBidAdapter.js @@ -1,11 +1,18 @@ import * as utils from '../src/utils.js'; -import {registerBidder} from '../src/adapters/bidderFactory.js'; +import { registerBidder } from '../src/adapters/bidderFactory.js'; import { config } from '../src/config.js'; +import { BANNER, VIDEO } from '../src/mediaTypes.js'; +import { INSTREAM as VIDEO_INSTREAM } from '../src/video.js'; +const { parseSizesInput, getKeys, logError, deepAccess } = utils; const BIDDER_CODE = 'visx'; -const ENDPOINT_URL = 'https://t.visx.net/hb'; +const BASE_URL = 'https://t.visx.net'; +const ENDPOINT_URL = BASE_URL + '/hb'; const TIME_TO_LIVE = 360; const DEFAULT_CUR = 'EUR'; -const ADAPTER_SYNC_URL = 'https://t.visx.net/push_sync'; +const ADAPTER_SYNC_URL = BASE_URL + '/push_sync'; +const TRACK_WIN_URL = BASE_URL + '/track/win'; +const TRACK_PENDING_URL = BASE_URL + '/track/pending'; +const TRACK_TIMEOUT_URL = BASE_URL + '/track/bid_timeout'; const LOG_ERROR_MESS = { noAuid: 'Bid from response has no auid parameter - ', noAdm: 'Bid from response has no adm parameter - ', @@ -17,12 +24,21 @@ const LOG_ERROR_MESS = { hasEmptySeatbidArray: 'Response has empty seatbid array', hasNoArrayOfBids: 'Seatbid from response has no array of bid objects - ', notAllowedCurrency: 'Currency is not supported - ', - currencyMismatch: 'Currency from the request is not match currency from the response - ' + currencyMismatch: 'Currency from the request is not match currency from the response - ', + onlyVideoInstream: `Only video ${VIDEO_INSTREAM} supported`, + videoMissing: 'Bid request videoType property is missing - ' }; const currencyWhiteList = ['EUR', 'USD', 'GBP', 'PLN']; +const RE_EMPTY_OR_ONLY_COMMAS = /^,*$/; export const spec = { code: BIDDER_CODE, + supportedMediaTypes: [BANNER, VIDEO], isBidRequestValid: function(bid) { + if (_isVideoBid(bid)) { + if (!_isValidVideoBid(bid)) { + return false; + } + } return !!bid.params.uid; }, buildRequests: function(validBidRequests, bidderRequest) { @@ -38,9 +54,10 @@ export const spec = { let reqId; let payloadSchain; let payloadUserId; + const videoTypes = _initVideoTypes(bids); if (currencyWhiteList.indexOf(currency) === -1) { - utils.logError(LOG_ERROR_MESS.notAllowedCurrency + currency); + logError(LOG_ERROR_MESS.notAllowedCurrency + currency); return; } @@ -54,7 +71,7 @@ export const spec = { if (!payloadUserId && userId) { payloadUserId = userId; } - const sizesId = utils.parseSizesInput(bid.sizes); + const sizesId = parseSizesInput(bid.sizes); if (!slotsMapByUid[uid]) { slotsMapByUid[uid] = {}; @@ -85,11 +102,12 @@ export const spec = { const payload = { pt: 'net', auids: auids.join(','), - sizes: utils.getKeys(sizeMap).join(','), + sizes: getKeys(sizeMap).join(','), r: reqId, cur: currency, wrapperType: 'Prebid_js', - wrapperVersion: '$prebid.version$' + wrapperVersion: '$prebid.version$', + ...videoTypes }; if (payloadSchain) { @@ -100,8 +118,8 @@ export const spec = { if (payloadUserId.tdid) { payload.tdid = payloadUserId.tdid; } - if (payloadUserId.id5id) { - payload.id5 = payloadUserId.id5id; + if (payloadUserId.id5id && payloadUserId.id5id.uid) { + payload.id5 = payloadUserId.id5id.uid; } if (payloadUserId.digitrustid && payloadUserId.digitrustid.data && payloadUserId.digitrustid.data.id) { payload.dtid = payloadUserId.digitrustid.data.id; @@ -151,7 +169,7 @@ export const spec = { _addBidResponse(serverBid, bidsMap, currency, bidResponses); }); } - if (errorMessage) utils.logError(errorMessage); + if (errorMessage) logError(errorMessage); return bidResponses; }, getUserSyncs: function(syncOptions, serverResponses, gdprConsent) { @@ -170,16 +188,28 @@ export const spec = { url: ADAPTER_SYNC_URL + (query.length ? '?' + query.join('&') : '') }]; } + }, + onSetTargeting: function(bid) { + // Call '/track/pending' with the corresponding bid.requestId + utils.triggerPixel(TRACK_PENDING_URL + '?requestId=' + bid.requestId); + }, + onBidWon: function(bid) { + // Call '/track/win' with the corresponding bid.requestId + utils.triggerPixel(TRACK_WIN_URL + '?requestId=' + bid.requestId); + }, + onTimeout: function(timeoutData) { + // Call '/track/bid_timeout' with timeout data + utils.triggerPixel(TRACK_TIMEOUT_URL + '?data=' + JSON.stringify(timeoutData)); } }; function _getBidFromResponse(respItem) { if (!respItem) { - utils.logError(LOG_ERROR_MESS.emptySeatbid); + logError(LOG_ERROR_MESS.emptySeatbid); } else if (!respItem.bid) { - utils.logError(LOG_ERROR_MESS.hasNoArrayOfBids + JSON.stringify(respItem)); + logError(LOG_ERROR_MESS.hasNoArrayOfBids + JSON.stringify(respItem)); } else if (!respItem.bid[0]) { - utils.logError(LOG_ERROR_MESS.noBid); + logError(LOG_ERROR_MESS.noBid); } return respItem && respItem.bid && respItem.bid[0]; } @@ -201,7 +231,7 @@ function _addBidResponse(serverBid, bidsMap, currency, bidResponses, bidsWithout const slot = awaitingBids[sizeId][0]; const bid = slot.bids.shift(); - bidResponses.push({ + const bidResponse = { requestId: bid.bidId, cpm: serverBid.price, width: serverBid.w, @@ -210,9 +240,17 @@ function _addBidResponse(serverBid, bidsMap, currency, bidResponses, bidsWithout currency: reqCurrency, netRevenue: true, ttl: TIME_TO_LIVE, - ad: serverBid.adm, dealId: serverBid.dealid - }); + }; + + if (!_isVideoBid(bid)) { + bidResponse.ad = serverBid.adm; + } else { + bidResponse.vastXml = serverBid.adm; + bidResponse.mediaType = 'video'; + } + + bidResponses.push(bidResponse); if (!slot.bids.length) { slot.parents.forEach(({parent, key, uid}) => { @@ -222,7 +260,7 @@ function _addBidResponse(serverBid, bidsMap, currency, bidResponses, bidsWithout } if (!parent[key].length) { delete parent[key]; - if (!utils.getKeys(parent).length) { + if (!getKeys(parent).length) { delete bidsMap[uid]; } } @@ -237,8 +275,88 @@ function _addBidResponse(serverBid, bidsMap, currency, bidResponses, bidsWithout } } if (errorMessage) { - utils.logError(errorMessage); + logError(errorMessage); + } +} + +function _isVideoBid(bid) { + return bid.mediaType === VIDEO || deepAccess(bid, 'mediaTypes.video'); +} + +function _isValidVideoBid(bid) { + let result = true; + const videoMediaType = deepAccess(bid, 'mediaTypes.video'); + if (videoMediaType.context !== VIDEO_INSTREAM) { + logError(LOG_ERROR_MESS.onlyVideoInstream) + result = false; + } + if (!(videoMediaType.playerSize && parseSizesInput(deepAccess(videoMediaType, 'playerSize', [])))) { + logError(LOG_ERROR_MESS.videoMissing + 'playerSize'); + result = false; + } + if (!videoMediaType.mimes) { + logError(LOG_ERROR_MESS.videoMissing + 'mimes'); + result = false; } + if (!videoMediaType.protocols) { + logError(LOG_ERROR_MESS.videoMissing + 'protocols'); + result = false; + } + return result; +} + +function _initVideoTypes(bids) { + const result = {}; + let _playerSize = []; + let _protocols = []; + let _api = []; + let _mimes = []; + let _minduration = []; + let _maxduration = []; + let _skip = []; + if (bids && bids.length) { + bids.forEach(function (bid) { + const mediaTypes = deepAccess(bid, 'mediaTypes.video', {}); + _playerSize.push(parseSizesInput(deepAccess(mediaTypes, 'playerSize', [])).join('|')); + _protocols.push(deepAccess(mediaTypes, 'protocols', []).join('|')); + _api.push(deepAccess(mediaTypes, 'api', []).join('|')); + _mimes.push(deepAccess(mediaTypes, 'mimes', []).join('|')); + _minduration.push(deepAccess(mediaTypes, 'minduration', null)); + _maxduration.push(deepAccess(mediaTypes, 'maxduration', null)); + _skip.push(deepAccess(mediaTypes, 'skip', null)); + }); + } + _playerSize = _playerSize.join(','); + _protocols = _protocols.join(','); + _api = _api.join(','); + _mimes = _mimes.join(','); + _minduration = _minduration.join(','); + _maxduration = _maxduration.join(','); + _skip = _skip.join(','); + + if (!RE_EMPTY_OR_ONLY_COMMAS.test(_playerSize)) { + result.playerSize = _playerSize; + } + if (!RE_EMPTY_OR_ONLY_COMMAS.test(_protocols)) { + result.protocols = _protocols; + } + if (!RE_EMPTY_OR_ONLY_COMMAS.test(_api)) { + result.api = _api; + } + if (!RE_EMPTY_OR_ONLY_COMMAS.test(_mimes)) { + result.mimes = _mimes; + } + if (!RE_EMPTY_OR_ONLY_COMMAS.test(_minduration)) { + result.minduration = _minduration; + } + if (!RE_EMPTY_OR_ONLY_COMMAS.test(_maxduration)) { + result.maxduration = _maxduration; + } + if (!RE_EMPTY_OR_ONLY_COMMAS.test(_skip)) { + result.skip = _skip; + } + + return result; } registerBidder(spec); diff --git a/modules/visxBidAdapter.md b/modules/visxBidAdapter.md index 41e45622481..9578f7cc4a7 100644 --- a/modules/visxBidAdapter.md +++ b/modules/visxBidAdapter.md @@ -11,33 +11,61 @@ Maintainer: service@yoc.com Module that connects to YOC VIS.X® demand source to fetch bids. # Test Parameters +```javascript +var adUnits = [ + // YOC Mystery Ad adUnit + { + code: 'yma-test-div', + mediaTypes: { + banner: { + sizes: [[1, 1]] + } + }, + bids: [ + { + bidder: 'visx', + params: { + uid: '903535' + } + } + ] + }, + // YOC Understitial Ad adUnit + { + code: 'yua-test-div', + mediaTypes: { + banner: { + sizes: [[300, 250]] + } + }, + bids: [ + { + bidder: 'visx', + params: { + uid: '903536' + } + } + ] + }, + // YOC In-stream adUnit + { + code: 'instream-test-div', + mediaTypes: { + video: { + context: 'instream', + playerSize: [400, 300], + mimes: ['video/mp4'], + protocols: [3, 6] + }, + }, + bids: [ + { + bidder: 'visx', + params: { + uid: '921068' + } + } + ] + } +]; ``` - var adUnits = [ - // YOC Mystery Ad adUnit - { - code: 'yma-test-div', - sizes: [[1, 1]], - bids: [ - { - bidder: 'visx', - params: { - uid: '903535' - } - } - ] - }, - // YOC Understitial Ad adUnit - { - code: 'yua-test-div', - sizes: [[300, 250]], - bids: [ - { - bidder: 'visx', - params: { - uid: '903536' - } - } - ] - } - ]; -``` \ No newline at end of file diff --git a/modules/voxBidAdapter.js b/modules/voxBidAdapter.js new file mode 100644 index 00000000000..73df9bb8b9b --- /dev/null +++ b/modules/voxBidAdapter.js @@ -0,0 +1,247 @@ +import * as utils from '../src/utils.js' +import { registerBidder } from '../src/adapters/bidderFactory.js' +import {BANNER, VIDEO} from '../src/mediaTypes.js' +import find from 'core-js-pure/features/array/find.js'; +import {auctionManager} from '../src/auctionManager.js'; +import {Renderer} from '../src/Renderer.js'; + +const BIDDER_CODE = 'vox'; +const SSP_ENDPOINT = 'https://ssp.hybrid.ai/auction/prebid'; +const VIDEO_RENDERER_URL = 'https://acdn.adnxs.com/video/outstream/ANOutstreamVideo.js'; +const TTL = 60; + +function buildBidRequests(validBidRequests) { + return utils._map(validBidRequests, function(validBidRequest) { + const params = validBidRequest.params; + const bidRequest = { + bidId: validBidRequest.bidId, + transactionId: validBidRequest.transactionId, + sizes: validBidRequest.sizes, + placement: params.placement, + placeId: params.placementId, + imageUrl: params.imageUrl + }; + + return bidRequest; + }) +} + +const outstreamRender = bid => { + bid.renderer.push(() => { + window.ANOutstreamVideo.renderAd({ + sizes: [bid.width, bid.height], + targetId: bid.adUnitCode, + rendererOptions: { + showBigPlayButton: false, + showProgressBar: 'bar', + showVolume: false, + allowFullscreen: true, + skippable: false, + content: bid.vastXml + } + }); + }); +} + +const createRenderer = (bid) => { + const renderer = Renderer.install({ + targetId: bid.adUnitCode, + url: VIDEO_RENDERER_URL, + loaded: false + }); + + try { + renderer.setRender(outstreamRender); + } catch (err) { + utils.logWarn('Prebid Error calling setRender on renderer', err); + } + + return renderer; +} + +function buildBid(bidData) { + const bid = { + requestId: bidData.bidId, + cpm: bidData.price, + width: bidData.content.width, + height: bidData.content.height, + creativeId: bidData.content.seanceId || bidData.bidId, + currency: bidData.currency, + netRevenue: true, + mediaType: BANNER, + ttl: TTL, + content: bidData.content + }; + + if (bidData.placement === 'video') { + bid.vastXml = bidData.content; + bid.mediaType = VIDEO; + + let adUnit = find(auctionManager.getAdUnits(), function (unit) { + return unit.transactionId === bidData.transactionId; + }); + + if (adUnit) { + bid.width = adUnit.mediaTypes.video.playerSize[0][0]; + bid.height = adUnit.mediaTypes.video.playerSize[0][1]; + + if (adUnit.mediaTypes.video.context === 'outstream') { + bid.renderer = createRenderer(bid); + } + } + } else if (bidData.placement === 'inImage') { + bid.mediaType = BANNER; + bid.ad = wrapInImageBanner(bid, bidData); + } else { + bid.mediaType = BANNER; + bid.ad = wrapBanner(bid, bidData); + } + + return bid; +} + +function getMediaTypeFromBid(bid) { + return bid.mediaTypes && Object.keys(bid.mediaTypes)[0]; +} + +function hasVideoMandatoryParams(mediaTypes) { + const isHasVideoContext = !!mediaTypes.video && (mediaTypes.video.context === 'instream' || mediaTypes.video.context === 'outstream'); + + const isPlayerSize = + !!utils.deepAccess(mediaTypes, 'video.playerSize') && + utils.isArray(utils.deepAccess(mediaTypes, 'video.playerSize')); + + return isHasVideoContext && isPlayerSize; +} + +function wrapInImageBanner(bid, bidData) { + return ` + + + + + + + + +
+ + + `; +} + +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/vuukleBidAdapter.js b/modules/vuukleBidAdapter.js new file mode 100644 index 00000000000..e9770b5e62e --- /dev/null +++ b/modules/vuukleBidAdapter.js @@ -0,0 +1,63 @@ +import * as utils from '../src/utils.js'; +import { registerBidder } from '../src/adapters/bidderFactory.js'; + +const BIDDER_CODE = 'vuukle'; +const URL = 'https://pb.vuukle.com/adapter'; +const TIME_TO_LIVE = 360; + +export const spec = { + code: BIDDER_CODE, + + isBidRequestValid: function(bid) { + return true + }, + + buildRequests: function(bidRequests) { + const requests = bidRequests.map(function (bid) { + const parseSized = utils.parseSizesInput(bid.sizes); + const arrSize = parseSized[0].split('x'); + const params = { + url: encodeURIComponent(window.location.href), + sizes: JSON.stringify(parseSized), + width: arrSize[0], + height: arrSize[1], + params: JSON.stringify(bid.params), + rnd: Math.random(), + bidId: bid.bidId, + source: 'pbjs', + version: '$prebid.version$', + v: 1, + }; + + return { + method: 'GET', + url: URL, + data: params, + options: {withCredentials: false} + } + }); + + return requests; + }, + + interpretResponse: function(serverResponse, bidRequest) { + if (!serverResponse || !serverResponse.body || !serverResponse.body.ad) { + return []; + } + + const res = serverResponse.body; + const bidResponse = { + requestId: bidRequest.data.bidId, + cpm: res.cpm, + width: res.width, + height: res.height, + creativeId: res.creative_id, + currency: res.currency || 'USD', + netRevenue: true, + ttl: TIME_TO_LIVE, + ad: res.ad + }; + return [bidResponse]; + }, +} +registerBidder(spec); diff --git a/modules/vuukleBidAdapter.md b/modules/vuukleBidAdapter.md new file mode 100644 index 00000000000..ee7b54c6262 --- /dev/null +++ b/modules/vuukleBidAdapter.md @@ -0,0 +1,26 @@ +# Overview +``` +Module Name: Vuukle Bid Adapter +Module Type: Bidder Adapter +Maintainer: support@vuukle.com +``` + +# Description +Module that connects to Vuukle's server for bids. +Currently module supports only banner mediaType. + +# Test Parameters +``` + var adUnits = [{ + code: '/test/div', + mediaTypes: { + banner: { + sizes: [[300, 250]] + } + }, + bids: [{ + bidder: 'vuukle', + params: {} + }] + }]; +``` 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/welectBidAdapter.js b/modules/welectBidAdapter.js index 3913cfde6a0..467b628c944 100644 --- a/modules/welectBidAdapter.js +++ b/modules/welectBidAdapter.js @@ -7,6 +7,7 @@ const DEFAULT_DOMAIN = 'www.welect.de'; export const spec = { code: BIDDER_CODE, aliases: ['wlt'], + gvlid: 282, supportedMediaTypes: ['video'], // short code @@ -17,7 +18,10 @@ export const spec = { * @return boolean True if this is a valid bid, and false otherwise. */ isBidRequestValid: function (bid) { - return utils.deepAccess(bid, 'mediaTypes.video.context') === 'instream' && !!(bid.params.placementId); + return ( + utils.deepAccess(bid, 'mediaTypes.video.context') === 'instream' && + !!bid.params.placementId + ); }, /** * Make a server request from the list of BidRequests. @@ -26,32 +30,33 @@ export const spec = { * @return ServerRequest Info describing the request to the server. */ buildRequests: function (validBidRequests) { - return validBidRequests.map(bidRequest => { - let rawSizes = utils.deepAccess(bidRequest, 'mediaTypes.video.playerSize') || bidRequest.sizes; - let size = rawSizes[0] + return validBidRequests.map((bidRequest) => { + let rawSizes = + utils.deepAccess(bidRequest, 'mediaTypes.video.playerSize') || + bidRequest.sizes; + let size = rawSizes[0]; let domain = bidRequest.params.domain || DEFAULT_DOMAIN; - let url = `https://${domain}/api/v2/preflight/by_alias/${bidRequest.params.placementId}`; + let url = `https://${domain}/api/v2/preflight/${bidRequest.params.placementId}`; let gdprConsent = null; if (bidRequest && bidRequest.gdprConsent) { gdprConsent = { - gdpr_consent: - { - gdpr_applies: bidRequest.gdprConsent.gdprApplies, - gdpr_consent: bidRequest.gdprConsent.gdprConsent - } - } + gdpr_consent: { + gdprApplies: bidRequest.gdprConsent.gdprApplies, + tcString: bidRequest.gdprConsent.gdprConsent, + }, + }; } const data = { width: size[0], height: size[1], bid_id: bidRequest.bidId, - ...gdprConsent - } + ...gdprConsent, + }; return { method: 'POST', @@ -61,8 +66,8 @@ export const spec = { contentType: 'application/json', withCredentials: false, crossOrigin: true, - } - } + }, + }; }); }, /** @@ -82,6 +87,6 @@ export const spec = { const bidResponse = responseBody.bidResponse; bidResponses.push(bidResponse); return bidResponses; - } -} + }, +}; registerBidder(spec); diff --git a/modules/yieldlabBidAdapter.js b/modules/yieldlabBidAdapter.js index b2a9176e342..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, @@ -47,6 +47,9 @@ export const spec = { query[prop] = bid.params.customParams[prop] } } + if (bid.schain && utils.isPlainObject(bid.schain) && Array.isArray(bid.schain.nodes)) { + query.schain = createSchainString(bid.schain) + } }) if (bidderRequest) { @@ -68,7 +71,8 @@ export const spec = { return { method: 'GET', url: `${ENDPOINT}/yp/${adslots}?${queryString}`, - validBidRequests: validBidRequests + validBidRequests: validBidRequests, + queryParams: query } }, @@ -80,6 +84,7 @@ export const spec = { interpretResponse: function (serverResponse, originalBidRequest) { const bidResponses = [] const timestamp = Date.now() + const reqParams = originalBidRequest.queryParams originalBidRequest.validBidRequests.forEach(function (bidRequest) { if (!serverResponse.body) { @@ -91,23 +96,25 @@ 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 : '' + const gdprConsent = reqParams.consent ? '&consent=' + reqParams.consent : '' 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)) { @@ -117,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}` - + bidResponse.vastUrl = `${ENDPOINT}/d/${matchedBid.id}/${bidRequest.params.supplyId}/?ts=${timestamp}${extId}${gdprApplies}${gdprConsent}` if (isOutstream(bidRequest)) { const renderer = Renderer.install({ id: bidRequest.bidId, @@ -198,7 +204,12 @@ function createQueryString (obj) { let str = [] for (var p in obj) { if (obj.hasOwnProperty(p)) { - str.push(encodeURIComponent(p) + '=' + encodeURIComponent(obj[p])) + let val = obj[p] + if (p !== 'schain') { + str.push(encodeURIComponent(p) + '=' + encodeURIComponent(val)) + } else { + str.push(p + '=' + val) + } } } return str.join('&') @@ -221,6 +232,30 @@ function createTargetingString (obj) { return str.join('&') } +/** + * Creates a string out of a schain object + * @param {Object} schain + * @returns {String} + */ +function createSchainString (schain) { + const ver = schain.ver || '' + const complete = (schain.complete === 1 || schain.complete === 0) ? schain.complete : '' + const keys = ['asi', 'sid', 'hp', 'rid', 'name', 'domain', 'ext'] + const nodesString = schain.nodes.reduce((acc, node) => { + return acc += `!${keys.map(key => node[key] ? encodeURIComponentWithBangIncluded(node[key]) : '').join(',')}` + }, '') + return `${ver},${complete}${nodesString}` +} + +/** + * Encodes URI Component with exlamation mark included. Needed for schain object. + * @param {String} str + * @returns {String} + */ +function encodeURIComponentWithBangIncluded(str) { + return encodeURIComponent(str).replace(/!/g, '%21') +} + /** * Handles an outstream response after the library is loaded * @param {Object} bid 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 829b573ffd9..36f93f60c9e 100644 --- a/modules/yieldmoBidAdapter.js +++ b/modules/yieldmoBidAdapter.js @@ -1,102 +1,155 @@ import * as utils from '../src/utils.js'; +import { BANNER, VIDEO } from '../src/mediaTypes.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; +import { Renderer } from '../src/Renderer.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'; const TIME_TO_LIVE = 300; const NET_REVENUE = true; -const SERVER_ENDPOINT = 'https://ads.yieldmo.com/exchange/prebid'; -const localWindow = utils.getWindowTop(); +const BANNER_SERVER_ENDPOINT = 'https://ads.yieldmo.com/exchange/prebid'; +const VIDEO_SERVER_ENDPOINT = 'https://ads.yieldmo.com/exchange/prebidvideo'; +const OUTSTREAM_VIDEO_PLAYER_URL = 'https://prebid-outstream.yieldmo.com/bundle.js'; +const OPENRTB_VIDEO_BIDPARAMS = ['placement', 'startdelay', 'skipafter', + 'protocols', 'api', 'playbackmethod', 'maxduration', 'minduration', 'pos']; +const OPENRTB_VIDEO_SITEPARAMS = ['name', 'domain', 'cat', 'keywords']; +const LOCAL_WINDOW = utils.getWindowTop(); +const DEFAULT_PLAYBACK_METHOD = 2; +const DEFAULT_START_DELAY = 0; +const VAST_TIMEOUT = 15000; +const MAX_BANNER_REQUEST_URL_LENGTH = 8000; +const BANNER_REQUEST_PROPERTIES_TO_REDUCE = ['description', 'title', 'pr', 'page_url']; export const spec = { code: BIDDER_CODE, - supportedMediaTypes: ['banner'], + supportedMediaTypes: [BANNER, VIDEO], + /** * Determines whether or not the given bid request is valid. * @param {object} bid, bid to validate * @return boolean, true if valid, otherwise false */ isBidRequestValid: function (bid) { - return !!(bid && bid.adUnitCode && bid.bidId); + return !!(bid && bid.adUnitCode && bid.bidId && (hasBannerMediaType(bid) || hasVideoMediaType(bid)) && + validateVideoParams(bid)); }, + /** * Make a server request from the list of BidRequests. * * @param {BidRequest[]} bidRequests A non-empty list of bid requests which should be sent to the Server. + * @param {BidderRequest} bidderRequest bidder request object. * @return ServerRequest Info describing the request to the server. */ buildRequests: function (bidRequests, bidderRequest) { - let serverRequest = { - p: [], - page_url: bidderRequest.refererInfo.referer, - bust: new Date().getTime().toString(), - pr: bidderRequest.refererInfo.referer, - scrd: localWindow.devicePixelRatio || 0, - dnt: getDNT(), - description: getPageDescription(), - title: localWindow.document.title || '', - w: localWindow.innerWidth, - h: localWindow.innerHeight, - userConsent: JSON.stringify({ - // case of undefined, stringify will remove param - gdprApplies: utils.deepAccess(bidderRequest, 'gdprConsent.gdprApplies') || '', - cmp: utils.deepAccess(bidderRequest, 'gdprConsent.consentString') || '' - }), - us_privacy: utils.deepAccess(bidderRequest, 'uspConsent') || '' - }; - - const mtp = window.navigator.maxTouchPoints; - if (mtp) { - serverRequest.mtp = mtp; - } + const bannerBidRequests = bidRequests.filter(request => hasBannerMediaType(request)); + const videoBidRequests = bidRequests.filter(request => hasVideoMediaType(request)); - bidRequests.forEach(request => { - serverRequest.p.push(addPlacement(request)); - const pubcid = getId(request, 'pubcid'); - if (pubcid) { - serverRequest.pubcid = pubcid; - } else if (request.crumbs) { - if (request.crumbs.pubcid) { + let serverRequests = []; + if (bannerBidRequests.length > 0) { + let serverRequest = { + pbav: '$prebid.version$', + p: [], + page_url: bidderRequest.refererInfo.referer, + bust: new Date().getTime().toString(), + pr: (LOCAL_WINDOW.document && LOCAL_WINDOW.document.referrer) || '', + scrd: LOCAL_WINDOW.devicePixelRatio || 0, + dnt: getDNT(), + description: getPageDescription(), + title: LOCAL_WINDOW.document.title || '', + w: LOCAL_WINDOW.innerWidth, + h: LOCAL_WINDOW.innerHeight, + userConsent: JSON.stringify({ + // case of undefined, stringify will remove param + gdprApplies: utils.deepAccess(bidderRequest, 'gdprConsent.gdprApplies') || '', + cmp: utils.deepAccess(bidderRequest, 'gdprConsent.consentString') || '' + }), + us_privacy: utils.deepAccess(bidderRequest, 'uspConsent') || '' + }; + + const mtp = window.navigator.maxTouchPoints; + if (mtp) { + serverRequest.mtp = mtp; + } + + bannerBidRequests.forEach(request => { + serverRequest.p.push(addPlacement(request)); + const pubcid = getId(request, 'pubcid'); + if (pubcid) { + serverRequest.pubcid = pubcid; + } else if (request.crumbs && request.crumbs.pubcid) { serverRequest.pubcid = request.crumbs.pubcid; } + const tdid = getId(request, 'tdid'); + if (tdid) { + serverRequest.tdid = tdid; + } + const criteoId = getId(request, 'criteoId'); + if (criteoId) { + serverRequest.cri_prebid = criteoId; + } + if (request.schain) { + serverRequest.schain = JSON.stringify(request.schain); + } + }); + serverRequest.p = '[' + serverRequest.p.toString() + ']'; + + // check if url exceeded max length + const url = `${BANNER_SERVER_ENDPOINT}?${utils.parseQueryStringParameters(serverRequest)}`; + let extraCharacters = url.length - MAX_BANNER_REQUEST_URL_LENGTH; + if (extraCharacters > 0) { + for (let i = 0; i < BANNER_REQUEST_PROPERTIES_TO_REDUCE.length; i++) { + extraCharacters = shortcutProperty(extraCharacters, serverRequest, BANNER_REQUEST_PROPERTIES_TO_REDUCE[i]); + + if (extraCharacters <= 0) { + break; + } + } } - const tdid = getId(request, 'tdid'); - if (tdid) { - serverRequest.tdid = tdid; - } - const criteoId = getId(request, 'criteoId'); - if (criteoId) { - serverRequest.cri_prebid = criteoId; - } - if (request.schain) { - serverRequest.schain = - JSON.stringify(request.schain); - } - }); - serverRequest.p = '[' + serverRequest.p.toString() + ']'; - return { - method: 'GET', - url: SERVER_ENDPOINT, - data: serverRequest - }; + + serverRequests.push({ + method: 'GET', + url: BANNER_SERVER_ENDPOINT, + data: serverRequest + }); + } + + if (videoBidRequests.length > 0) { + const serverRequest = openRtbRequest(videoBidRequests, bidderRequest); + serverRequests.push({ + method: 'POST', + url: VIDEO_SERVER_ENDPOINT, + data: serverRequest + }); + } + return serverRequests; }, + /** * Makes Yieldmo Ad Server response compatible to Prebid specs - * @param serverResponse successful response from Ad Server + * @param {ServerResponse} serverResponse successful response from Ad Server + * @param {ServerRequest} bidRequest * @return {Bid[]} an array of bids */ - interpretResponse: function (serverResponse) { + interpretResponse: function (serverResponse, bidRequest) { let bids = []; - let data = serverResponse.body; + const data = serverResponse.body; if (data.length > 0) { data.forEach(response => { - if (response.cpm && response.cpm > 0) { - bids.push(createNewBid(response)); + if (response.cpm > 0) { + bids.push(createNewBannerBid(response)); } }); } + if (data.seatbid) { + const seatbids = data.seatbid.reduce((acc, seatBid) => acc.concat(seatBid.bid), []); + seatbids.forEach(bid => bids.push(createNewVideoBid(bid, bidRequest))); + } return bids; }, + getUserSyncs: function () { return []; } @@ -107,6 +160,20 @@ registerBidder(spec); * Helper Functions ***************************************/ +/** + * @param {BidRequest} bidRequest bid request + */ +function hasBannerMediaType(bidRequest) { + return !!utils.deepAccess(bidRequest, 'mediaTypes.banner'); +} + +/** + * @param {BidRequest} bidRequest bid request + */ +function hasVideoMediaType(bidRequest) { + return !!utils.deepAccess(bidRequest, 'mediaTypes.video'); +} + /** * Adds placement information to array * @param request bid request @@ -121,18 +188,19 @@ function addPlacement(request) { if (request.params.placementId) { placementInfo.ym_placement_id = request.params.placementId; } - if (request.params.bidFloor) { - placementInfo.bidFloor = request.params.bidFloor; + const bidfloor = getBidFloor(request, BANNER); + if (bidfloor) { + placementInfo.bidFloor = bidfloor; } } return JSON.stringify(placementInfo); } /** - * creates a new bid with response information + * creates a new banner bid with response information * @param response server response */ -function createNewBid(response) { +function createNewBannerBid(response) { return { requestId: response['callback_id'], cpm: response.cpm, @@ -143,9 +211,68 @@ function createNewBid(response) { netRevenue: NET_REVENUE, ttl: TIME_TO_LIVE, ad: response.ad, + meta: { + advertiserDomains: response.adomain || [], + mediaType: BANNER, + }, }; } +/** + * creates a new video bid with response information + * @param response openRTB server response + * @param bidRequest server request + */ +function createNewVideoBid(response, bidRequest) { + const imp = find((utils.deepAccess(bidRequest, 'data.imp') || []), imp => imp.id === response.impid); + + let result = { + requestId: imp.id, + cpm: response.price, + width: imp.video.w, + height: imp.video.h, + creativeId: response.crid || response.adid, + currency: CURRENCY, + netRevenue: NET_REVENUE, + mediaType: VIDEO, + ttl: TIME_TO_LIVE, + vastXml: response.adm, + meta: { + advertiserDomains: response.adomain || [], + mediaType: VIDEO, + }, + }; + + if (imp.video.placement && imp.video.placement !== 1) { + const renderer = Renderer.install({ + url: OUTSTREAM_VIDEO_PLAYER_URL, + config: { + width: result.width, + height: result.height, + vastTimeout: VAST_TIMEOUT, + maxAllowedVastTagRedirects: 5, + allowVpaid: true, + autoPlay: true, + preload: true, + mute: true + }, + id: imp.tagid, + loaded: false, + }); + + renderer.setRender(function (bid) { + bid.renderer.push(() => { + const { id, config } = bid.renderer; + window.YMoutstreamPlayer(bid, id, config); + }); + }); + + result.renderer = renderer; + } + + return result; +} + /** * Detects whether dnt is true * @returns true if user enabled dnt @@ -163,7 +290,7 @@ function getPageDescription() { if (document.querySelector('meta[name="description"]')) { return document .querySelector('meta[name="description"]') - .getAttribute('content'); // Value of the description metadata from the publisher's page. + .getAttribute('content') || ''; // Value of the description metadata from the publisher's page. } else { return ''; } @@ -178,3 +305,251 @@ function getPageDescription() { function getId(request, idType) { return (typeof utils.deepAccess(request, 'userId') === 'object') ? request.userId[idType] : undefined; } + +/** + * @param {BidRequest[]} bidRequests bid request object + * @param {BidderRequest} bidderRequest bidder request object + * @return Object OpenRTB request object + */ +function openRtbRequest(bidRequests, bidderRequest) { + let openRtbRequest = { + id: bidRequests[0].bidderRequestId, + at: 1, + imp: bidRequests.map(bidRequest => openRtbImpression(bidRequest)), + site: openRtbSite(bidRequests[0], bidderRequest), + device: openRtbDevice(), + badv: bidRequests[0].params.badv || [], + bcat: bidRequests[0].params.bcat || [], + ext: { + prebid: '$prebid.version$', + } + }; + + populateOpenRtbGdpr(openRtbRequest, bidderRequest); + + return openRtbRequest; +} + +/** + * @param {BidRequest} bidRequest bidder request object. + * @return Object OpenRTB's 'imp' (impression) object + */ +function openRtbImpression(bidRequest) { + const videoReq = utils.deepAccess(bidRequest, 'mediaTypes.video'); + const size = extractPlayerSize(bidRequest); + const imp = { + id: bidRequest.bidId, + tagid: bidRequest.adUnitCode, + bidfloor: getBidFloor(bidRequest, VIDEO), + ext: { + placement_id: bidRequest.params.placementId + }, + video: { + w: size[0], + h: size[1], + mimes: videoReq.mimes, + linearity: 1 + } + }; + + const videoParams = utils.deepAccess(bidRequest, 'params.video'); + Object.keys(videoParams) + .filter(param => includes(OPENRTB_VIDEO_BIDPARAMS, param)) + .forEach(param => imp.video[param] = videoParams[param]); + + if (videoParams.skippable) imp.video.skip = 1; + if (videoParams.placement !== 1) { + imp.video = { + ...imp.video, + startdelay: DEFAULT_START_DELAY, + playbackmethod: [ DEFAULT_PLAYBACK_METHOD ] + } + } + return imp; +} + +function getBidFloor(bidRequest, mediaType) { + let floorInfo = {}; + + if (typeof bidRequest.getFloor === 'function') { + floorInfo = bidRequest.getFloor({ currency: CURRENCY, mediaType, size: '*' }); + } + + return floorInfo.floor || bidRequest.params.bidfloor || bidRequest.params.bidFloor || 0; +} + +/** + * @param {BidRequest} bidRequest bidder request object. + * @return [number, number] || null Player's width and height, or undefined otherwise. + */ +function extractPlayerSize(bidRequest) { + const sizeArr = utils.deepAccess(bidRequest, 'mediaTypes.video.playerSize'); + if (utils.isArrayOfNums(sizeArr, 2)) { + return sizeArr; + } else if (utils.isArray(sizeArr) && utils.isArrayOfNums(sizeArr[0], 2)) { + return sizeArr[0]; + } + return null; +} + +/** + * @param {BidRequest} bidRequest bid request object + * @param {BidderRequest} bidderRequest bidder request object + * @return Object OpenRTB's 'site' object + */ +function openRtbSite(bidRequest, bidderRequest) { + let result = {}; + + const loc = utils.parseUrl(utils.deepAccess(bidderRequest, 'refererInfo.referer')); + if (!utils.isEmpty(loc)) { + result.page = `${loc.protocol}://${loc.hostname}${loc.pathname}`; + } + + if (self === top && document.referrer) { + result.ref = document.referrer; + } + + const keywords = document.getElementsByTagName('meta')['keywords']; + if (keywords && keywords.content) { + result.keywords = keywords.content; + } + + const siteParams = utils.deepAccess(bidRequest, 'params.site'); + if (siteParams) { + Object.keys(siteParams) + .filter(param => includes(OPENRTB_VIDEO_SITEPARAMS, param)) + .forEach(param => result[param] = siteParams[param]); + } + return result; +} + +/** + * @return Object OpenRTB's 'device' object + */ +function openRtbDevice() { + return { + ua: navigator.userAgent, + language: (navigator.language || navigator.browserLanguage || navigator.userLanguage || navigator.systemLanguage), + }; +} + +/** + * Updates openRtbRequest with GDPR info from bidderRequest, if present. + * @param {Object} openRtbRequest OpenRTB's request to update. + * @param {BidderRequest} bidderRequest bidder request object. + */ +function populateOpenRtbGdpr(openRtbRequest, bidderRequest) { + const gdpr = bidderRequest.gdprConsent; + if (gdpr && 'gdprApplies' in gdpr) { + utils.deepSetValue(openRtbRequest, 'regs.ext.gdpr', gdpr.gdprApplies ? 1 : 0); + utils.deepSetValue(openRtbRequest, 'user.ext.consent', gdpr.consentString); + } + const uspConsent = utils.deepAccess(bidderRequest, 'uspConsent'); + if (uspConsent) { + utils.deepSetValue(openRtbRequest, 'regs.ext.us_privacy', uspConsent); + } +} + +/** + * Determines whether or not the given video bid request is valid. If it's not a video bid, returns true. + * @param {object} bid, bid to validate + * @return boolean, true if valid, otherwise false + */ +function validateVideoParams(bid) { + if (!hasVideoMediaType(bid)) { + return true; + } + + const paramRequired = (paramStr, value, conditionStr) => { + let error = `"${paramStr}" is required`; + if (conditionStr) { + error += ' when ' + conditionStr; + } + throw new Error(error); + } + + const paramInvalid = (paramStr, value, expectedStr) => { + expectedStr = expectedStr ? ', expected: ' + expectedStr : ''; + value = JSON.stringify(value); + throw new Error(`"${paramStr}"=${value} is invalid${expectedStr}`); + } + + const isDefined = val => typeof val !== 'undefined'; + const validate = (fieldPath, validateCb, errorCb, errorCbParam) => { + const value = utils.deepAccess(bid, fieldPath); + if (!validateCb(value)) { + errorCb(fieldPath, value, errorCbParam); + } + return value; + } + + try { + validate('params.placementId', val => !utils.isEmpty(val), paramRequired); + + validate('mediaTypes.video.playerSize', val => utils.isArrayOfNums(val, 2) || + (utils.isArray(val) && val.every(v => utils.isArrayOfNums(v, 2))), + paramInvalid, 'array of 2 integers, ex: [640,480] or [[640,480]]'); + + validate('mediaTypes.video.mimes', val => isDefined(val), paramRequired); + validate('mediaTypes.video.mimes', val => utils.isArray(val) && val.every(v => utils.isStr(v)), paramInvalid, + 'array of strings, ex: ["video/mp4"]'); + + validate('params.video', val => !utils.isEmpty(val), paramRequired); + + const placement = validate('params.video.placement', val => isDefined(val), paramRequired); + validate('params.video.placement', val => val >= 1 && val <= 5, paramInvalid); + if (placement === 1) { + validate('params.video.startdelay', val => isDefined(val), + (field, v) => paramRequired(field, v, 'placement == 1')); + validate('params.video.startdelay', val => utils.isNumber(val), paramInvalid, 'number, ex: 5'); + } + + validate('params.video.protocols', val => isDefined(val), paramRequired); + validate('params.video.protocols', val => utils.isArrayOfNums(val) && val.every(v => (v >= 1 && v <= 6)), + paramInvalid, 'array of numbers, ex: [2,3]'); + + validate('params.video.api', val => isDefined(val), paramRequired); + validate('params.video.api', val => utils.isArrayOfNums(val) && val.every(v => (v >= 1 && v <= 6)), + paramInvalid, 'array of numbers, ex: [2,3]'); + + validate('params.video.playbackmethod', val => !isDefined(val) || utils.isArrayOfNums(val), paramInvalid, + 'array of integers, ex: [2,6]'); + + validate('params.video.maxduration', val => isDefined(val), paramRequired); + validate('params.video.maxduration', val => utils.isInteger(val), paramInvalid); + validate('params.video.minduration', val => !isDefined(val) || utils.isNumber(val), paramInvalid); + validate('params.video.skippable', val => !isDefined(val) || utils.isBoolean(val), paramInvalid); + validate('params.video.skipafter', val => !isDefined(val) || utils.isNumber(val), paramInvalid); + validate('params.video.pos', val => !isDefined(val) || utils.isNumber(val), paramInvalid); + validate('params.badv', val => !isDefined(val) || utils.isArray(val), paramInvalid, + 'array of strings, ex: ["ford.com","pepsi.com"]'); + validate('params.bcat', val => !isDefined(val) || utils.isArray(val), paramInvalid, + 'array of strings, ex: ["IAB1-5","IAB1-6"]'); + return true; + } catch (e) { + utils.logError(e.message); + return false; + } +} + +/** + * Shortcut object property and check if required characters count was deleted + * + * @param {number} extraCharacters, count of characters to remove + * @param {object} target, object on which string property length should be reduced + * @param {string} propertyName, name of property to reduce + * @return {number} 0 if required characters count was removed otherwise count of how many left + */ +function shortcutProperty(extraCharacters, target, propertyName) { + if (target[propertyName].length > extraCharacters) { + target[propertyName] = target[propertyName].substring(0, target[propertyName].length - extraCharacters); + + return 0 + } + + const charactersLeft = extraCharacters - target[propertyName].length; + + target[propertyName] = ''; + + return charactersLeft; +} diff --git a/modules/yieldmoBidAdapter.md b/modules/yieldmoBidAdapter.md index 0f86d2507d1..54be295a1a1 100644 --- a/modules/yieldmoBidAdapter.md +++ b/modules/yieldmoBidAdapter.md @@ -11,26 +11,88 @@ Note: Our ads will only render in mobile Connects to Yieldmo Ad Server for bids. -Yieldmo bid adapter supports Banner. +Yieldmo bid adapter supports Banner and Video. # Test Parameters + +## Banner + +Sample banner ad unit config: +```javascript +var adUnits = [{ // Banner adUnit + code: 'div-gpt-ad-1460505748561-0', + mediaTypes: { + banner: { + sizes: [[300, 250], [300, 600]], + } + }, + bids: [{ + bidder: 'yieldmo', + params: { + placementId: '1779781193098233305', // string with at most 19 characters (may include numbers only) + bidFloor: .28 // optional param + } + }] +}]; +``` + +## Video + +Sample instream video ad unit config: +```javascript +var adUnits = [{ // Video adUnit + code: 'div-video-ad-1234567890', + mediaTypes: { + video: { + playerSize: [640, 480], // required + context: 'instream', + mimes: ['video/mp4'] // required, array of strings + } + }, + bids: [{ + bidder: 'yieldmo', + params: { + placementId: '1524592390382976659', // required + video: { + placement: 1, // required, integer + maxduration: 30, // required, integer + minduration: 15, // optional, integer + pos: 1, // optional, integer + startdelay: 10, // required if placement == 1 + protocols: [2, 3], // required, array of integers + api: [2, 3], // required, array of integers + playbackmethod: [2,6], // required, array of integers + skippable: true, // optional, boolean + skipafter: 10 // optional, integer + } + } + }] +}]; +``` + +Sample out-stream video ad unit config: +```javascript +var videoAdUnit = [{ + code: 'div-video-ad-1234567890', + mediaTypes: { + video: { + playerSize: [640, 480], // required + context: 'outstream', + mimes: ['video/mp4'] // required, array of strings + } + }, + bids: [{ + bidder: 'yieldmo', + params: { + placementId: '1524592390382976659', // required + video: { + placement: 3, // required, integer ( 3,4,5 ) + maxduration: 30, // required, integer + protocols: [2, 3], // required, array of integers + api: [2, 3], // required, array of integers + playbackmethod: [1,2] // required, array of integers + } + } + }] +}]; ``` -var adUnits = [ - // Banner adUnit - { - code: 'div-gpt-ad-1460505748561-0', - mediaTypes: { - banner: { - sizes: [[300, 250], [300,600]], - } - } - bids: [{ - bidder: 'yieldmo', - params: { - placementId: '1779781193098233305', // string with at most 19 characters (may include numbers only) - bidFloor: .28 // optional param - } - }] - } -]; -``` \ No newline at end of file diff --git a/modules/yuktamediaAnalyticsAdapter.js b/modules/yuktamediaAnalyticsAdapter.js index 3c27ca9754a..2ef2d251ace 100644 --- a/modules/yuktamediaAnalyticsAdapter.js +++ b/modules/yuktamediaAnalyticsAdapter.js @@ -4,18 +4,37 @@ import adapterManager from '../src/adapterManager.js'; 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.0.0'; +const yuktamediaAnalyticsVersion = 'v3.1.0'; let initOptions; -let auctionTimestamp; const events = { auctions: {} }; const localStoragePrefix = 'yuktamediaAnalytics_'; const utmTags = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content']; +const location = utils.getWindowLocation(); +const referer = getRefererInfo().referer; +const _pageInfo = { + userAgent: window.navigator.userAgent, + timezoneOffset: new Date().getTimezoneOffset(), + language: window.navigator.language, + screenWidth: window.screen.width, + screenHeight: window.screen.height, + pageViewId: utils.generateUUID(), + host: location.host, + path: location.pathname, + search: location.search, + hash: location.hash, + referer: referer, + refererDomain: utils.parseUrl(referer).host, + yuktamediaAnalyticsVersion: yuktamediaAnalyticsVersion, + prebidVersion: $$PREBID_GLOBAL$$.version +}; function getParameterByName(param) { let vars = {}; @@ -60,20 +79,12 @@ function isUtmTimeoutExpired() { } function send(data, status) { - const location = utils.getWindowLocation(); - data.initOptions = Object.assign({ host: location.host, path: location.pathname, search: location.search }, initOptions); - + data.initOptions = Object.assign(_pageInfo, initOptions); const yuktamediaAnalyticsRequestUrl = utils.buildUrl({ protocol: 'https', hostname: 'analytics-prebid.yuktamedia.com', - pathname: '/api/bids', - search: { - auctionTimestamp: auctionTimestamp, - yuktamediaAnalyticsVersion: yuktamediaAnalyticsVersion, - prebidVersion: $$PREBID_GLOBAL$$.version - } + pathname: '/api/bids' }); - if (isNavigatorSendBeaconSupported()) { window.navigator.sendBeacon(yuktamediaAnalyticsRequestUrl, JSON.stringify(data)); } else { @@ -81,7 +92,7 @@ function send(data, status) { } } -var yuktamediaAnalyticsAdapter = Object.assign(adapter({analyticsType: 'endpoint'}), { +var yuktamediaAnalyticsAdapter = Object.assign(adapter({ analyticsType: 'endpoint' }), { track({ eventType, args }) { if (typeof args !== 'undefined') { switch (eventType) { @@ -89,7 +100,6 @@ var yuktamediaAnalyticsAdapter = Object.assign(adapter({analyticsType: 'endpoint utils.logInfo(localStoragePrefix + 'AUCTION_INIT:', JSON.stringify(args)); if (typeof args.auctionId !== 'undefined' && args.auctionId.length) { events.auctions[args.auctionId] = { bids: {} }; - auctionTimestamp = args.timestamp; } break; case CONSTANTS.EVENTS.BID_REQUESTED: @@ -143,7 +153,7 @@ var yuktamediaAnalyticsAdapter = Object.assign(adapter({analyticsType: 'endpoint 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..b2c00b69c24 --- /dev/null +++ b/modules/zemantaBidAdapter.js @@ -0,0 +1,281 @@ +// 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 GVLID = 164; +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, + gvlid: GVLID, + aliases: [ + { code: 'outbrain', gvlid: GVLID } + ], + 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 bcat = setOnAny(validBidRequests, 'params.bcat'); + const badv = setOnAny(validBidRequests, 'params.badv'); + 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, + bcat: bcat, + badv: badv, + }; + + 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; + } + bidObject.meta = {}; + if (bidResponse.adomain && bidResponse.adomain.length > 0) { + bidObject.meta.advertiserDomains = bidResponse.adomain; + } + return bidObject; + } + }).filter(Boolean); + }, + getUserSyncs: (syncOptions, responses, gdprConsent, uspConsent) => { + const syncs = []; + let syncUrl = config.getConfig('zemanta.usersyncUrl') || config.getConfig('outbrain.usersyncUrl'); + if (syncOptions.pixelEnabled && syncUrl) { + if (gdprConsent) { + syncUrl += '&gdpr=' + (gdprConsent.gdprApplies & 1); + syncUrl += '&gdpr_consent=' + encodeURIComponent(gdprConsent.consentString || ''); + } + if (uspConsent) { + syncUrl += '&us_privacy=' + encodeURIComponent(uspConsent); + } + + syncs.push({ + type: 'image', + url: syncUrl + }); + } + return syncs; + }, + onBidWon: (bid) => { + // for native requests we put the nurl as an imp tracker, otherwise if the auction takes place on prebid server + // the server JS adapter puts the nurl in the adm as a tracking pixel and removes the attribute + if (bid.nurl) { + 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..fa933ecd922 --- /dev/null +++ b/modules/zemantaBidAdapter.md @@ -0,0 +1,111 @@ +# 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', + bcat: ['IAB1-1'], + badv: ['example.com'] + } + }] + ]; + + 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', + bcat: ['IAB1-1'], + badv: ['example.com'] + }, + }] + ]; + + pbjs.setConfig({ + zemanta: { + bidderUrl: 'https://prebidtest.zemanta.com/api/bidder/prebidtest/bid/' + } + }); +``` diff --git a/modules/zeotapIdPlusIdSystem.js b/modules/zeotapIdPlusIdSystem.js new file mode 100644 index 00000000000..8f26cc827d6 --- /dev/null +++ b/modules/zeotapIdPlusIdSystem.js @@ -0,0 +1,64 @@ +/** + * This module adds Zeotap to the User ID module + * The {@link module:modules/userId} module is required + * @module modules/zeotapIdPlusIdSystem + * @requires module:modules/userId + */ +import * as utils from '../src/utils.js' +import {submodule} from '../src/hook.js'; +import { getStorageManager } from '../src/storageManager.js'; + +const ZEOTAP_COOKIE_NAME = 'IDP'; +const ZEOTAP_VENDOR_ID = 301; +const ZEOTAP_MODULE_NAME = 'zeotapIdPlus'; + +function readCookie() { + return storage.cookiesAreEnabled() ? storage.getCookie(ZEOTAP_COOKIE_NAME) : null; +} + +function readFromLocalStorage() { + 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: ZEOTAP_MODULE_NAME, + /** + * Vendor ID of Zeotap + * @type {Number} + */ + gvlid: ZEOTAP_VENDOR_ID, + /** + * decode the stored id value for passing to bid requests + * @function + * @param { Object | string | undefined } value + * @return { Object | string | undefined } + */ + decode(value) { + const id = value ? utils.isStr(value) ? value : utils.isPlainObject(value) ? value.id : undefined : undefined; + return id ? { + 'IDP': JSON.parse(atob(id)) + } : undefined; + }, + /** + * performs action to obtain id and return a value in the callback's response argument + * @function + * @param {SubmoduleConfig} config + * @return {{id: string | undefined} | undefined} + */ + getId() { + const id = readCookie() || readFromLocalStorage(); + return id ? { id } : undefined; + } +}; +submodule('userId', zeotapIdPlusSubmodule); diff --git a/modules/zetaBidAdapter.js b/modules/zetaBidAdapter.js new file mode 100644 index 00000000000..ee5c854df97 --- /dev/null +++ b/modules/zetaBidAdapter.js @@ -0,0 +1,185 @@ +import * as utils from '../src/utils.js'; +import { registerBidder } from '../src/adapters/bidderFactory.js'; +import {BANNER} from '../src/mediaTypes.js'; +const BIDDER_CODE = 'zeta_global'; +const ENDPOINT_URL = 'https://prebid.rfihub.com/prebid'; +const USER_SYNC_URL = 'https://p.rfihub.com/cm?pub=42770&in=1'; +const DEFAULT_CUR = 'USD'; +const TTL = 200; +const NET_REV = true; + +export const spec = { + code: BIDDER_CODE, + supportedMediaTypes: [BANNER], + + /** + * Determines whether or not the given bid request is valid. + * + * @param {BidRequest} bid The bid params to validate. + * @return boolean True if this is a valid bid, and false otherwise. + */ + isBidRequestValid: function(bid) { + // check for all required bid fields + if (!(bid && + bid.bidId && + bid.params)) { + utils.logWarn('Invalid bid request - missing required bid data'); + return false; + } + + 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; + }, + + /** + * Make a server request from the list of BidRequests. + * + * @param {Bids[]} validBidRequests - an array of bidRequest objects + * @param {BidderRequest} bidderRequest - master bidRequest object + * @return ServerRequest Info describing the request to the server. + */ + buildRequests: function(validBidRequests, bidderRequest) { + const secure = 1; // treat all requests as secure + const request = validBidRequests[0]; + const params = request.params; + let impData = { + id: request.bidId, + secure: secure, + banner: buildBanner(request) + }; + let payload = { + id: bidderRequest.auctionId, + imp: [impData], + site: params.site ? params.site : {}, + app: params.app ? params.app : {}, + device: params.device ? params.device : {}, + user: params.user ? params.user : {}, + at: params.at, + tmax: params.tmax, + wseat: params.wseat, + bseat: params.bseat, + allimps: params.allimps, + cur: [DEFAULT_CUR], + wlang: params.wlang, + bcat: params.bcat, + badv: params.badv, + bapp: params.bapp, + source: params.source ? params.source : {}, + ext: params.ext ? params.ext : {} + }; + + 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; + payload.ext.definerId = params.definerId; + + if (params.test) { + payload.test = params.test; + } + if (request.gdprConsent) { + payload.regs = { + ext: { + gdpr: request.gdprConsent.gdprApplies === true ? 1 : 0 + } + }; + } + if (request.gdprConsent && request.gdprConsent.gdprApplies) { + payload.user = { + ext: { + consent: request.gdprConsent.consentString + } + }; + } + const postUrl = params.definerId !== '0' ? ENDPOINT_URL.concat('/', params.definerId) : ENDPOINT_URL; + return { + method: 'POST', + url: postUrl, + data: JSON.stringify(payload), + }; + }, + + /** + * Unpack the response from the server into a list of bids. + * + * @param {ServerResponse} serverResponse A successful response from the server. + * @param bidRequest The payload from the server's response. + * @return {Bid[]} An array of bids which were nested inside the server. + */ + interpretResponse: function(serverResponse, bidRequest) { + let bidResponse = []; + if (Object.keys(serverResponse.body).length !== 0) { + let zetaResponse = serverResponse.body; + let zetaBid = zetaResponse.seatbid[0].bid[0]; + let bid = { + requestId: zetaBid.impid, + cpm: zetaBid.price, + currency: zetaResponse.cur, + width: zetaBid.w, + height: zetaBid.h, + ad: zetaBid.adm, + ttl: TTL, + creativeId: zetaBid.crid, + netRevenue: NET_REV + }; + bidResponse.push(bid); + } + return bidResponse; + }, + + /** + * Register the user sync pixels which should be dropped after the auction. + * + * @param {SyncOptions} syncOptions Which user syncs are allowed? + * @param {ServerResponse[]} serverResponses List of server's responses. + * @param gdprConsent The GDPR consent parameters + * @param uspConsent The USP consent parameters + * @return {UserSync[]} The user syncs which should be dropped. + */ + getUserSyncs: function(syncOptions, serverResponses, gdprConsent, uspConsent) { + const syncs = []; + if (syncOptions.iframeEnabled) { + syncs.push({ + type: 'iframe', + url: USER_SYNC_URL + }); + } + return syncs; + } +} + +function buildBanner(request) { + let sizes = request.sizes; + if (request.mediaTypes && + request.mediaTypes.banner && + request.mediaTypes.banner.sizes) { + sizes = request.mediaTypes.banner.sizes; + } + return { + w: sizes[0][0], + h: sizes[0][1] + }; +} + +registerBidder(spec); diff --git a/modules/zetaBidAdapter.md b/modules/zetaBidAdapter.md new file mode 100644 index 00000000000..89a9767d29a --- /dev/null +++ b/modules/zetaBidAdapter.md @@ -0,0 +1,45 @@ +# Overview + +``` +Module Name: Zeta Bidder Adapter +Module Type: Bidder Adapter +Maintainer: DL-ZetaDSP-Supply-Engineering@zetaglobal.com +``` + +# Description + +Module that connects to Zeta's demand sources + +# Test Parameters +``` + var adUnits = [ + { + mediaTypes: { + banner: { + sizes: [[300, 250]], // a display size + } + }, + bids: [ + { + bidder: 'zeta_global', + bidId: 12345, + params: { + placement: 12345, + user: { + uid: 12345, + buyeruid: 12345 + }, + device: { + ip: '111.222.33.44', + geo: { + country: 'USA' + } + }, + definerId: '0', + test: 1 + } + } + ] + } + ]; +``` diff --git a/modules/zetaSspBidAdapter.js b/modules/zetaSspBidAdapter.js new file mode 100644 index 00000000000..450608a82f4 --- /dev/null +++ b/modules/zetaSspBidAdapter.js @@ -0,0 +1,159 @@ +import * as utils from '../src/utils.js'; +import {registerBidder} from '../src/adapters/bidderFactory.js'; +import {BANNER} from '../src/mediaTypes.js'; +import {config} from '../src/config.js'; + +const BIDDER_CODE = 'zeta_global_ssp'; +const ENDPOINT_URL = 'https://ssp.disqus.com/bid'; +const USER_SYNC_URL = 'https://ssp.disqus.com/match'; +const DEFAULT_CUR = 'USD'; +const TTL = 200; +const NET_REV = true; + +export const spec = { + code: BIDDER_CODE, + supportedMediaTypes: [BANNER], + + /** + * Determines whether or not the given bid request is valid. + * + * @param {BidRequest} bid The bid params to validate. + * @return boolean True if this is a valid bid, and false otherwise. + */ + isBidRequestValid: function (bid) { + // check for all required bid fields + if (!(bid && + bid.bidId && + bid.params)) { + utils.logWarn('Invalid bid request - missing required bid data'); + return false; + } + return true; + }, + + /** + * Make a server request from the list of BidRequests. + * + * @param {Bids[]} validBidRequests - an array of bidRequest objects + * @param {BidderRequest} bidderRequest - master bidRequest object + * @return ServerRequest Info describing the request to the server. + */ + buildRequests: function (validBidRequests, bidderRequest) { + const secure = 1; // treat all requests as secure + const request = validBidRequests[0]; + const params = request.params; + let impData = { + id: request.bidId, + secure: secure, + banner: buildBanner(request) + }; + const fpd = config.getLegacyFpd(config.getConfig('ortb2')) || {}; + let payload = { + id: bidderRequest.auctionId, + cur: [DEFAULT_CUR], + imp: [impData], + site: params.site ? params.site : {}, + device: {...fpd.device, ...params.device}, + user: params.user ? params.user : {}, + app: params.app ? params.app : {}, + ext: { + tags: params.tags ? params.tags : {}, + sid: params.sid ? params.sid : {} + } + }; + + 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; + } + if (request.gdprConsent) { + payload.regs = { + ext: { + gdpr: request.gdprConsent.gdprApplies === true ? 1 : 0 + } + }; + } + if (request.gdprConsent && request.gdprConsent.gdprApplies) { + payload.user = { + ext: { + consent: request.gdprConsent.consentString + } + }; + } + return { + method: 'POST', + url: ENDPOINT_URL, + data: JSON.stringify(payload), + }; + }, + + /** + * Unpack the response from the server into a list of bids. + * + * @param {ServerResponse} serverResponse A successful response from the server. + * @param bidRequest The payload from the server's response. + * @return {Bid[]} An array of bids which were nested inside the server. + */ + interpretResponse: function (serverResponse, bidRequest) { + let bidResponse = []; + if (Object.keys(serverResponse.body).length !== 0) { + let zetaResponse = serverResponse.body; + let zetaBid = zetaResponse.seatbid[0].bid[0]; + let bid = { + requestId: zetaBid.impid, + cpm: zetaBid.price, + currency: zetaResponse.cur, + width: zetaBid.w, + height: zetaBid.h, + ad: zetaBid.adm, + ttl: TTL, + creativeId: zetaBid.crid, + netRevenue: NET_REV, + }; + if (zetaBid.adomain && zetaBid.adomain.length) { + bid.meta = {}; + bid.meta.advertiserDomains = zetaBid.adomain; + } + bidResponse.push(bid); + } + return bidResponse; + }, + + /** + * Register the user sync pixels which should be dropped after the auction. + * + * @param {SyncOptions} syncOptions Which user syncs are allowed? + * @param {ServerResponse[]} serverResponses List of server's responses. + * @param gdprConsent The GDPR consent parameters + * @param uspConsent The USP consent parameters + * @return {UserSync[]} The user syncs which should be dropped. + */ + getUserSyncs: function (syncOptions, serverResponses, gdprConsent, uspConsent) { + const syncs = []; + if (syncOptions.iframeEnabled) { + syncs.push({ + type: 'iframe', + url: USER_SYNC_URL + }); + } + return syncs; + } +} + +function buildBanner(request) { + let sizes = request.sizes; + if (request.mediaTypes && + request.mediaTypes.banner && + request.mediaTypes.banner.sizes) { + sizes = request.mediaTypes.banner.sizes; + } + return { + w: sizes[0][0], + h: sizes[0][1] + }; +} + +registerBidder(spec); diff --git a/modules/zetaSspBidAdapter.md b/modules/zetaSspBidAdapter.md new file mode 100644 index 00000000000..d2950bce6b9 --- /dev/null +++ b/modules/zetaSspBidAdapter.md @@ -0,0 +1,42 @@ +# Overview + +``` +Module Name: Zeta Ssp Bidder Adapter +Module Type: Bidder Adapter +Maintainer: miakovlev@zetaglobal.com +``` + +# Description + +Module that connects to Zeta's SSP + +# Test Parameters +``` + var adUnits = [ + { + mediaTypes: { + banner: { + sizes: [[300, 250]], // a display size + } + }, + bids: [ + { + bidder: 'zeta_global_ssp', + bidId: 12345, + params: { + placement: 12345, + user: { + uid: 12345, + buyeruid: 12345 + }, + tags: { + someTag: 123, + sid: 'publisherId' + }, + test: 1 + } + } + ] + } + ]; +``` diff --git a/package-lock.json b/package-lock.json index 4f3d2120d72..3c4e59cdba7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "prebid.js", - "version": "3.27.0-pre", + "version": "4.39.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", @@ -7796,13 +7787,24 @@ "function-bind": "^1.1.1", "has": "^1.0.3", "has-symbols": "^1.0.1", - "is-callable": "^1.2.0", - "is-regex": "^1.1.0", + "is-callable": "^1.1.5", + "is-regex": "^1.0.5", "object-inspect": "^1.7.0", "object-keys": "^1.1.1", "object.assign": "^4.1.0", - "string.prototype.trimend": "^1.0.1", - "string.prototype.trimstart": "^1.0.1" + "string.prototype.trimleft": "^2.1.1", + "string.prototype.trimright": "^2.1.1" + }, + "dependencies": { + "is-regex": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.1.tgz", + "integrity": "sha512-1+QkEcxiLlB7VEyFtyBg94e08OAsvq7FUBgApTq/w2ymCLyKJgDPsybBENVtA7XCQEgEXxKPonG+mvYRxh/LIg==", + "dev": true, + "requires": { + "has-symbols": "^1.0.1" + } + } } }, "es-array-method-boxes-properly": { @@ -7838,6 +7840,7 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", + "dev": true, "requires": { "is-callable": "^1.1.4", "is-date-object": "^1.0.1", @@ -11109,9 +11112,9 @@ } }, "handlebars": { - "version": "4.7.6", - "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.6.tgz", - "integrity": "sha512-1f2BACcBfiwAfStCKZNrUCgqNZkGsAT7UM3kkYtXuLo0KnaVfjKOyf7PRzB6++aK9STyT1Pd2ZCPe3EGOXleXA==", + "version": "4.7.7", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.7.tgz", + "integrity": "sha512-aAcXm5OAfE/8IXkcZvCepKU3VzW1/39Fb5ZuqMtgI/hT8X2YgoMvBY5dLhq/cpOvw7Lk1nK/UF71aLG/ZnVYRA==", "dev": true, "requires": { "minimist": "^1.2.5", @@ -11434,9 +11437,9 @@ "dev": true }, "hosted-git-info": { - "version": "2.8.8", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.8.tgz", - "integrity": "sha512-f/wzC2QaWBs7t9IYqB4T3sR1xviIViXJRJTWBlx2Gf3g0Xi5vI7Yy4koXQ1c9OYDGHN9sBy1DQ2AB8fqZBWhUg==", + "version": "2.8.9", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", + "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==", "dev": true }, "html-encoding-sniffer": { @@ -12019,6 +12022,12 @@ "integrity": "sha1-aRC8pdqMleeEtXUbl2z1oQ/uNtI=", "dev": true }, + "is-negative-zero": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.0.tgz", + "integrity": "sha1-lVOxIbD6wohp2p7UWeIMdUN4hGE=", + "dev": true + }, "is-number": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", @@ -12076,7 +12085,7 @@ "integrity": "sha512-vlKW17SNq44owv5AQR3Cq0bQPEb8+kF3UKZ2fiZNOWtztYE5i0CzCZxFDwO58qAOWtxdBRVO/V5Qin1wjCqFYQ==", "dev": true, "requires": { - "has-symbols": "^1.0.1" + "has": "^1.0.3" } }, "is-relative": { @@ -15709,13 +15718,10 @@ "dev": true }, "live-connect-js": { - "version": "1.1.10", - "resolved": "https://registry.npmjs.org/live-connect-js/-/live-connect-js-1.1.10.tgz", - "integrity": "sha512-G/LJKN3b21DZILCQRyataC/znLvJRyogtu7mAkKlkhP9B9UJ8bcOL7ihW/clD2PsT4hVUkeabHhUGsPCmhsjFw==", + "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" } }, @@ -15765,9 +15771,9 @@ } }, "lodash": { - "version": "4.17.19", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.19.tgz", - "integrity": "sha512-JNvd8XER9GQX0v2qJgsaN/mzFCNA5BRe/j8JN9d+tWyGLSodKQHKFicdwNYzWwI3wjRnaKPsGj1XkBjx/F96DQ==", + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", "dev": true }, "lodash._basecopy": { @@ -17484,7 +17490,8 @@ "object-inspect": { "version": "1.8.0", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.8.0.tgz", - "integrity": "sha512-jLdtEOB112fORuypAyl/50VRVIBIdVQOSUUGQHzJ4xBSbit81zRarz7GThkEFZy1RceYrWYcPcBFPQwHyAc1gA==" + "integrity": "sha512-jLdtEOB112fORuypAyl/50VRVIBIdVQOSUUGQHzJ4xBSbit81zRarz7GThkEFZy1RceYrWYcPcBFPQwHyAc1gA==", + "dev": true }, "object-is": { "version": "1.1.2", @@ -18765,9 +18772,9 @@ "dev": true }, "querystringify": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.1.1.tgz", - "integrity": "sha512-w7fLxIRCRT7U8Qu53jQnJyPkYZIaR4n5151KMfcJlO/A9397Wxb1amJvROTK6TOnp7PfoAmg/qXiNHI+08jRfA==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", + "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==", "dev": true }, "quick-lru": { @@ -20697,23 +20704,143 @@ } }, "string.prototype.trimend": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.1.tgz", - "integrity": "sha512-LRPxFUaTtpqYsTeNKaFOw3R4bxIzWOnbQ837QfBylo8jIxtcbK/A/sMV7Q+OAV/vWo+7s25pOE10KYSjaSO06g==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.2.tgz", + "integrity": "sha512-8oAG/hi14Z4nOVP0z6mdiVZ/wqjDtWSLygMigTzAb+7aPEDTleeFf+WrF+alzecxIRkckkJVn+dTlwzJXORATw==", "dev": true, "requires": { "define-properties": "^1.1.3", - "es-abstract": "^1.17.5" + "es-abstract": "^1.18.0-next.1" + }, + "dependencies": { + "es-abstract": { + "version": "1.18.0-next.1", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.18.0-next.1.tgz", + "integrity": "sha512-I4UGspA0wpZXWENrdA0uHbnhte683t3qT/1VFH9aX2dA5PPSf6QW5HHXf5HImaqPmjXaVeVk4RGWnaylmV7uAA==", + "dev": true, + "requires": { + "es-to-primitive": "^1.2.1", + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.1", + "is-callable": "^1.2.2", + "is-negative-zero": "^2.0.0", + "is-regex": "^1.1.1", + "object-inspect": "^1.8.0", + "object-keys": "^1.1.1", + "object.assign": "^4.1.1", + "string.prototype.trimend": "^1.0.1", + "string.prototype.trimstart": "^1.0.1" + } + }, + "is-callable": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.2.tgz", + "integrity": "sha512-dnMqspv5nU3LoewK2N/y7KLtxtakvTuaCsU9FU50/QDmdbHNy/4/JuRtMHqRU22o3q+W89YQndQEeCVwK+3qrA==", + "dev": true + }, + "is-regex": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.1.tgz", + "integrity": "sha512-1+QkEcxiLlB7VEyFtyBg94e08OAsvq7FUBgApTq/w2ymCLyKJgDPsybBENVtA7XCQEgEXxKPonG+mvYRxh/LIg==", + "dev": true, + "requires": { + "has-symbols": "^1.0.1" + } + }, + "object.assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.1.tgz", + "integrity": "sha512-VT/cxmx5yaoHSOTSyrCygIDFco+RsibY2NM0a4RdEeY/4KgqezwFtK1yr3U67xYhqJSlASm2pKhLVzPj2lr4bA==", + "dev": true, + "requires": { + "define-properties": "^1.1.3", + "es-abstract": "^1.18.0-next.0", + "has-symbols": "^1.0.1", + "object-keys": "^1.1.1" + } + } + } + }, + "string.prototype.trimleft": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/string.prototype.trimleft/-/string.prototype.trimleft-2.1.2.tgz", + "integrity": "sha512-gCA0tza1JBvqr3bfAIFJGqfdRTyPae82+KTnm3coDXkZN9wnuW3HjGgN386D7hfv5CHQYCI022/rJPVlqXyHSw==", + "dev": true, + "requires": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.5", + "string.prototype.trimstart": "^1.0.0" + } + }, + "string.prototype.trimright": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/string.prototype.trimright/-/string.prototype.trimright-2.1.2.tgz", + "integrity": "sha512-ZNRQ7sY3KroTaYjRS6EbNiiHrOkjihL9aQE/8gfQ4DtAC/aEBRHFJa44OmoWxGGqXuJlfKkZW4WcXErGr+9ZFg==", + "dev": true, + "requires": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.5", + "string.prototype.trimend": "^1.0.0" } }, "string.prototype.trimstart": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.1.tgz", - "integrity": "sha512-XxZn+QpvrBI1FOcg6dIpxUPgWCPuNXvMD72aaRaUQv1eD4e/Qy8i/hFTe0BUmD60p/QA6bh1avmuPTfNjqVWRw==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.2.tgz", + "integrity": "sha512-7F6CdBTl5zyu30BJFdzSTlSlLPwODC23Od+iLoVH8X6+3fvDPPuBVVj9iaB1GOsSTSIgVfsfm27R2FGrAPznWg==", "dev": true, "requires": { "define-properties": "^1.1.3", - "es-abstract": "^1.17.5" + "es-abstract": "^1.18.0-next.1" + }, + "dependencies": { + "es-abstract": { + "version": "1.18.0-next.1", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.18.0-next.1.tgz", + "integrity": "sha512-I4UGspA0wpZXWENrdA0uHbnhte683t3qT/1VFH9aX2dA5PPSf6QW5HHXf5HImaqPmjXaVeVk4RGWnaylmV7uAA==", + "dev": true, + "requires": { + "es-to-primitive": "^1.2.1", + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.1", + "is-callable": "^1.2.2", + "is-negative-zero": "^2.0.0", + "is-regex": "^1.1.1", + "object-inspect": "^1.8.0", + "object-keys": "^1.1.1", + "object.assign": "^4.1.1", + "string.prototype.trimend": "^1.0.1", + "string.prototype.trimstart": "^1.0.1" + } + }, + "is-callable": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.2.tgz", + "integrity": "sha512-dnMqspv5nU3LoewK2N/y7KLtxtakvTuaCsU9FU50/QDmdbHNy/4/JuRtMHqRU22o3q+W89YQndQEeCVwK+3qrA==", + "dev": true + }, + "is-regex": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.1.tgz", + "integrity": "sha512-1+QkEcxiLlB7VEyFtyBg94e08OAsvq7FUBgApTq/w2ymCLyKJgDPsybBENVtA7XCQEgEXxKPonG+mvYRxh/LIg==", + "dev": true, + "requires": { + "has-symbols": "^1.0.1" + } + }, + "object.assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.1.tgz", + "integrity": "sha512-VT/cxmx5yaoHSOTSyrCygIDFco+RsibY2NM0a4RdEeY/4KgqezwFtK1yr3U67xYhqJSlASm2pKhLVzPj2lr4bA==", + "dev": true, + "requires": { + "define-properties": "^1.1.3", + "es-abstract": "^1.18.0-next.0", + "has-symbols": "^1.0.1", + "object-keys": "^1.1.1" + } + } } }, "string_decoder": { @@ -21876,9 +22003,9 @@ "dev": true }, "url-parse": { - "version": "1.4.7", - "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.4.7.tgz", - "integrity": "sha512-d3uaVyzDB9tQoSXFvuSUNFibTd9zxd2bkVrDRvF5TmvWWQwqE4lgYJ5m+x1DbecWkw+LK4RNl2CU1hHuOKPVlg==", + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.0.tgz", + "integrity": "sha512-9iT6N4s93SMfzunOyDPe4vo4nLcSu1yq0IQK1gURmjm8tQNlM6loiuCRrKG1hHGXfB2EWd6H4cGi7tGdaygMFw==", "dev": true, "requires": { "querystringify": "^2.1.1", diff --git a/package.json b/package.json index 3cb04dc6f8a..f7176162f8e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "prebid.js", - "version": "4.4.0-pre", + "version": "4.41.0-pre", "description": "Header Bidding Management Library", "main": "src/prebid.js", "scripts": { @@ -86,7 +86,7 @@ "karma-sourcemap-loader": "^0.3.7", "karma-spec-reporter": "^0.0.31", "karma-webpack": "^3.0.5", - "lodash": "^4.17.4", + "lodash": "^4.17.21", "mocha": "^5.0.0", "morgan": "^1.10.0", "opn": "^5.4.0", @@ -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.10" + "live-connect-js": "2.0.0" } } diff --git a/src/AnalyticsAdapter.js b/src/AnalyticsAdapter.js index f3297412a35..80c12a3eb8e 100644 --- a/src/AnalyticsAdapter.js +++ b/src/AnalyticsAdapter.js @@ -18,6 +18,7 @@ const { BIDDER_DONE, SET_TARGETING, AD_RENDER_FAILED, + AUCTION_DEBUG, ADD_AD_UNITS } } = CONSTANTS; @@ -112,6 +113,7 @@ export default function AnalyticsAdapter({ url, analyticsType, global, handler } [SET_TARGETING]: args => this.enqueue({ eventType: SET_TARGETING, args }), [AUCTION_END]: args => this.enqueue({ eventType: AUCTION_END, args }), [AD_RENDER_FAILED]: args => this.enqueue({ eventType: AD_RENDER_FAILED, args }), + [AUCTION_DEBUG]: args => this.enqueue({ eventType: AUCTION_DEBUG, args }), [ADD_AD_UNITS]: args => this.enqueue({ eventType: ADD_AD_UNITS, args }), [AUCTION_INIT]: args => { args.config = typeof config === 'object' ? config.options || {} : {}; // enableAnaltyics configuration object diff --git a/src/Renderer.js b/src/Renderer.js index 85bcbb383e8..c997658b30b 100644 --- a/src/Renderer.js +++ b/src/Renderer.js @@ -39,17 +39,22 @@ export function Renderer(options) { // use a function, not an arrow, in order to be able to pass "arguments" through this.render = function () { - if (!isRendererDefinedOnAdUnit(adUnitCode)) { + const renderArgs = arguments + const runRender = () => { + if (this._render) { + this._render.apply(this, renderArgs) + } else { + utils.logWarn(`No render function was provided, please use .setRender on the renderer`); + } + } + + if (!isRendererPreferredFromAdUnit(adUnitCode)) { // we expect to load a renderer url once only so cache the request to load script + this.cmd.unshift(runRender) // should render run first ? loadExternalScript(url, moduleCode, this.callback); } else { utils.logWarn(`External Js not loaded by Renderer since renderer url and callback is already defined on adUnit ${adUnitCode}`); - } - - if (this._render) { - this._render.apply(this, arguments) // _render is expected to use push as appropriate - } else { - utils.logWarn(`No render function was provided, please use .setRender on the renderer`); + runRender() } }.bind(this) // bind the function to this object to avoid 'this' errors } @@ -110,10 +115,26 @@ export function executeRenderer(renderer, bid) { renderer.render(bid); } -function isRendererDefinedOnAdUnit(adUnitCode) { +function isRendererPreferredFromAdUnit(adUnitCode) { const adUnits = $$PREBID_GLOBAL$$.adUnits; const adUnit = find(adUnits, adUnit => { return adUnit.code === adUnitCode; }); - return !!(adUnit && adUnit.renderer && adUnit.renderer.url && adUnit.renderer.render); + + if (!adUnit) { + return false + } + + // renderer defined at adUnit level + const adUnitRenderer = utils.deepAccess(adUnit, 'renderer'); + const hasValidAdUnitRenderer = !!(adUnitRenderer && adUnitRenderer.url && adUnitRenderer.render); + + // renderer defined at adUnit.mediaTypes level + const mediaTypeRenderer = utils.deepAccess(adUnit, 'mediaTypes.video.renderer'); + const hasValidMediaTypeRenderer = !!(mediaTypeRenderer && mediaTypeRenderer.url && mediaTypeRenderer.render) + + return !!( + (hasValidAdUnitRenderer && !(adUnitRenderer.backupOnly === true)) || + (hasValidMediaTypeRenderer && !(mediaTypeRenderer.backupOnly === true)) + ); } diff --git a/src/adapterManager.js b/src/adapterManager.js index 5124ae99694..5f8f2e5721c 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 = {}; @@ -66,7 +68,7 @@ function getBids({bidderCode, auctionId, bidderRequestId, adUnits, labels, src}) } bid = Object.assign({}, bid, getDefinedParams(adUnit, [ - 'fpd', + 'ortb2Imp', 'mediaType', 'renderer', 'storedAuctionResponse' @@ -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,27 @@ export let uspDataHandler = { } }; +export let coppaDataHandler = { + getCoppa: function() { + return !!(config.getConfig('coppa')) + } +}; + +// 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 +207,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 +214,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); + } + }) + } } + }) + + // don't call these client side (unless client request is needed for testing) + clientBidderCodes = bidderCodes.filter(bidderCode => { + return !includes(allS2SBidders, bidderCode) || includes(clientTestAdapters, bidderCode) + }); - 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); + // 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,64 +343,84 @@ 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 => { bidRequest.start = timestamp(); // TODO : Do we check for bid in pool from here and skip calling adapter again ? const adapter = _bidderRegistry[bidRequest.bidderCode]; - utils.logMessage(`CALLING BIDDER ======= ${bidRequest.bidderCode}`); - events.emit(CONSTANTS.EVENTS.BID_REQUESTED, bidRequest); + config.runWithBidder(bidRequest.bidderCode, () => { + utils.logMessage(`CALLING BIDDER`); + events.emit(CONSTANTS.EVENTS.BID_REQUESTED, bidRequest); + }); let ajax = ajaxBuilder(requestBidsTimeout, requestCallbacks ? { request: requestCallbacks.request.bind(null, bidRequest.bidderCode), done: requestCallbacks.done @@ -390,27 +447,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 +479,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 +487,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 +614,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..c71c4ee355b 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 })); }); } } @@ -195,8 +197,10 @@ export function newBidder(spec) { const responses = []; function afterAllResponses() { done(); - events.emit(CONSTANTS.EVENTS.BIDDER_DONE, bidderRequest); - registerSyncs(responses, bidderRequest.gdprConsent, bidderRequest.uspConsent); + config.runWithBidder(spec.code, () => { + events.emit(CONSTANTS.EVENTS.BIDDER_DONE, bidderRequest); + registerSyncs(responses, bidderRequest.gdprConsent, bidderRequest.uspConsent); + }); } const validBidRequests = bidderRequest.bids.filter(filterAndWarn); diff --git a/src/adloader.js b/src/adloader.js index 1c18ce82b16..5460cc79410 100644 --- a/src/adloader.js +++ b/src/adloader.js @@ -4,6 +4,7 @@ import * as utils from './utils.js'; const _requestCache = {}; // The below list contains modules or vendors whom Prebid allows to load external JS. const _approvedLoadExternalJSList = [ + 'adloox', 'criteo', 'outstream', 'adagio', diff --git a/src/auction.js b/src/auction.js index b82b4752479..a6f0342e582 100644 --- a/src/auction.js +++ b/src/auction.js @@ -66,6 +66,7 @@ import { config } from './config.js'; import { userSync } from './userSync.js'; import { hook } from './hook.js'; import find from 'core-js-pure/features/array/find.js'; +import includes from 'core-js-pure/features/array/includes.js'; import { OUTSTREAM } from './video.js'; import { VIDEO } from './mediaTypes.js'; @@ -196,6 +197,7 @@ export function newAuction({adUnits, adUnitCodes, callback, cbTimeout, labels, a } function auctionDone() { + config.resetBidder(); // when all bidders have called done callback atleast once it means auction is complete utils.logInfo(`Bids Received for Auction with id: ${_auctionId}`, _bidsReceived); _auctionStatus = AUCTION_COMPLETED; @@ -397,10 +399,19 @@ export function auctionCallbacks(auctionDone, auctionInstance) { function adapterDone() { let bidderRequest = this; + let bidderRequests = auctionInstance.getBidRequests(); + const auctionOptionsConfig = config.getConfig('auctionOptions'); bidderRequestsDone.add(bidderRequest); - allAdapterCalledDone = auctionInstance.getBidRequests() - .every(bidderRequest => bidderRequestsDone.has(bidderRequest)); + + if (auctionOptionsConfig && !utils.isEmpty(auctionOptionsConfig)) { + const secondaryBidders = auctionOptionsConfig.secondaryBidders; + if (secondaryBidders && !bidderRequests.every(bidder => includes(secondaryBidders, bidder.bidderCode))) { + bidderRequests = bidderRequests.filter(request => !includes(secondaryBidders, request.bidderCode)); + } + } + + allAdapterCalledDone = bidderRequests.every(bidderRequest => bidderRequestsDone.has(bidderRequest)); bidderRequest.bids.forEach(bid => { if (!bidResponseMap[bid.bidId]) { @@ -442,13 +453,13 @@ export function addBidToAuction(auctionInstance, bidResponse) { function tryAddVideoBid(auctionInstance, bidResponse, bidRequests, afterBidAdded) { let addBid = true; - const bidderRequest = getBidRequest(bidResponse.requestId, [bidRequests]); + const bidderRequest = getBidRequest(bidResponse.originalRequestId || bidResponse.requestId, [bidRequests]); const videoMediaType = bidderRequest && deepAccess(bidderRequest, 'mediaTypes.video'); 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) { @@ -509,12 +520,29 @@ function getPreparedBidForAuction({adUnitCode, bid, bidderRequest, auctionId}) { events.emit(CONSTANTS.EVENTS.BID_ADJUSTMENT, bidObject); // a publisher-defined renderer can be used to render bids - const bidReq = bidderRequest.bids && find(bidderRequest.bids, bid => bid.adUnitCode == adUnitCode); + const bidReq = bidderRequest.bids && find(bidderRequest.bids, bid => bid.adUnitCode == adUnitCode && bid.bidId == bidObject.requestId); const adUnitRenderer = bidReq && bidReq.renderer; - if (adUnitRenderer && adUnitRenderer.url) { - bidObject.renderer = Renderer.install({ url: adUnitRenderer.url }); - bidObject.renderer.setRender(adUnitRenderer.render); + // a publisher can also define a renderer for a mediaType + const bidObjectMediaType = bidObject.mediaType; + const bidMediaType = bidReq && + bidReq.mediaTypes && + bidReq.mediaTypes[bidObjectMediaType]; + + var mediaTypeRenderer = bidMediaType && bidMediaType.renderer; + + var renderer = null; + + // the renderer for the mediaType takes precendence + if (mediaTypeRenderer && mediaTypeRenderer.url && mediaTypeRenderer.render && !(mediaTypeRenderer.backupOnly === true && bid.renderer)) { + renderer = mediaTypeRenderer; + } else if (adUnitRenderer && adUnitRenderer.url && adUnitRenderer.render && !(adUnitRenderer.backupOnly === true && bid.renderer)) { + renderer = adUnitRenderer; + } + + if (renderer) { + bidObject.renderer = Renderer.install({ url: renderer.url }); + bidObject.renderer.setRender(renderer.render); } // Use the config value 'mediaTypeGranularity' if it has been defined for mediaType, else use 'customPriceBucket' @@ -599,6 +627,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 @@ -635,6 +673,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/auctionManager.js b/src/auctionManager.js index 3d4bd0afe99..1aca1277aa8 100644 --- a/src/auctionManager.js +++ b/src/auctionManager.js @@ -9,6 +9,7 @@ * * @property {function(): Array} getBidsRequested - returns consolidated bid requests * @property {function(): Array} getBidsReceived - returns consolidated bid received + * @property {function(): Array} getAllBidsForAdUnitCode - returns consolidated bid received for a given adUnit * @property {function(): Array} getAdUnits - returns consolidated adUnits * @property {function(): Array} getAdUnitCodes - returns consolidated adUnitCodes * @property {function(): Object} createAuction - creates auction instance and stores it for future reference @@ -66,6 +67,13 @@ export function newAuctionManager() { .filter(bid => bid); }; + auctionManager.getAllBidsForAdUnitCode = function(adUnitCode) { + return _auctions.map((auction) => { + return auction.getBidsReceived(); + }).reduce(flatten, []) + .filter(bid => bid && bid.adUnitCode === adUnitCode) + }; + auctionManager.getAdUnits = function() { return _auctions.map(auction => auction.getAdUnits()) .reduce(flatten, []); diff --git a/src/config.js b/src/config.js index 3284be52296..7cb1a81955d 100644 --- a/src/config.js +++ b/src/config.js @@ -29,6 +29,7 @@ const DEFAULT_ENABLE_SEND_ALL_BIDS = true; const DEFAULT_DISABLE_AJAX_TIMEOUT = false; const DEFAULT_BID_CACHE = false; const DEFAULT_DEVICE_ACCESS = true; +const DEFAULT_MAX_NESTED_IFRAMES = 10; const DEFAULT_TIMEOUTBUFFER = 400; @@ -199,6 +200,25 @@ export function newConfig() { set disableAjaxTimeout(val) { this._disableAjaxTimeout = val; }, + + // default max nested iframes for referer detection + _maxNestedIframes: DEFAULT_MAX_NESTED_IFRAMES, + get maxNestedIframes() { + return this._maxNestedIframes; + }, + set maxNestedIframes(val) { + this._maxNestedIframes = val; + }, + + _auctionOptions: {}, + get auctionOptions() { + return this._auctionOptions; + }, + set auctionOptions(val) { + if (validateauctionOptions(val)) { + this._auctionOptions = val; + } + }, }; if (config) { @@ -237,6 +257,30 @@ export function newConfig() { } return true; } + + function validateauctionOptions(val) { + if (!utils.isPlainObject(val)) { + utils.logWarn('Auction Options must be an object') + return false + } + + for (let k of Object.keys(val)) { + if (k !== 'secondaryBidders') { + utils.logWarn(`Auction Options given an incorrect param: ${k}`) + return false + } + if (k === 'secondaryBidders') { + if (!utils.isArray(val[k])) { + utils.logWarn(`Auction Options ${k} must be of type Array`); + return false + } else if (!val[k].every(utils.isStr)) { + utils.logWarn(`Auction Options ${k} must be only string`); + return false + } + } + } + return true; + } } /** @@ -290,6 +334,119 @@ export function newConfig() { return bidderConfig; } + /** + * Returns backwards compatible FPD data for modules + */ + function getLegacyFpd(obj) { + if (typeof obj !== 'object') return; + + let duplicate = {}; + + Object.keys(obj).forEach((type) => { + let prop = (type === 'site') ? 'context' : type; + duplicate[prop] = (prop === 'context' || prop === 'user') ? Object.keys(obj[type]).filter(key => key !== 'data').reduce((result, key) => { + if (key === 'ext') { + utils.mergeDeep(result, obj[type][key]); + } else { + utils.mergeDeep(result, {[key]: obj[type][key]}); + } + + return result; + }, {}) : obj[type]; + }); + + return duplicate; + } + + /** + * Returns backwards compatible FPD data for modules + */ + function getLegacyImpFpd(obj) { + if (typeof obj !== 'object') return; + + let duplicate = {}; + + if (utils.deepAccess(obj, 'ext.data')) { + Object.keys(obj.ext.data).forEach((key) => { + if (key === 'pbadslot') { + utils.mergeDeep(duplicate, {context: {pbAdSlot: obj.ext.data[key]}}); + } else if (key === 'adserver') { + utils.mergeDeep(duplicate, {context: {adServer: obj.ext.data[key]}}); + } else { + utils.mergeDeep(duplicate, {context: {data: {[key]: obj.ext.data[key]}}}); + } + }); + } + + return duplicate; + } + + /** + * Copy FPD over to OpenRTB standard format in config + */ + function convertFpd(opt) { + let duplicate = {}; + + Object.keys(opt).forEach((type) => { + let prop = (type === 'context') ? 'site' : type; + duplicate[prop] = (prop === 'site' || prop === 'user') ? Object.keys(opt[type]).reduce((result, key) => { + if (key === 'data') { + utils.mergeDeep(result, {ext: {data: opt[type][key]}}); + } else { + utils.mergeDeep(result, {[key]: opt[type][key]}); + } + + return result; + }, {}) : opt[type]; + }); + + return duplicate; + } + + /** + * Copy Impression FPD over to OpenRTB standard format in config + * Only accepts bid level context.data values with pbAdSlot and adServer exceptions + */ + function convertImpFpd(opt) { + let duplicate = {}; + + Object.keys(opt).filter(prop => prop === 'context').forEach((type) => { + Object.keys(opt[type]).forEach((key) => { + if (key === 'data') { + utils.mergeDeep(duplicate, {ext: {data: opt[type][key]}}); + } else { + if (typeof opt[type][key] === 'object' && !Array.isArray(opt[type][key])) { + Object.keys(opt[type][key]).forEach(data => { + utils.mergeDeep(duplicate, {ext: {data: {[key.toLowerCase()]: {[data.toLowerCase()]: opt[type][key][data]}}}}); + }); + } else { + utils.mergeDeep(duplicate, {ext: {data: {[key.toLowerCase()]: opt[type][key]}}}); + } + } + }); + }); + + return duplicate; + } + + /** + * Copy FPD over to OpenRTB standard format in each adunit + */ + function convertAdUnitFpd(arr) { + let convert = []; + + arr.forEach((adunit) => { + if (adunit.fpd) { + (adunit['ortb2Imp']) ? utils.mergeDeep(adunit['ortb2Imp'], convertImpFpd(adunit.fpd)) : adunit['ortb2Imp'] = convertImpFpd(adunit.fpd); + convert.push((({ fpd, ...duplicate }) => duplicate)(adunit)); + } else { + convert.push(adunit); + } + }); + + return convert; + } + /* * Sets configuration given an object containing key-value pairs and calls * listeners that were added by the `subscribe` function @@ -304,13 +461,14 @@ export function newConfig() { let topicalConfig = {}; topics.forEach(topic => { - let option = options[topic]; + let prop = (topic === 'fpd') ? 'ortb2' : topic; + let option = (topic === 'fpd') ? convertFpd(options[topic]) : options[topic]; - if (utils.isPlainObject(defaults[topic]) && utils.isPlainObject(option)) { - option = Object.assign({}, defaults[topic], option); + if (utils.isPlainObject(defaults[prop]) && utils.isPlainObject(option)) { + option = Object.assign({}, defaults[prop], option); } - topicalConfig[topic] = config[topic] = option; + topicalConfig[prop] = config[prop] = option; }); callSubscribers(topicalConfig); @@ -403,11 +561,13 @@ export function newConfig() { bidderConfig[bidder] = {}; } Object.keys(config.config).forEach(topic => { - let option = config.config[topic]; + let prop = (topic === 'fpd') ? 'ortb2' : topic; + let option = (topic === 'fpd') ? convertFpd(config.config[topic]) : config.config[topic]; + if (utils.isPlainObject(option)) { - bidderConfig[bidder][topic] = Object.assign({}, bidderConfig[bidder][topic] || {}, option); + bidderConfig[bidder][prop] = Object.assign({}, bidderConfig[bidder][prop] || {}, option); } else { - bidderConfig[bidder][topic] = option; + bidderConfig[bidder][prop] = option; } }); }); @@ -435,7 +595,7 @@ export function newConfig() { try { return fn(); } finally { - currBidder = null; + resetBidder(); } } function callbackWithBidder(bidder) { @@ -454,10 +614,15 @@ export function newConfig() { return currBidder; } + function resetBidder() { + currBidder = null; + } + resetConfig(); return { getCurrentBidder, + resetBidder, getConfig, setConfig, setDefaults, @@ -465,7 +630,10 @@ export function newConfig() { runWithBidder, callbackWithBidder, setBidderConfig, - getBidderConfig + getBidderConfig, + convertAdUnitFpd, + getLegacyFpd, + getLegacyImpFpd }; } diff --git a/src/constants.json b/src/constants.json index 1b5feda6a05..e6b9687f911 100644 --- a/src/constants.json +++ b/src/constants.json @@ -37,10 +37,12 @@ "REQUEST_BIDS": "requestBids", "ADD_AD_UNITS": "addAdUnits", "AD_RENDER_FAILED": "adRenderFailed", - "TCF2_ENFORCEMENT": "tcf2Enforcement" + "TCF2_ENFORCEMENT": "tcf2Enforcement", + "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", @@ -58,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", @@ -87,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/events.js b/src/events.js index e7a11635476..8749ddf206b 100644 --- a/src/events.js +++ b/src/events.js @@ -44,7 +44,8 @@ module.exports = (function () { eventsFired.push({ eventType: eventString, args: eventPayload, - id: key + id: key, + elapsedTime: utils.getPerformanceNow(), }); /** Push each specific callback to the `callbacks` array. diff --git a/src/native.js b/src/native.js index e41d7740ffa..7596b38534d 100644 --- a/src/native.js +++ b/src/native.js @@ -77,18 +77,6 @@ export function nativeBidIsValid(bid, bidRequests) { return false; } - if (deepAccess(bid, 'native.image')) { - if (!deepAccess(bid, 'native.image.height') || !deepAccess(bid, 'native.image.width')) { - return false; - } - } - - if (deepAccess(bid, 'native.icon')) { - if (!deepAccess(bid, 'native.icon.height') || !deepAccess(bid, 'native.icon.width')) { - return false; - } - } - const requestedAssets = bidRequest.nativeParams; if (!requestedAssets) { return true; @@ -154,21 +142,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 sendPlaceholder = deepAccess( - bidReq, - `mediaTypes.native.${asset}.sendId` - ); + const flatBidNativeKeys = { ...bid.native, ...bid.native.ext }; + delete flatBidNativeKeys.ext; + + 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 +202,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 +218,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 +258,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 827ba2e2270..2211e8ec2b5 100644 --- a/src/prebid.js +++ b/src/prebid.js @@ -1,17 +1,17 @@ /** @module pbjs */ import { getGlobal } from './prebidGlobal.js'; -import { flatten, uniques, isGptPubadsDefined, adUnitsFilter, isArrayOfNums } from './utils.js'; +import { adUnitsFilter, flatten, getHighestCpm, isArrayOfNums, isGptPubadsDefined, uniques } from './utils.js'; import { listenMessagesFromCreative } from './secureCreatives.js'; import { userSync } from './userSync.js'; import { config } from './config.js'; import { auctionManager } from './auctionManager.js'; -import { targeting } from './targeting.js'; +import { filters, targeting } from './targeting.js'; import { hook } from './hook.js'; import { sessionLoader } from './debugging.js'; import includes from 'core-js-pure/features/array/includes.js'; import { adunitCounter } from './adUnits.js'; -import { isRendererRequired, executeRenderer } from './Renderer.js'; +import { executeRenderer, isRendererRequired } from './Renderer.js'; import { createBid } from './bidfactory.js'; import { storageCallbacks } from './storageManager.js'; @@ -43,6 +43,9 @@ $$PREBID_GLOBAL$$.libLoaded = true; $$PREBID_GLOBAL$$.version = 'v$prebid.version$'; utils.logInfo('Prebid.js v$prebid.version$ loaded'); +// modules list generated from build +$$PREBID_GLOBAL$$.installedModules = ['v$prebid.modulesList$']; + // create adUnit array $$PREBID_GLOBAL$$.adUnits = $$PREBID_GLOBAL$$.adUnits || []; @@ -149,7 +152,14 @@ export const checkAdUnitSetup = hook('sync', function (adUnits) { adUnits.forEach(adUnit => { const mediaTypes = adUnit.mediaTypes; + const bids = adUnit.bids; let validatedBanner, validatedVideo, validatedNative; + + if (!bids || !utils.isArray(bids)) { + utils.logError(`Detected adUnit.code '${adUnit.code}' did not have 'adUnit.bids' defined or 'adUnit.bids' is not an array. Removing adUnit from auction.`); + return; + } + if (!mediaTypes || Object.keys(mediaTypes).length === 0) { utils.logError(`Detected adUnit.code '${adUnit.code}' did not have a 'mediaTypes' object defined. This is a required field for the auction, so this adUnit has been removed.`); return; @@ -199,6 +209,24 @@ $$PREBID_GLOBAL$$.getAdserverTargetingForAdUnitCodeStr = function (adunitCode) { } }; +/** + * This function returns the query string targeting parameters available at this moment for a given ad unit. Note that some bidder's response may not have been received if you call this function too quickly after the requests are sent. + * @param adUnitCode {string} adUnitCode to get the bid responses for + * @alias module:pbjs.getHighestUnusedBidResponseForAdUnitCode + * @returns {Object} returnObj return bid + */ +$$PREBID_GLOBAL$$.getHighestUnusedBidResponseForAdUnitCode = function (adunitCode) { + if (adunitCode) { + const bid = auctionManager.getAllBidsForAdUnitCode(adunitCode) + .filter(filters.isUnusedBid) + .filter(filters.isBidNotExpired) + + return bid.length ? bid.reduce(getHighestCpm) : {} + } else { + utils.logMessage('Need to call getHighestUnusedBidResponseForAdUnitCode with adunitCode'); + } +}; + /** * This function returns the query string targeting parameters available at this moment for a given ad unit. Note that some bidder's response may not have been received if you call this function too quickly after the requests are sent. * @param adUnitCode {string} adUnitCode to get the bid responses for @@ -251,6 +279,18 @@ $$PREBID_GLOBAL$$.getNoBids = function () { return getBids('getNoBids'); }; +/** + * This function returns the bids requests involved in an auction but not bid on or the specified adUnitCode + * @param {string} adUnitCode adUnitCode + * @alias module:pbjs.getNoBidsForAdUnitCode + * @return {Object} bidResponse object + */ + +$$PREBID_GLOBAL$$.getNoBidsForAdUnitCode = function (adUnitCode) { + const bids = auctionManager.getNoBids().filter(bid => bid.adUnitCode === adUnitCode); + return { bids }; +}; + /** * This function returns the bid responses at the given moment. * @alias module:pbjs.getBidResponses @@ -342,7 +382,7 @@ function emitAdRenderFail({ reason, message, bid, id }) { * @param {string} id bid id to locate the ad * @alias module:pbjs.renderAd */ -$$PREBID_GLOBAL$$.renderAd = function (doc, id) { +$$PREBID_GLOBAL$$.renderAd = function (doc, id, options) { utils.logInfo('Invoking $$PREBID_GLOBAL$$.renderAd', arguments); utils.logMessage('Calling renderAd with adId :' + id); @@ -354,6 +394,14 @@ $$PREBID_GLOBAL$$.renderAd = function (doc, id) { // replace macros according to openRTB with price paid = bid.cpm bid.ad = utils.replaceAuctionPrice(bid.ad, bid.cpm); bid.adUrl = utils.replaceAuctionPrice(bid.adUrl, bid.cpm); + + // replacing clickthrough if submitted + if (options && options.clickThrough) { + const { clickThrough } = options; + bid.ad = utils.replaceClickThrough(bid.ad, clickThrough); + bid.adUrl = utils.replaceClickThrough(bid.adUrl, clickThrough); + } + // save winning bids auctionManager.addWinningBid(bid); @@ -457,10 +505,22 @@ $$PREBID_GLOBAL$$.removeAdUnit = function (adUnitCode) { $$PREBID_GLOBAL$$.requestBids = hook('async', function ({ bidsBackHandler, timeout, adUnits, adUnitCodes, labels, auctionId } = {}) { events.emit(REQUEST_BIDS); const cbTimeout = timeout || config.getConfig('bidderTimeout'); - adUnits = adUnits || $$PREBID_GLOBAL$$.adUnits; + adUnits = (adUnits && config.convertAdUnitFpd(utils.isArray(adUnits) ? adUnits : [adUnits])) || $$PREBID_GLOBAL$$.adUnits; 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) { @@ -485,11 +545,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(); @@ -540,6 +596,7 @@ export function executeCallbacks(fn, reqBidsConfigObj) { runAll(storageCallbacks); runAll(enableAnalyticsCallbacks); fn.call(this, reqBidsConfigObj); + function runAll(queue) { var queued; while ((queued = queue.shift())) { @@ -559,11 +616,7 @@ $$PREBID_GLOBAL$$.requestBids.before(executeCallbacks, 49); */ $$PREBID_GLOBAL$$.addAdUnits = function (adUnitArr) { utils.logInfo('Invoking $$PREBID_GLOBAL$$.addAdUnits', arguments); - if (utils.isArray(adUnitArr)) { - $$PREBID_GLOBAL$$.adUnits.push.apply($$PREBID_GLOBAL$$.adUnits, adUnitArr); - } else if (typeof adUnitArr === 'object') { - $$PREBID_GLOBAL$$.adUnits.push(adUnitArr); - } + $$PREBID_GLOBAL$$.adUnits.push.apply($$PREBID_GLOBAL$$.adUnits, config.convertAdUnitFpd(utils.isArray(adUnitArr) ? adUnitArr : [adUnitArr])); // emit event events.emit(ADD_AD_UNITS); }; @@ -614,6 +667,16 @@ $$PREBID_GLOBAL$$.offEvent = function (event, handler, id) { events.off(event, handler, id); }; +/** + * Return a copy of all events emitted + * + * @alias module:pbjs.getEvents + */ +$$PREBID_GLOBAL$$.getEvents = function () { + utils.logInfo('Invoking $$PREBID_GLOBAL$$.getEvents'); + return events.getEvents(); +}; + /* * Wrapper to register bidderAdapter externally (adapterManager.registerBidAdapter()) * @param {Function} bidderAdaptor [description] @@ -730,12 +793,12 @@ $$PREBID_GLOBAL$$.aliasBidder = function (bidderCode, alias, options) { * @property {string} adserverTargeting.hb_adid The ad ID of the creative, as understood by the ad server. * @property {string} adserverTargeting.hb_pb The price paid to show the creative, as logged in the ad server. * @property {string} adserverTargeting.hb_bidder The winning bidder whose ad creative will be served by the ad server. -*/ + */ /** * Get all of the bids that have been rendered. Useful for [troubleshooting your integration](http://prebid.org/dev-docs/prebid-troubleshooting-guide.html). * @return {Array} A list of bids that have been rendered. -*/ + */ $$PREBID_GLOBAL$$.getAllWinningBids = function () { return auctionManager.getAllWinningBids(); }; @@ -767,7 +830,7 @@ $$PREBID_GLOBAL$$.getHighestCpmBids = function (adUnitCode) { * @property {string} adId The id representing the ad we want to mark * * @alias module:pbjs.markWinningBidAsUsed -*/ + */ $$PREBID_GLOBAL$$.markWinningBidAsUsed = function (markBidRequest) { let bids = []; @@ -779,7 +842,7 @@ $$PREBID_GLOBAL$$.markWinningBidAsUsed = function (markBidRequest) { } else if (markBidRequest.adId) { bids = auctionManager.getBidsReceived().filter(bid => bid.adId === markBidRequest.adId); } else { - utils.logWarn('Inproper usage of markWinningBidAsUsed. It\'ll need an adUnitCode and/or adId to function.'); + utils.logWarn('Improper use of markWinningBidAsUsed. It needs an adUnitCode or an adId to function.'); } if (bids.length > 0) { diff --git a/src/refererDetection.js b/src/refererDetection.js index 60198678666..56d1fa43f7b 100644 --- a/src/refererDetection.js +++ b/src/refererDetection.js @@ -3,160 +3,56 @@ * The information that it tries to collect includes: * The detected top url in the nav bar, * Whether it was able to reach the top most window (if for example it was embedded in several iframes), - * The number of iframes it was embedded in if applicable, + * The number of iframes it was embedded in if applicable (by default max ten iframes), * A list of the domains of each embedded window if applicable. * Canonical URL which refers to an HTML link element, with the attribute of rel="canonical", found in the element of your webpage */ +import { config } from './config.js'; import { logWarn } from './utils.js'; +/** + * @param {Window} win Window + * @returns {Function} + */ export function detectReferer(win) { - /** - * Returns number of frames to reach top from current frame where prebid.js sits - * @returns {Array} levels - */ - function getLevels() { - let levels = walkUpWindows(); - let ancestors = getAncestorOrigins(); - - if (ancestors) { - for (let i = 0, l = ancestors.length; i < l; i++) { - levels[i].ancestor = ancestors[i]; - } - } - return levels; - } - /** * This function would return a read-only array of hostnames for all the parent frames. * win.location.ancestorOrigins is only supported in webkit browsers. For non-webkit browsers it will return undefined. + * + * @param {Window} win Window object * @returns {(undefined|Array)} Ancestor origins or undefined */ - function getAncestorOrigins() { + function getAncestorOrigins(win) { try { if (!win.location.ancestorOrigins) { return; } + return win.location.ancestorOrigins; } catch (e) { // Ignore error } } - /** - * This function would try to get referer and urls for all parent frames in case of win.location.ancestorOrigins undefined. - * @param {Array} levels - * @returns {Object} urls for all parent frames and top most detected referer url - */ - function getPubUrlStack(levels) { - let stack = []; - let defUrl = null; - let frameLocation = null; - let prevFrame = null; - let prevRef = null; - let ancestor = null; - let detectedRefererUrl = null; - - let i; - for (i = levels.length - 1; i >= 0; i--) { - try { - frameLocation = levels[i].location; - } catch (e) { - // Ignore error - } - - if (frameLocation) { - stack.push(frameLocation); - if (!detectedRefererUrl) { - detectedRefererUrl = frameLocation; - } - } else if (i !== 0) { - prevFrame = levels[i - 1]; - try { - prevRef = prevFrame.referrer; - ancestor = prevFrame.ancestor; - } catch (e) { - // Ignore error - } - - if (prevRef) { - stack.push(prevRef); - if (!detectedRefererUrl) { - detectedRefererUrl = prevRef; - } - } else if (ancestor) { - stack.push(ancestor); - if (!detectedRefererUrl) { - detectedRefererUrl = ancestor; - } - } else { - stack.push(defUrl); - } - } else { - stack.push(defUrl); - } - } - return { - stack, - detectedRefererUrl - }; - } - /** * This function returns canonical URL which refers to an HTML link element, with the attribute of rel="canonical", found in the element of your webpage + * * @param {Object} doc document + * @returns {string|null} */ function getCanonicalUrl(doc) { try { - let element = doc.querySelector("link[rel='canonical']"); + const element = doc.querySelector("link[rel='canonical']"); + if (element !== null) { return element.href; } } catch (e) { + // Ignore error } - return null; - } - /** - * Walk up to the top of the window to detect origin, number of iframes, ancestor origins and canonical url - */ - function walkUpWindows() { - let acc = []; - let currentWindow; - do { - try { - currentWindow = currentWindow ? currentWindow.parent : win; - try { - let isTop = (currentWindow == win.top); - let refData = { - referrer: currentWindow.document.referrer || null, - location: currentWindow.location.href || null, - isTop - } - if (isTop) { - refData = Object.assign(refData, { - canonicalUrl: getCanonicalUrl(currentWindow.document) - }) - } - acc.push(refData); - } catch (e) { - acc.push({ - referrer: null, - location: null, - isTop: (currentWindow == win.top) - }); - logWarn('Trying to access cross domain iframe. Continuing without referrer and location'); - } - } catch (e) { - acc.push({ - referrer: null, - location: null, - isTop: false - }); - return acc; - } - } while (currentWindow != win.top); - return acc; + return null; } /** @@ -170,31 +66,115 @@ export function detectReferer(win) { */ /** - * Get referer info + * Walk up the windows to get the origin stack and best available referrer, canonical URL, etc. + * * @returns {refererInfo} */ function refererInfo() { - try { - let levels = getLevels(); - let numIframes = levels.length - 1; - let reachedTop = (levels[numIframes].location !== null || - (numIframes > 0 && levels[numIframes - 1].referrer !== null)); - let stackInfo = getPubUrlStack(levels); - let canonicalUrl; - if (levels[levels.length - 1].canonicalUrl) { - canonicalUrl = levels[levels.length - 1].canonicalUrl; + const stack = []; + const ancestors = getAncestorOrigins(win); + const maxNestedIframes = config.getConfig('maxNestedIframes'); + let currentWindow; + let bestReferrer; + let bestCanonicalUrl; + let reachedTop = false; + let level = 0; + let valuesFromAmp = false; + let inAmpFrame = false; + + do { + const previousWindow = currentWindow; + const wasInAmpFrame = inAmpFrame; + let currentLocation; + let crossOrigin = false; + let foundReferrer = null; + + inAmpFrame = false; + currentWindow = currentWindow ? currentWindow.parent : win; + + try { + currentLocation = currentWindow.location.href || null; + } catch (e) { + crossOrigin = true; } - return { - referer: stackInfo.detectedRefererUrl, - reachedTop, - numIframes, - stack: stackInfo.stack, - canonicalUrl - }; - } catch (e) { - // Ignore error - } + if (crossOrigin) { + if (wasInAmpFrame) { + const context = previousWindow.context; + + try { + foundReferrer = context.sourceUrl; + bestReferrer = foundReferrer; + + valuesFromAmp = true; + + if (currentWindow === win.top) { + reachedTop = true; + } + + if (context.canonicalUrl) { + bestCanonicalUrl = context.canonicalUrl; + } + } catch (e) { /* Do nothing */ } + } else { + logWarn('Trying to access cross domain iframe. Continuing without referrer and location'); + + try { + const referrer = previousWindow.document.referrer; + + if (referrer) { + foundReferrer = referrer; + + if (currentWindow === win.top) { + reachedTop = true; + } + } + } catch (e) { /* Do nothing */ } + + if (!foundReferrer && ancestors && ancestors[level - 1]) { + foundReferrer = ancestors[level - 1]; + } + + if (foundReferrer && !valuesFromAmp) { + bestReferrer = foundReferrer; + } + } + } else { + if (currentLocation) { + foundReferrer = currentLocation; + bestReferrer = foundReferrer; + valuesFromAmp = false; + + if (currentWindow === win.top) { + reachedTop = true; + + const canonicalUrl = getCanonicalUrl(currentWindow.document); + + if (canonicalUrl) { + bestCanonicalUrl = canonicalUrl; + } + } + } + + if (currentWindow.context && currentWindow.context.sourceUrl) { + inAmpFrame = true; + } + } + + stack.push(foundReferrer); + level++; + } while (currentWindow !== win.top && level < maxNestedIframes); + + stack.reverse(); + + return { + referer: bestReferrer || null, + reachedTop, + isAmp: valuesFromAmp, + numIframes: level - 1, + stack, + canonicalUrl: bestCanonicalUrl || null + }; } return refererInfo; diff --git a/src/secureCreatives.js b/src/secureCreatives.js index 34de2be275c..def1a9abdbb 100644 --- a/src/secureCreatives.js +++ b/src/secureCreatives.js @@ -4,15 +4,15 @@ */ import events from './events.js'; -import { fireNativeTrackers, getAssetMessage } from './native.js'; -import { EVENTS } from './constants.json'; +import { fireNativeTrackers, getAssetMessage, getAllAssetsMessage } from './native.js'; +import constants from './constants.json'; import { logWarn, replaceAuctionPrice } from './utils.js'; import { auctionManager } from './auctionManager.js'; import find from 'core-js-pure/features/array/find.js'; import { isRendererRequired, executeRenderer } from './Renderer.js'; import includes from 'core-js-pure/features/array/includes.js'; -const BID_WON = EVENTS.BID_WON; +const BID_WON = constants.EVENTS.BID_WON; export function listenMessagesFromCreative() { window.addEventListener('message', receiveMessage, false); @@ -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/storageManager.js b/src/storageManager.js index 0d88a8ccea1..66a0cf68cbf 100644 --- a/src/storageManager.js +++ b/src/storageManager.js @@ -1,4 +1,4 @@ -import { hook } from './hook.js'; +import {hook} from './hook.js'; import * as utils from './utils.js'; import includes from 'core-js-pure/features/array/includes.js'; @@ -110,7 +110,12 @@ export function newStorageManager({gvlid, moduleName, moduleType} = {}) { try { localStorage.setItem('prebid.cookieTest', '1'); return localStorage.getItem('prebid.cookieTest') === '1'; - } catch (error) {} + } catch (error) { + } finally { + try { + localStorage.removeItem('prebid.cookieTest'); + } catch (error) {} + } } return false; } @@ -154,7 +159,7 @@ export function newStorageManager({gvlid, moduleName, moduleType} = {}) { */ const setDataInLocalStorage = function (key, value, done) { let cb = function (result) { - if (result && result.valid) { + if (result && result.valid && hasLocalStorage()) { window.localStorage.setItem(key, value); } } @@ -174,7 +179,7 @@ export function newStorageManager({gvlid, moduleName, moduleType} = {}) { */ const getDataFromLocalStorage = function (key, done) { let cb = function (result) { - if (result && result.valid) { + if (result && result.valid && hasLocalStorage()) { return window.localStorage.getItem(key); } return null; @@ -194,7 +199,7 @@ export function newStorageManager({gvlid, moduleName, moduleType} = {}) { */ const removeDataFromLocalStorage = function (key, done) { let cb = function (result) { - if (result && result.valid) { + if (result && result.valid && hasLocalStorage()) { window.localStorage.removeItem(key); } } diff --git a/src/targeting.js b/src/targeting.js index 5873eeeb559..365453e1e8f 100644 --- a/src/targeting.js +++ b/src/targeting.js @@ -4,7 +4,9 @@ import { NATIVE_TARGETING_KEYS } from './native.js'; import { auctionManager } from './auctionManager.js'; import { sizeSupported } from './sizeMapping.js'; import { ADPOD } from './mediaTypes.js'; +import { hook } from './hook.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 +21,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); @@ -32,26 +34,31 @@ export let filters = { // If two bids are found for same adUnitCode, we will use the highest one to take part in auction // This can happen in case of concurrent auctions // If adUnitBidLimit is set above 0 return top N number of bids -export function getHighestCpmBidsFromBidPool(bidsReceived, highestCpmCallback, adUnitBidLimit = 0) { - const bids = []; - const dealPrioritization = config.getConfig('sendBidsControl.dealPrioritization'); - // bucket by adUnitcode - let buckets = groupBy(bidsReceived, 'adUnitCode'); - // filter top bid for each bucket by bidder - Object.keys(buckets).forEach(bucketKey => { - let bucketBids = []; - let bidsByBidder = groupBy(buckets[bucketKey], 'bidderCode'); - Object.keys(bidsByBidder).forEach(key => bucketBids.push(bidsByBidder[key].reduce(highestCpmCallback))); - // if adUnitBidLimit is set, pass top N number bids - if (adUnitBidLimit > 0) { - bucketBids = dealPrioritization ? bucketBids.sort(sortByDealAndPriceBucketOrCpm(true)) : bucketBids.sort((a, b) => b.cpm - a.cpm); - bids.push(...bucketBids.slice(0, adUnitBidLimit)); - } else { - bids.push(...bucketBids); - } - }); - return bids; -} +export const getHighestCpmBidsFromBidPool = hook('sync', function(bidsReceived, highestCpmCallback, adUnitBidLimit = 0, hasModified = false) { + if (!hasModified) { + const bids = []; + const dealPrioritization = config.getConfig('sendBidsControl.dealPrioritization'); + // bucket by adUnitcode + let buckets = groupBy(bidsReceived, 'adUnitCode'); + // filter top bid for each bucket by bidder + Object.keys(buckets).forEach(bucketKey => { + let bucketBids = []; + let bidsByBidder = groupBy(buckets[bucketKey], 'bidderCode'); + Object.keys(bidsByBidder).forEach(key => bucketBids.push(bidsByBidder[key].reduce(highestCpmCallback))); + // if adUnitBidLimit is set, pass top N number bids + if (adUnitBidLimit > 0) { + bucketBids = dealPrioritization ? bucketBids.sort(sortByDealAndPriceBucketOrCpm(true)) : bucketBids.sort((a, b) => b.cpm - a.cpm); + bids.push(...bucketBids.slice(0, adUnitBidLimit)); + } else { + bids.push(...bucketBids); + } + }); + + return bids; + } + + return bidsReceived; +}) /** * A descending sort function that will sort the list of objects based on the following two dimensions: @@ -181,6 +188,48 @@ export function newTargeting(auctionManager) { return []; }; + /** + * Returns filtered ad server targeting for custom and allowed keys. + * @param {targetingArray} targeting + * @param {string[]} allowedKeys + * @return {targetingArray} filtered targeting + */ + function getAllowedTargetingKeyValues(targeting, allowedKeys) { + const defaultKeyring = Object.assign({}, CONSTANTS.TARGETING_KEYS, CONSTANTS.NATIVE_KEYS); + const defaultKeys = Object.keys(defaultKeyring); + const keyDispositions = {}; + logInfo(`allowTargetingKeys - allowed keys [ ${allowedKeys.map(k => defaultKeyring[k]).join(', ')} ]`); + targeting.map(adUnit => { + const adUnitCode = Object.keys(adUnit)[0]; + const keyring = adUnit[adUnitCode]; + const keys = keyring.filter(kvPair => { + const key = Object.keys(kvPair)[0]; + // 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 || 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) + const found = key.indexOf(allowedKeyName) === 0; + return found; + }); + keyDispositions[key] = !found; + return found; + }); + adUnit[adUnitCode] = keys; + }); + const removedKeys = Object.keys(keyDispositions).filter(d => keyDispositions[d]); + logInfo(`allowTargetingKeys - removed keys [ ${removedKeys.join(', ')} ]`); + // remove any empty targeting objects, as they're unnecessary. + const filteredTargeting = targeting.filter(adUnit => { + const adUnitCode = Object.keys(adUnit)[0]; + const keyring = adUnit[adUnitCode]; + return keyring.length > 0; + }); + return filteredTargeting + } + /** * Returns all ad server targeting for all ad units. * @param {string=} adUnitCode @@ -193,7 +242,8 @@ export function newTargeting(auctionManager) { // `alwaysUseBid=true`. If sending all bids is enabled, add targeting for losing bids. var targeting = getWinningBidTargeting(adUnitCodes, bidsReceived) .concat(getCustomBidTargeting(adUnitCodes, bidsReceived)) - .concat(config.getConfig('enableSendAllBids') ? getBidLandscapeTargeting(adUnitCodes, bidsReceived) : getDealBids(adUnitCodes, bidsReceived)); + .concat(config.getConfig('enableSendAllBids') ? getBidLandscapeTargeting(adUnitCodes, bidsReceived) : getDealBids(adUnitCodes, bidsReceived)) + .concat(getAdUnitTargeting(adUnitCodes)); // store a reference of the targeting keys targeting.map(adUnitCode => { @@ -206,6 +256,12 @@ export function newTargeting(auctionManager) { }); }); + 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); + } + targeting = flattenTargeting(targeting); const auctionKeysThreshold = config.getConfig('targetingControls.auctionKeyMaxChars'); @@ -325,7 +381,10 @@ export function newTargeting(auctionManager) { Object.keys(targetingConfig).filter(customSlotMatching ? customSlotMatching(slot) : isAdUnitCodeMatchingSlot(slot)) .forEach(targetId => Object.keys(targetingConfig[targetId]).forEach(key => { - let valueArr = targetingConfig[targetId][key].split(','); + let valueArr = targetingConfig[targetId][key]; + if (typeof valueArr === 'string') { + valueArr = valueArr.split(','); + } valueArr = (valueArr.length > 1) ? [valueArr] : valueArr; valueArr.map((value) => { utils.logMessage(`Attempting to set key value for slot: ${slot.getSlotElementId()} key: ${key} value: ${value}`); @@ -559,6 +618,27 @@ export function newTargeting(auctionManager) { }); } + function getAdUnitTargeting(adUnitCodes) { + function getTargetingObj(adUnit) { + return deepAccess(adUnit, CONSTANTS.JSON_MAPPING.ADSERVER_TARGETING); + } + + function getTargetingValues(adUnit) { + const aut = getTargetingObj(adUnit); + + return Object.keys(aut) + .map(function(key) { + return {[key]: utils.isArray(aut[key]) ? aut[key] : aut[key].split(',')}; + }); + } + + return auctionManager.getAdUnits() + .filter(adUnit => includes(adUnitCodes, adUnit.code) && getTargetingObj(adUnit)) + .map(adUnit => { + return {[adUnit.code]: getTargetingValues(adUnit)} + }); + } + targeting.isApntagDefined = function() { if (window.apntag && utils.isFn(window.apntag.setKeywords)) { return true; diff --git a/src/userSync.js b/src/userSync.js index fceeb1d722d..f653880fa29 100644 --- a/src/userSync.js +++ b/src/userSync.js @@ -225,7 +225,7 @@ export function newUserSync(userSyncDependencies) { } return checkForFiltering[filterType](biddersToFilter, bidder); } - return false; + return !permittedPixels[type]; } /** diff --git a/src/utils.js b/src/utils.js index f0fa57c4cff..35ce3e7d0dd 100644 --- a/src/utils.js +++ b/src/utils.js @@ -21,6 +21,7 @@ let consoleLogExists = Boolean(consoleExists && window.console.log); let consoleInfoExists = Boolean(consoleExists && window.console.info); let consoleWarnExists = Boolean(consoleExists && window.console.warn); let consoleErrorExists = Boolean(consoleExists && window.console.error); +var events = require('./events.js'); // this allows stubbing of utility functions that are used internally by other utility functions export const internal = { @@ -42,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 @@ -255,20 +264,31 @@ export function logWarn() { if (debugTurnedOn() && consoleWarnExists) { console.warn.apply(console, decorateLog(arguments, 'WARNING:')); } + events.emit(CONSTANTS.EVENTS.AUCTION_DEBUG, {type: 'WARNING', arguments: arguments}); } export function logError() { if (debugTurnedOn() && consoleErrorExists) { console.error.apply(console, decorateLog(arguments, 'ERROR:')); } + events.emit(CONSTANTS.EVENTS.AUCTION_DEBUG, {type: 'ERROR', arguments: arguments}); } function decorateLog(args, prefix) { args = [].slice.call(args); + let bidder = config.getCurrentBidder(); + prefix && args.unshift(prefix); - args.unshift('display: inline-block; color: #fff; background: #3b88c3; padding: 1px 4px; border-radius: 3px;'); - args.unshift('%cPrebid'); + if (bidder) { + args.unshift(label('#aaa')); + } + args.unshift(label('#3b88c3')); + args.unshift('%cPrebid' + (bidder ? `%c${bidder}` : '')); return args; + + function label(color) { + return `display: inline-block; color: #fff; background: ${color}; padding: 1px 4px; border-radius: 3px;` + } } export function hasConsoleLogger() { @@ -716,10 +736,23 @@ export function replaceAuctionPrice(str, cpm) { return str.replace(/\$\{AUCTION_PRICE\}/g, cpm); } +export function replaceClickThrough(str, clicktag) { + if (!str || !clicktag || typeof clicktag !== 'string') return; + return str.replace(/\${CLICKTHROUGH}/g, clicktag); +} + export function timestamp() { return new Date().getTime(); } +/** + * The returned value represents the time elapsed since the time origin. @see https://developer.mozilla.org/en-US/docs/Web/API/Performance/now + * @returns {number} + */ +export function getPerformanceNow() { + return (window.performance && window.performance.now && window.performance.now()) || 0; +} + /** * When the deviceAccess flag config option is false, no cookies should be read or set * @returns {boolean} @@ -1215,3 +1248,43 @@ export function mergeDeep(target, ...sources) { return mergeDeep(target, ...sources); } + +/** + * returns a hash of a string using a fast algorithm + * source: https://stackoverflow.com/a/52171480/845390 + * @param str + * @param seed (optional) + * @returns {string} + */ +export function cyrb53Hash(str, seed = 0) { + // IE doesn't support imul + // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/imul#Polyfill + let imul = function(opA, opB) { + if (isFn(Math.imul)) { + return Math.imul(opA, opB); + } else { + opB |= 0; // ensure that opB is an integer. opA will automatically be coerced. + // floating points give us 53 bits of precision to work with plus 1 sign bit + // automatically handled for our convienence: + // 1. 0x003fffff /*opA & 0x000fffff*/ * 0x7fffffff /*opB*/ = 0x1fffff7fc00001 + // 0x1fffff7fc00001 < Number.MAX_SAFE_INTEGER /*0x1fffffffffffff*/ + var result = (opA & 0x003fffff) * opB; + // 2. We can remove an integer coersion from the statement above because: + // 0x1fffff7fc00001 + 0xffc00000 = 0x1fffffff800001 + // 0x1fffffff800001 < Number.MAX_SAFE_INTEGER /*0x1fffffffffffff*/ + if (opA & 0xffc00000) result += (opA & 0xffc00000) * opB | 0; + return result | 0; + } + }; + + let h1 = 0xdeadbeef ^ seed; + let h2 = 0x41c6ce57 ^ seed; + for (let i = 0, ch; i < str.length; i++) { + ch = str.charCodeAt(i); + h1 = imul(h1 ^ ch, 2654435761); + h2 = imul(h2 ^ ch, 1597334677); + } + h1 = imul(h1 ^ (h1 >>> 16), 2246822507) ^ imul(h2 ^ (h2 >>> 13), 3266489909); + h2 = imul(h2 ^ (h2 >>> 16), 2246822507) ^ imul(h1 ^ (h1 >>> 13), 3266489909); + return (4294967296 * (2097151 & h2) + (h1 >>> 0)).toString(); +} diff --git a/src/video.js b/src/video.js index befeb2ded39..20df7a92442 100644 --- a/src/video.js +++ b/src/video.js @@ -59,7 +59,7 @@ export const checkVideoBidSetup = hook('sync', function(bid, bidRequest, videoMe // outstream bids require a renderer on the bid or pub-defined on adunit if (context === OUTSTREAM) { - return !!(bid.renderer || bidRequest.renderer); + return !!(bid.renderer || bidRequest.renderer || videoMediaType.renderer); } return true; diff --git a/src/videoCache.js b/src/videoCache.js index 9f1fd7e4117..9e378d90574 100644 --- a/src/videoCache.js +++ b/src/videoCache.js @@ -71,6 +71,7 @@ function toStorageRequest(bid) { if (config.getConfig('cache.vasttrack')) { payload.bidder = bid.bidder; payload.bidid = bid.requestId; + payload.aid = bid.auctionId; // function has a thisArg set to bidderRequest for accessing the auctionStart if (utils.isPlainObject(this) && this.hasOwnProperty('auctionStart')) { payload.timestamp = this.auctionStart; 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/mocks/adloaderStub.js b/test/mocks/adloaderStub.js index b52ca5e9280..6e7f5756100 100644 --- a/test/mocks/adloaderStub.js +++ b/test/mocks/adloaderStub.js @@ -4,7 +4,7 @@ import * as adloader from 'src/adloader.js'; // this export is for adloader's tests against actual implementation export let loadExternalScript = adloader.loadExternalScript; -let stub = createStub(); +export let loadExternalScriptStub = createStub(); function createStub() { return sinon.stub(adloader, 'loadExternalScript').callsFake((...args) => { @@ -18,6 +18,6 @@ function createStub() { } beforeEach(function() { - stub.restore(); - stub = createStub(); + loadExternalScriptStub.restore(); + loadExternalScriptStub = createStub(); }); diff --git a/test/mocks/fabrickId.json b/test/mocks/fabrickId.json new file mode 100644 index 00000000000..a8723ec88ec --- /dev/null +++ b/test/mocks/fabrickId.json @@ -0,0 +1,3 @@ +{ + "fabrickId": 1980 +} diff --git a/test/spec/AnalyticsAdapter_spec.js b/test/spec/AnalyticsAdapter_spec.js index 4afa430f81e..2b36848bd8f 100644 --- a/test/spec/AnalyticsAdapter_spec.js +++ b/test/spec/AnalyticsAdapter_spec.js @@ -9,6 +9,7 @@ const BID_RESPONSE = CONSTANTS.EVENTS.BID_RESPONSE; const BID_WON = CONSTANTS.EVENTS.BID_WON; const BID_TIMEOUT = CONSTANTS.EVENTS.BID_TIMEOUT; const AD_RENDER_FAILED = CONSTANTS.EVENTS.AD_RENDER_FAILED; +const AUCTION_DEBUG = CONSTANTS.EVENTS.AUCTION_DEBUG; const ADD_AD_UNITS = CONSTANTS.EVENTS.ADD_AD_UNITS; const AnalyticsAdapter = require('src/AnalyticsAdapter').default; @@ -48,8 +49,10 @@ FEATURE: Analytics Adapters API events.emit(eventType, args); adapter.enableAnalytics(); - let result = JSON.parse(server.requests[0].requestBody); - expect(result).to.deep.equal({args: {wat: 'wot'}, eventType: 'bidResponse'}); + // As now AUCTION_DEBUG is triggered for WARNINGS too, the BID_RESPONSE goes last in the array + const index = server.requests.length - 1; + let result = JSON.parse(server.requests[index].requestBody); + expect(result).to.deep.equal({eventType: 'bidResponse', args: {wat: 'wot'}}); }); describe(`WHEN an event occurs after enable analytics\n`, function () { @@ -83,6 +86,17 @@ FEATURE: Analytics Adapters API expect(result).to.deep.equal({args: {call: 'adRenderFailed'}, eventType: 'adRenderFailed'}); }); + it('SHOULD call global when an auction debug event occurs', function () { + const eventType = AUCTION_DEBUG; + const args = { call: 'auctionDebug' }; + + adapter.enableAnalytics(); + events.emit(eventType, args); + + let result = JSON.parse(server.requests[0].requestBody); + expect(result).to.deep.equal({args: {call: 'auctionDebug'}, eventType: 'auctionDebug'}); + }); + it('SHOULD call global when an addAdUnits event occurs', function () { const eventType = ADD_AD_UNITS; const args = { call: 'addAdUnits' }; diff --git a/test/spec/adUnits_spec.js b/test/spec/adUnits_spec.js index 7dd48a13208..baa5b4ac8c4 100644 --- a/test/spec/adUnits_spec.js +++ b/test/spec/adUnits_spec.js @@ -23,6 +23,16 @@ describe('Publisher API _ AdUnits', function () { } ] }, { + fpd: { + context: { + pbAdSlot: 'adSlotTest', + data: { + inventory: [4], + keywords: 'foo,bar', + visitor: [1, 2, 3], + } + } + }, code: '/1996833/slot-2', sizes: [[468, 60]], bids: [ @@ -85,6 +95,7 @@ describe('Publisher API _ AdUnits', function () { it('the second adUnits value should be same with the adUnits that is added by $$PREBID_GLOBAL$$.addAdUnits();', function () { assert.strictEqual(adUnit2.code, '/1996833/slot-2', 'adUnit2 code'); assert.deepEqual(adUnit2.sizes, [[468, 60]], 'adUnit2 sizes'); + assert.deepEqual(adUnit2['ortb2Imp'], {'ext': {'data': {'pbadslot': 'adSlotTest', 'inventory': [4], 'keywords': 'foo,bar', 'visitor': [1, 2, 3]}}}, 'adUnit2 ortb2Imp'); assert.strictEqual(bids2[0].bidder, 'rubicon', 'adUnit2 bids1 bidder'); assert.strictEqual(bids2[0].params.rp_account, '4934', 'adUnit2 bids1 params.rp_account'); assert.strictEqual(bids2[0].params.rp_zonesize, '23948-15', 'adUnit2 bids1 params.rp_zonesize'); diff --git a/test/spec/adapters/adbutler_spec.js b/test/spec/adapters/adbutler_spec.js deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/test/spec/api_spec.js b/test/spec/api_spec.js index 6f21eba7aaf..cb6c7aa87a3 100755 --- a/test/spec/api_spec.js +++ b/test/spec/api_spec.js @@ -43,10 +43,14 @@ describe('Publisher API', function () { assert.isFunction($$PREBID_GLOBAL$$.getBidResponses); }); - it('should have function $$PREBID_GLOBAL$$.getBidResponses', function () { + it('should have function $$PREBID_GLOBAL$$.getNoBids', function () { assert.isFunction($$PREBID_GLOBAL$$.getNoBids); }); + it('should have function $$PREBID_GLOBAL$$.getNoBidsForAdUnitCode', function () { + assert.isFunction($$PREBID_GLOBAL$$.getNoBidsForAdUnitCode); + }); + it('should have function $$PREBID_GLOBAL$$.getBidResponsesForAdUnitCode', function () { assert.isFunction($$PREBID_GLOBAL$$.getBidResponsesForAdUnitCode); }); @@ -78,5 +82,9 @@ describe('Publisher API', function () { it('should have function $$PREBID_GLOBAL$$.getAllWinningBids', function () { assert.isFunction($$PREBID_GLOBAL$$.getAllWinningBids); }); + + it('should have function $$PREBID_GLOBAL$$.getHighestUnusedBidResponseForAdUnitCode', function () { + assert.isFunction($$PREBID_GLOBAL$$.getHighestUnusedBidResponseForAdUnitCode); + }); }); }); diff --git a/test/spec/auctionmanager_spec.js b/test/spec/auctionmanager_spec.js index 35a29727614..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]; + } } ] @@ -740,6 +756,91 @@ describe('auctionmanager.js', function () { assert.equal(addedBid.renderer.url, 'renderer.js'); }); + it('installs publisher-defined backup renderers on bids', function () { + let renderer = { + url: 'renderer.js', + backupOnly: true, + render: (bid) => bid + }; + let bidRequests = [Object.assign({}, TEST_BID_REQS[0])]; + bidRequests[0].bids[0] = Object.assign({ renderer }, bidRequests[0].bids[0]); + makeRequestsStub.returns(bidRequests); + + let bids1 = Object.assign({}, + bids[0], + { + bidderCode: BIDDER_CODE, + mediaType: 'video-outstream', + } + ); + spec.interpretResponse.returns(bids1); + auction.callBids(); + const addedBid = auction.getBidsReceived().pop(); + assert.equal(addedBid.renderer.url, 'renderer.js'); + }); + + it('installs publisher-defined renderers for a media type', function () { + const renderer = { + url: 'videoRenderer.js', + render: (bid) => bid + }; + let myBid = mockBid(); + let bidRequest = mockBidRequest(myBid); + + bidRequest.bids[0] = { + ...bidRequest.bids[0], + mediaTypes: { + banner: { + sizes: [[300, 250], [300, 600]] + }, + video: { + context: 'outstream', + renderer + } + } + }; + makeRequestsStub.returns([bidRequest]); + + myBid.mediaType = 'video'; + spec.interpretResponse.returns(myBid); + auction.callBids(); + + const addedBid = auction.getBidsReceived().pop(); + assert.equal(addedBid.renderer.url, renderer.url); + }); + + it('installs bidder-defined renderer when onlyBackup is true in mediaTypes.video options ', function () { + const renderer = { + url: 'videoRenderer.js', + backupOnly: true, + render: (bid) => bid + }; + let myBid = mockBid(); + let bidRequest = mockBidRequest(myBid); + + bidRequest.bids[0] = { + ...bidRequest.bids[0], + mediaTypes: { + video: { + context: 'outstream', + renderer + } + } + }; + makeRequestsStub.returns([bidRequest]); + + myBid.mediaType = 'video'; + myBid.renderer = { + url: 'renderer.js', + render: sinon.spy() + }; + spec.interpretResponse.returns(myBid); + auction.callBids(); + + const addedBid = auction.getBidsReceived().pop(); + assert.strictEqual(addedBid.renderer.url, myBid.renderer.url); + }); + it('bid for a regular unit and a video unit', function() { let renderer = { url: 'renderer.js', @@ -1229,4 +1330,119 @@ describe('auctionmanager.js', function () { assert.equal(doneSpy.callCount, 1); }) }); + + describe('auctionOptions', function() { + let bidRequests; + let doneSpy; + let clock; + let auction = { + getBidRequests: () => bidRequests, + getAuctionId: () => '1', + addBidReceived: () => true, + getTimeout: () => 1000 + } + let requiredBidder = BIDDER_CODE; + let requiredBidder1 = BIDDER_CODE1; + let secondaryBidder = 'doNotWaitForMe'; + + beforeEach(() => { + clock = sinon.useFakeTimers(); + doneSpy = sinon.spy(); + config.setConfig({ + 'auctionOptions': { + secondaryBidders: [ secondaryBidder ] + } + }) + }); + + afterEach(() => { + doneSpy.resetHistory(); + config.resetConfig(); + clock.restore(); + }); + + it('should not wait to call auction done for secondary bidders', function () { + let bids1 = [mockBid({ bidderCode: requiredBidder })]; + let bids2 = [mockBid({ bidderCode: requiredBidder1 })]; + let bids3 = [mockBid({ bidderCode: secondaryBidder })]; + bidRequests = [ + mockBidRequest(bids1[0], { adUnitCode: ADUNIT_CODE1 }), + mockBidRequest(bids2[0], { adUnitCode: ADUNIT_CODE1 }), + mockBidRequest(bids3[0], { adUnitCode: ADUNIT_CODE1 }), + ]; + let cbs = auctionCallbacks(doneSpy, auction); + // required bidder responds immeaditely to auction + cbs.addBidResponse.call(bidRequests[0], ADUNIT_CODE1, bids1[0]); + cbs.adapterDone.call(bidRequests[0]); + assert.equal(doneSpy.callCount, 0); + + // auction waits for second required bidder to respond + clock.tick(100); + cbs.addBidResponse.call(bidRequests[1], ADUNIT_CODE1, bids2[0]); + cbs.adapterDone.call(bidRequests[1]); + + // auction done is reported and does not wait for secondaryBidder request + assert.equal(doneSpy.callCount, 1); + + cbs.addBidResponse.call(bidRequests[2], ADUNIT_CODE1, bids3[0]); + cbs.adapterDone.call(bidRequests[2]); + }); + + it('should wait for all bidders if they are all secondary', function () { + config.setConfig({ + 'auctionOptions': { + secondaryBidders: [requiredBidder, requiredBidder1, secondaryBidder] + } + }) + let bids1 = [mockBid({ bidderCode: requiredBidder })]; + let bids2 = [mockBid({ bidderCode: requiredBidder1 })]; + let bids3 = [mockBid({ bidderCode: secondaryBidder })]; + bidRequests = [ + mockBidRequest(bids1[0], { adUnitCode: ADUNIT_CODE1 }), + mockBidRequest(bids2[0], { adUnitCode: ADUNIT_CODE1 }), + mockBidRequest(bids3[0], { adUnitCode: ADUNIT_CODE1 }), + ]; + let cbs = auctionCallbacks(doneSpy, auction); + cbs.addBidResponse.call(bidRequests[0], ADUNIT_CODE1, bids1[0]); + cbs.adapterDone.call(bidRequests[0]); + clock.tick(100); + assert.equal(doneSpy.callCount, 0) + + cbs.addBidResponse.call(bidRequests[1], ADUNIT_CODE1, bids2[0]); + cbs.adapterDone.call(bidRequests[1]); + clock.tick(100); + assert.equal(doneSpy.callCount, 0); + + cbs.addBidResponse.call(bidRequests[2], ADUNIT_CODE1, bids3[0]); + cbs.adapterDone.call(bidRequests[2]); + assert.equal(doneSpy.callCount, 1); + }); + + it('should allow secondaryBidders to respond in auction before is is done', function () { + let bids1 = [mockBid({ bidderCode: requiredBidder })]; + let bids2 = [mockBid({ bidderCode: requiredBidder1 })]; + let bids3 = [mockBid({ bidderCode: secondaryBidder })]; + bidRequests = [ + mockBidRequest(bids1[0], { adUnitCode: ADUNIT_CODE1 }), + mockBidRequest(bids2[0], { adUnitCode: ADUNIT_CODE1 }), + mockBidRequest(bids3[0], { adUnitCode: ADUNIT_CODE1 }), + ]; + let cbs = auctionCallbacks(doneSpy, auction); + // secondaryBidder is first to respond + cbs.addBidResponse.call(bidRequests[2], ADUNIT_CODE1, bids3[0]); + cbs.adapterDone.call(bidRequests[2]); + clock.tick(100); + assert.equal(doneSpy.callCount, 0); + + cbs.addBidResponse.call(bidRequests[1], ADUNIT_CODE1, bids2[0]); + cbs.adapterDone.call(bidRequests[1]); + clock.tick(100); + assert.equal(doneSpy.callCount, 0); + + // first required bidder takes longest to respond, auction isn't marked as done until this occurs + cbs.addBidResponse.call(bidRequests[0], ADUNIT_CODE1, bids1[0]); + cbs.adapterDone.call(bidRequests[0]); + assert.equal(doneSpy.callCount, 1); + }); + }); }); diff --git a/test/spec/config_spec.js b/test/spec/config_spec.js index be5b4bbb78b..8161d9f1827 100644 --- a/test/spec/config_spec.js +++ b/test/spec/config_spec.js @@ -6,6 +6,8 @@ const utils = require('src/utils'); let getConfig; let setConfig; +let getBidderConfig; +let setBidderConfig; let setDefaults; describe('config API', function () { @@ -15,6 +17,8 @@ describe('config API', function () { const config = newConfig(); getConfig = config.getConfig; setConfig = config.setConfig; + getBidderConfig = config.getBidderConfig; + setBidderConfig = config.setBidderConfig; setDefaults = config.setDefaults; logErrorSpy = sinon.spy(utils, 'logError'); logWarnSpy = sinon.spy(utils, 'logWarn'); @@ -57,6 +61,17 @@ describe('config API', function () { expect(getConfig('foo')).to.eql({baz: 'qux'}); }); + it('moves fpd config into ortb2 properties', function () { + setConfig({fpd: {context: {keywords: 'foo,bar', data: {inventory: [1]}}}}); + expect(getConfig('ortb2')).to.eql({site: {keywords: 'foo,bar', ext: {data: {inventory: [1]}}}}); + expect(getConfig('fpd')).to.eql(undefined); + }); + + it('moves fpd bidderconfig into ortb2 properties', function () { + setBidderConfig({bidders: ['bidderA'], config: {fpd: {context: {keywords: 'foo,bar', data: {inventory: [1]}}}}}); + expect(getBidderConfig()).to.eql({'bidderA': {ortb2: {site: {keywords: 'foo,bar', ext: {data: {inventory: [1]}}}}}}); + }); + it('sets debugging', function () { setConfig({ debug: true }); expect(getConfig('debug')).to.be.true; @@ -195,6 +210,12 @@ describe('config API', function () { expect(getConfig('deviceAccess')).to.be.equal(true); }); + it('sets maxNestedIframes', function () { + expect(getConfig('maxNestedIframes')).to.be.equal(10); + setConfig({ maxNestedIframes: 2 }); + expect(getConfig('maxNestedIframes')).to.be.equal(2); + }); + it('should log error for invalid priceGranularity', function () { setConfig({ priceGranularity: '' }); const error = 'Prebid Error: no value passed to `setPriceGranularity()`'; @@ -211,4 +232,37 @@ describe('config API', function () { setConfig({ bidderSequence: 'random' }); expect(logWarnSpy.called).to.equal(false); }); + + it('sets auctionOptions', function () { + const auctionOptionsConfig = { + 'secondaryBidders': ['rubicon', 'appnexus'] + } + setConfig({ auctionOptions: auctionOptionsConfig }); + expect(getConfig('auctionOptions')).to.eql(auctionOptionsConfig); + }); + + it('should log warning for the wrong value passed to auctionOptions', function () { + setConfig({ auctionOptions: '' }); + expect(logWarnSpy.calledOnce).to.equal(true); + const warning = 'Auction Options must be an object'; + assert.ok(logWarnSpy.calledWith(warning), 'expected warning was logged'); + }); + + it('should log warning for invalid auctionOptions bidder values', function () { + setConfig({ auctionOptions: { + 'secondaryBidders': 'appnexus, rubicon', + }}); + expect(logWarnSpy.calledOnce).to.equal(true); + const warning = 'Auction Options secondaryBidders must be of type Array'; + assert.ok(logWarnSpy.calledWith(warning), 'expected warning was logged'); + }); + + it('should log warning for invalid properties to auctionOptions', function () { + setConfig({ auctionOptions: { + 'testing': true + }}); + expect(logWarnSpy.calledOnce).to.equal(true); + const warning = 'Auction Options given an incorrect param: testing'; + assert.ok(logWarnSpy.calledWith(warning), 'expected warning was logged'); + }); }); diff --git a/test/spec/integration/faker/googletag.js b/test/spec/integration/faker/googletag.js index a0ce04402f7..9d91bf315d9 100644 --- a/test/spec/integration/faker/googletag.js +++ b/test/spec/integration/faker/googletag.js @@ -43,18 +43,52 @@ export function makeSlot() { return slot; } -window.googletag = { - _slots: [], - pubads: function () { - var self = this; - return { - getSlots: function () { - return self._slots; - }, - - setSlots: function (slots) { - self._slots = slots; - } - }; - } -}; +export function emitEvent(eventName, params) { + (window.googletag._callbackMap[eventName] || []).forEach(eventCb => eventCb({...params, eventName})); +} + +export function enable() { + window.googletag = { + _slots: [], + _callbackMap: {}, + pubads: function () { + var self = this; + return { + getSlots: function () { + return self._slots; + }, + + setSlots: function (slots) { + self._slots = slots; + }, + + setTargeting: function(key, arrayOfValues) { + self._targeting[key] = Array.isArray(arrayOfValues) ? arrayOfValues : [arrayOfValues]; + }, + + getTargeting: function(key) { + return self._targeting[key] || []; + }, + + getTargetingKeys: function() { + return Object.getOwnPropertyNames(self._targeting); + }, + + clearTargeting: function() { + self._targeting = {}; + }, + + addEventListener: function (eventName, cb) { + self._callbackMap[eventName] = self._callbackMap[eventName] || []; + self._callbackMap[eventName].push(cb); + } + }; + } + }; +} + +export function disable() { + window.googletag = undefined; +} + +enable(); diff --git a/test/spec/modules/33acrossBidAdapter_spec.js b/test/spec/modules/33acrossBidAdapter_spec.js index d30659791ea..b5443cdd5c2 100644 --- a/test/spec/modules/33acrossBidAdapter_spec.js +++ b/test/spec/modules/33acrossBidAdapter_spec.js @@ -5,10 +5,18 @@ import { config } from 'src/config.js'; import { spec } from 'modules/33acrossBidAdapter.js'; +function validateBuiltServerRequest(builtReq, expectedReq) { + expect(builtReq.url).to.equal(expectedReq.url); + expect(builtReq.options).to.deep.equal(expectedReq.options); + expect(JSON.parse(builtReq.data)).to.deep.equal( + JSON.parse(expectedReq.data) + ) +} + describe('33acrossBidAdapter:', function () { const BIDDER_CODE = '33across'; - const SITE_ID = 'pub1234'; - const PRODUCT_ID = 'product1'; + const SITE_ID = 'sample33xGUID123456789'; + const PRODUCT_ID = 'siab'; const END_POINT = 'https://ssc.33across.com/api/v1/hb'; let element, win; @@ -17,7 +25,29 @@ describe('33acrossBidAdapter:', function () { function TtxRequestBuilder() { const ttxRequest = { - imp: [{ + imp: [{}], + site: { + id: SITE_ID + }, + id: 'b1', + regs: { + ext: { + gdpr: 0 + } + }, + ext: { + ttx: { + prebidStartedAt: 1, + caller: [{ + 'name': 'prebidjs', + 'version': '$prebid.version$' + }] + } + } + }; + + this.withBanner = () => { + Object.assign(ttxRequest.imp[0], { banner: { format: [ { @@ -36,46 +66,32 @@ describe('33acrossBidAdapter:', function () { } } } - }, - ext: { - ttx: { - prod: PRODUCT_ID - } - } - }], - site: { - id: SITE_ID - }, - id: 'b1', - user: { - ext: { - consent: undefined - } - }, - regs: { - ext: { - gdpr: 0, - us_privacy: null } - }, - ext: { - ttx: { - prebidStartedAt: 1, - caller: [{ - 'name': 'prebidjs', - 'version': '$prebid.version$' - }] - } - } + }); + + return this; }; - this.withSizes = sizes => { + this.withBannerSizes = this.withSizes = sizes => { Object.assign(ttxRequest.imp[0].banner, { format: sizes }); return this; }; - this.withViewability = viewability => { - Object.assign(ttxRequest.imp[0].banner, { + this.withVideo = (params = {}) => { + Object.assign(ttxRequest.imp[0], { + video: { + w: 300, + h: 250, + placement: 2, + ...params + } + }); + + return this; + }; + + this.withViewability = (viewability, format = 'banner') => { + Object.assign(ttxRequest.imp[0][format], { ext: { ttx: { viewability } } @@ -83,6 +99,18 @@ describe('33acrossBidAdapter:', function () { return this; }; + this.withProduct = (prod = PRODUCT_ID) => { + Object.assign(ttxRequest.imp[0], { + ext: { + ttx: { + prod + } + } + }); + + return this; + }; + this.withGdprConsent = (consent, gdpr) => { Object.assign(ttxRequest, { user: { @@ -140,21 +168,46 @@ describe('33acrossBidAdapter:', function () { return this; }; - this.withFormatFloors = floors => { - const format = ttxRequest.imp[0].banner.format.map((fm, i) => { - return Object.assign(fm, { - ext: { - ttx: { - bidfloors: [ floors[i] ] + this.withFloors = this.withFormatFloors = (mediaType, floors) => { + switch (mediaType) { + case 'banner': + const format = ttxRequest.imp[0].banner.format.map((fm, i) => { + return Object.assign(fm, { + ext: { + ttx: { + bidfloors: [ floors[i] ] + } + } + }) + }); + + ttxRequest.imp[0].banner.format = format; + break; + case 'video': + Object.assign(ttxRequest.imp[0].video, { + ext: { + ttx: { + bidfloors: floors + } } + }); + break; + } + + return this; + }; + + this.withUserIds = (eids) => { + Object.assign(ttxRequest, { + user: { + ext: { + eids } - }) + } }); - ttxRequest.imp[0].banner.format = format; - return this; - }; + } this.build = () => ttxRequest; } @@ -188,6 +241,59 @@ describe('33acrossBidAdapter:', function () { this.build = () => serverRequest; } + function BidRequestsBuilder() { + const bidRequests = [ + { + bidId: 'b1', + bidder: '33across', + bidderRequestId: 'b1a', + params: { + siteId: SITE_ID, + productId: PRODUCT_ID + }, + adUnitCode: 'div-id', + auctionId: 'r1', + mediaTypes: {}, + transactionId: 't1' + } + ]; + + this.withBanner = () => { + bidRequests[0].mediaTypes.banner = { + sizes: [ + [300, 250], + [728, 90] + ] + }; + + return this; + }; + + this.withProduct = (prod) => { + bidRequests[0].params.productId = prod; + + return this; + }; + + this.withVideo = (params) => { + bidRequests[0].mediaTypes.video = { + playerSize: [[300, 250]], + context: 'outstream', + ...params + }; + + return this; + } + + this.withUserIds = (eids) => { + bidRequests[0].userIdAsEids = eids; + + return this; + }; + + this.build = () => bidRequests; + } + beforeEach(function() { element = { x: 0, @@ -217,24 +323,11 @@ describe('33acrossBidAdapter:', function () { innerHeight: 600 }; - bidRequests = [ - { - bidId: 'b1', - bidder: '33across', - bidderRequestId: 'b1a', - params: { - siteId: SITE_ID, - productId: PRODUCT_ID - }, - adUnitCode: 'div-id', - auctionId: 'r1', - sizes: [ - [300, 250], - [728, 90] - ], - transactionId: 't1' - } - ]; + bidRequests = ( + new BidRequestsBuilder() + .withBanner() + .build() + ); sandbox = sinon.sandbox.create(); sandbox.stub(Date, 'now').returns(1); @@ -248,78 +341,246 @@ describe('33acrossBidAdapter:', function () { }); describe('isBidRequestValid:', function() { - it('returns true when valid bid request is sent', function() { - const validBid = { - bidder: BIDDER_CODE, - params: { - siteId: SITE_ID, - productId: PRODUCT_ID - } - }; + context('basic validation', function() { + it('returns true for valid guid values', function() { + // NOTE: We ignore whitespace at the start and end since + // in our experience these are common typos + const validGUIDs = [ + `${SITE_ID}`, + `${SITE_ID} `, + ` ${SITE_ID}`, + ` ${SITE_ID} ` + ]; - expect(spec.isBidRequestValid(validBid)).to.be.true; - }); + validGUIDs.forEach((siteId) => { + const bid = { + bidder: '33across', + params: { + siteId + } + }; - it('returns true when valid test bid request is sent', function() { - const validBid = { - bidder: BIDDER_CODE, - params: { - siteId: SITE_ID, - productId: PRODUCT_ID, - test: 1 - } - }; + expect(spec.isBidRequestValid(bid)).to.be.true; + }); + }); - expect(spec.isBidRequestValid(validBid)).to.be.true; - }); + it('returns false for invalid guid values', function() { + const invalidGUIDs = [ + undefined, + 'siab' + ]; - it('returns false when bidder not set to "33across"', function() { - const invalidBid = { - bidder: 'foo', - params: { - siteId: SITE_ID, - productId: PRODUCT_ID - } - }; + invalidGUIDs.forEach((siteId) => { + const bid = { + bidder: '33across', + params: { + siteId + } + }; - expect(spec.isBidRequestValid(invalidBid)).to.be.false; + expect(spec.isBidRequestValid(bid)).to.be.false; + }); + }); }); - it('returns false when params not set', function() { - const invalidBid = { - bidder: 'foo' - }; + context('banner validation', function() { + it('returns true when banner mediaType does not exist', function() { + const bid = { + bidder: '33across', + params: { + siteId: 'cxBE0qjUir6iopaKkGJozW' + } + }; - expect(spec.isBidRequestValid(invalidBid)).to.be.false; - }); + expect(spec.isBidRequestValid(bid)).to.be.true; + }); - it('returns false when site ID is not set in params', function() { - const invalidBid = { - bidder: 'foo', - params: { - productId: PRODUCT_ID - } - }; + it('returns true when banner sizes are defined', function() { + const bid = { + bidder: '33across', + mediaTypes: { + banner: { + sizes: [[250, 300]] + } + }, + params: { + siteId: 'cxBE0qjUir6iopaKkGJozW' + } + }; - expect(spec.isBidRequestValid(invalidBid)).to.be.false; - }); + expect(spec.isBidRequestValid(bid)).to.be.true; + }); - it('returns false when product ID not set in params', function() { - const invalidBid = { - bidder: 'foo', - params: { - siteId: SITE_ID - } - }; + it('returns false when banner sizes are invalid', function() { + const invalidSizes = [ + undefined, + '16:9', + 300, + 'foo' + ]; - expect(spec.isBidRequestValid(invalidBid)).to.be.false; + invalidSizes.forEach((sizes) => { + const bid = { + bidder: '33across', + mediaTypes: { + banner: { + sizes + } + }, + params: { + siteId: 'cxBE0qjUir6iopaKkGJozW' + } + }; + + expect(spec.isBidRequestValid(bid)).to.be.false; + }); + }); }); + + context('video validation', function() { + beforeEach(function() { + // Basic Valid BidRequest + this.bid = { + bidder: '33across', + mediaTypes: { + video: { + playerSize: [[300, 50]], + context: 'outstream', + mimes: ['foo', 'bar'], + protocols: [1, 2] + } + }, + params: { + siteId: `${SITE_ID}` + } + }; + }); + + it('returns true when video mediaType does not exist', function() { + const bid = { + bidder: '33across', + params: { + siteId: `${SITE_ID}` + } + }; + + expect(spec.isBidRequestValid(bid)).to.be.true; + }); + + it('returns true when valid video mediaType is defined', function() { + expect(spec.isBidRequestValid(this.bid)).to.be.true; + }); + + it('returns false when video context is not defined', function() { + delete this.bid.mediaTypes.video.context; + + expect(spec.isBidRequestValid(this.bid)).to.be.false; + }); + + it('returns false when video playserSize is invalid', function() { + const invalidSizes = [ + undefined, + '16:9', + 300, + 'foo' + ]; + + invalidSizes.forEach((playerSize) => { + this.bid.mediaTypes.video.playerSize = playerSize; + expect(spec.isBidRequestValid(this.bid)).to.be.false; + }); + }); + + it('returns false when video mimes is invalid', function() { + const invalidMimes = [ + undefined, + 'foo', + 1, + [] + ] + + invalidMimes.forEach((mimes) => { + this.bid.mediaTypes.video.mimes = mimes; + expect(spec.isBidRequestValid(this.bid)).to.be.false; + }) + }); + + it('returns false when video protocols is invalid', function() { + const invalidMimes = [ + undefined, + 'foo', + 1, + [] + ] + + invalidMimes.forEach((protocols) => { + this.bid.mediaTypes.video.protocols = protocols; + expect(spec.isBidRequestValid(this.bid)).to.be.false; + }) + }); + + it('returns false when video placement is invalid', function() { + const invalidPlacement = [ + [], + '1', + {}, + 'foo' + ]; + + invalidPlacement.forEach((placement) => { + this.bid.mediaTypes.video.placement = placement; + expect(spec.isBidRequestValid(this.bid)).to.be.false; + }); + }); + + it('returns false when video startdelay is invalid for instream context', function() { + const bidRequests = ( + new BidRequestsBuilder() + .withVideo({context: 'instream', protocols: [1, 2], mimes: ['foo', 'bar']}) + .build() + ); + + const invalidStartdelay = [ + [], + '1', + {}, + 'foo' + ]; + + invalidStartdelay.forEach((startdelay) => { + bidRequests[0].mediaTypes.video.startdelay = startdelay; + expect(spec.isBidRequestValid(bidRequests[0])).to.be.false; + }); + }); + + it('returns true when video startdelay is invalid for outstream context', function() { + const bidRequests = ( + new BidRequestsBuilder() + .withVideo({context: 'outstream', protocols: [1, 2], mimes: ['foo', 'bar']}) + .build() + ); + + const invalidStartdelay = [ + [], + '1', + {}, + 'foo' + ]; + + invalidStartdelay.forEach((startdelay) => { + bidRequests[0].mediaTypes.video.startdelay = startdelay; + expect(spec.isBidRequestValid(bidRequests[0])).to.be.true; + }); + }); + }) }); describe('buildRequests:', function() { context('when element is fully in view', function() { it('returns 100', function() { const ttxRequest = new TtxRequestBuilder() + .withBanner() + .withProduct() .withViewability({amount: 100}) .build(); const serverRequest = new ServerRequestBuilder() @@ -328,13 +589,16 @@ describe('33acrossBidAdapter:', function () { Object.assign(element, { width: 600, height: 400 }); - expect(spec.buildRequests(bidRequests)).to.deep.equal([ serverRequest ]); + const [ buildRequest ] = spec.buildRequests(bidRequests); + validateBuiltServerRequest(buildRequest, serverRequest); }); }); context('when element is out of view', function() { it('returns 0', function() { const ttxRequest = new TtxRequestBuilder() + .withBanner() + .withProduct() .withViewability({amount: 0}) .build(); const serverRequest = new ServerRequestBuilder() @@ -343,13 +607,16 @@ describe('33acrossBidAdapter:', function () { Object.assign(element, { x: -300, y: 0, width: 207, height: 320 }); - expect(spec.buildRequests(bidRequests)).to.deep.equal([ serverRequest ]); + const [ buildRequest ] = spec.buildRequests(bidRequests); + validateBuiltServerRequest(buildRequest, serverRequest); }); }); context('when element is partially in view', function() { it('returns percentage', function() { const ttxRequest = new TtxRequestBuilder() + .withBanner() + .withProduct() .withViewability({amount: 75}) .build(); const serverRequest = new ServerRequestBuilder() @@ -358,13 +625,16 @@ describe('33acrossBidAdapter:', function () { Object.assign(element, { width: 800, height: 800 }); - expect(spec.buildRequests(bidRequests)).to.deep.equal([ serverRequest ]); + const [ buildRequest ] = spec.buildRequests(bidRequests); + validateBuiltServerRequest(buildRequest, serverRequest); }); }); context('when width or height of the element is zero', function() { it('try to use alternative values', function() { const ttxRequest = new TtxRequestBuilder() + .withBanner() + .withProduct() .withSizes([{ w: 800, h: 2400 }]) .withViewability({amount: 25}) .build(); @@ -373,15 +643,18 @@ describe('33acrossBidAdapter:', function () { .build(); Object.assign(element, { width: 0, height: 0 }); - bidRequests[0].sizes = [[800, 2400]]; + bidRequests[0].mediaTypes.banner.sizes = [[800, 2400]]; - expect(spec.buildRequests(bidRequests)).to.deep.equal([ serverRequest ]); + const [ buildRequest ] = spec.buildRequests(bidRequests); + validateBuiltServerRequest(buildRequest, serverRequest); }); }); context('when nested iframes', function() { it('returns \'nm\'', function() { const ttxRequest = new TtxRequestBuilder() + .withBanner() + .withProduct() .withViewability({amount: spec.NON_MEASURABLE}) .build(); const serverRequest = new ServerRequestBuilder() @@ -395,13 +668,16 @@ describe('33acrossBidAdapter:', function () { sandbox.stub(utils, 'getWindowTop').returns({}); sandbox.stub(utils, 'getWindowSelf').returns(win); - expect(spec.buildRequests(bidRequests)).to.deep.equal([ serverRequest ]); + const [ buildRequest ] = spec.buildRequests(bidRequests); + validateBuiltServerRequest(buildRequest, serverRequest); }); }); context('when tab is inactive', function() { it('returns 0', function() { const ttxRequest = new TtxRequestBuilder() + .withBanner() + .withProduct() .withViewability({amount: 0}) .build(); const serverRequest = new ServerRequestBuilder() @@ -414,7 +690,8 @@ describe('33acrossBidAdapter:', function () { win.document.visibilityState = 'hidden'; sandbox.stub(utils, 'getWindowTop').returns(win); - expect(spec.buildRequests(bidRequests)).to.deep.equal([ serverRequest ]); + const [ buildRequest ] = spec.buildRequests(bidRequests); + validateBuiltServerRequest(buildRequest, serverRequest); }); }); @@ -432,14 +709,16 @@ describe('33acrossBidAdapter:', function () { it('returns corresponding server requests with gdpr consent data', function() { const ttxRequest = new TtxRequestBuilder() + .withBanner() + .withProduct() .withGdprConsent('foobarMyPreference', 1) .build(); const serverRequest = new ServerRequestBuilder() .withData(ttxRequest) .build(); - const builtServerRequests = spec.buildRequests(bidRequests, bidderRequest); + const [ builtServerRequest ] = spec.buildRequests(bidRequests, bidderRequest); - expect(builtServerRequests).to.deep.equal([serverRequest]); + validateBuiltServerRequest(builtServerRequest, serverRequest); }); it('returns corresponding test server requests with gdpr consent data', function() { @@ -450,15 +729,17 @@ describe('33acrossBidAdapter:', function () { }); const ttxRequest = new TtxRequestBuilder() + .withBanner() + .withProduct() .withGdprConsent('foobarMyPreference', 1) .build(); const serverRequest = new ServerRequestBuilder() .withData(ttxRequest) .withUrl('https://foo.com/hb/') .build(); - const builtServerRequests = spec.buildRequests(bidRequests, bidderRequest); + const [ builtServerRequest ] = spec.buildRequests(bidRequests, bidderRequest); - expect(builtServerRequests).to.deep.equal([serverRequest]); + validateBuiltServerRequest(builtServerRequest, serverRequest); }); }); @@ -471,13 +752,15 @@ describe('33acrossBidAdapter:', function () { it('returns corresponding server requests with default gdpr consent data', function() { const ttxRequest = new TtxRequestBuilder() + .withBanner() + .withProduct() .build(); const serverRequest = new ServerRequestBuilder() .withData(ttxRequest) .build(); - const builtServerRequests = spec.buildRequests(bidRequests, bidderRequest); + const [ builtServerRequest ] = spec.buildRequests(bidRequests, bidderRequest); - expect(builtServerRequests).to.deep.equal([serverRequest]); + validateBuiltServerRequest(builtServerRequest, serverRequest); }); it('returns corresponding test server requests with default gdpr consent data', function() { @@ -488,14 +771,16 @@ describe('33acrossBidAdapter:', function () { }); const ttxRequest = new TtxRequestBuilder() + .withBanner() + .withProduct() .build(); const serverRequest = new ServerRequestBuilder() .withData(ttxRequest) .withUrl('https://foo.com/hb/') .build(); - const builtServerRequests = spec.buildRequests(bidRequests, bidderRequest); + const [ builtServerRequest ] = spec.buildRequests(bidRequests, bidderRequest); - expect(builtServerRequests).to.deep.equal([serverRequest]); + validateBuiltServerRequest(builtServerRequest, serverRequest); }); }); @@ -510,14 +795,16 @@ describe('33acrossBidAdapter:', function () { it('returns corresponding server requests with us_privacy consent data', function() { const ttxRequest = new TtxRequestBuilder() + .withBanner() + .withProduct() .withUspConsent('foo') .build(); const serverRequest = new ServerRequestBuilder() .withData(ttxRequest) .build(); - const builtServerRequests = spec.buildRequests(bidRequests, bidderRequest); + const [ builtServerRequest ] = spec.buildRequests(bidRequests, bidderRequest); - expect(builtServerRequests).to.deep.equal([serverRequest]); + validateBuiltServerRequest(builtServerRequest, serverRequest); }); it('returns corresponding test server requests with us_privacy consent data', function() { @@ -528,15 +815,17 @@ describe('33acrossBidAdapter:', function () { }); const ttxRequest = new TtxRequestBuilder() + .withBanner() + .withProduct() .withUspConsent('foo') .build(); const serverRequest = new ServerRequestBuilder() .withData(ttxRequest) .withUrl('https://foo.com/hb/') .build(); - const builtServerRequests = spec.buildRequests(bidRequests, bidderRequest); + const [ builtServerRequest ] = spec.buildRequests(bidRequests, bidderRequest); - expect(builtServerRequests).to.deep.equal([serverRequest]); + validateBuiltServerRequest(builtServerRequest, serverRequest); }); }); @@ -549,13 +838,15 @@ describe('33acrossBidAdapter:', function () { it('returns corresponding server requests with default us_privacy data', function() { const ttxRequest = new TtxRequestBuilder() + .withBanner() + .withProduct() .build(); const serverRequest = new ServerRequestBuilder() .withData(ttxRequest) .build(); - const builtServerRequests = spec.buildRequests(bidRequests, bidderRequest); + const [ builtServerRequest ] = spec.buildRequests(bidRequests, bidderRequest); - expect(builtServerRequests).to.deep.equal([serverRequest]); + validateBuiltServerRequest(builtServerRequest, serverRequest); }); it('returns corresponding test server requests with default us_privacy consent data', function() { @@ -566,14 +857,16 @@ describe('33acrossBidAdapter:', function () { }); const ttxRequest = new TtxRequestBuilder() + .withBanner() + .withProduct() .build(); const serverRequest = new ServerRequestBuilder() .withData(ttxRequest) .withUrl('https://foo.com/hb/') .build(); - const builtServerRequests = spec.buildRequests(bidRequests, bidderRequest); + const [ builtServerRequest ] = spec.buildRequests(bidRequests, bidderRequest); - expect(builtServerRequests).to.deep.equal([serverRequest]); + validateBuiltServerRequest(builtServerRequest, serverRequest); }); }); @@ -586,15 +879,17 @@ describe('33acrossBidAdapter:', function () { }; const ttxRequest = new TtxRequestBuilder() + .withBanner() + .withProduct() .withPageUrl('http://foo.com/bar') .build(); const serverRequest = new ServerRequestBuilder() .withData(ttxRequest) .build(); - const builtServerRequests = spec.buildRequests(bidRequests, bidderRequest); + const [ builtServerRequest ] = spec.buildRequests(bidRequests, bidderRequest); - expect(builtServerRequests).to.deep.equal([serverRequest]); + validateBuiltServerRequest(builtServerRequest, serverRequest); }); }); @@ -605,14 +900,16 @@ describe('33acrossBidAdapter:', function () { }; const ttxRequest = new TtxRequestBuilder() + .withBanner() + .withProduct() .build(); const serverRequest = new ServerRequestBuilder() .withData(ttxRequest) .build(); - const builtServerRequests = spec.buildRequests(bidRequests, bidderRequest); + const [ builtServerRequest ] = spec.buildRequests(bidRequests, bidderRequest); - expect(builtServerRequests).to.deep.equal([serverRequest]); + validateBuiltServerRequest(builtServerRequest, serverRequest); }); }); @@ -656,15 +953,17 @@ describe('33acrossBidAdapter:', function () { bidRequests[0].schain = schain; const ttxRequest = new TtxRequestBuilder() + .withBanner() + .withProduct() .withSchain(schain) .build(); const serverRequest = new ServerRequestBuilder() .withData(ttxRequest) .build(); - const builtServerRequests = spec.buildRequests(bidRequests, {}); + const [ builtServerRequest ] = spec.buildRequests(bidRequests, {}); - expect(builtServerRequests).to.deep.equal([serverRequest]); + validateBuiltServerRequest(builtServerRequest, serverRequest); }); }); }); @@ -672,43 +971,49 @@ describe('33acrossBidAdapter:', function () { context('when there no schain object is passed', function() { it('does not set source field', function() { const ttxRequest = new TtxRequestBuilder() + .withBanner() + .withProduct() .build(); const serverRequest = new ServerRequestBuilder() .withData(ttxRequest) .build(); - const builtServerRequests = spec.buildRequests(bidRequests, {}); + const [ builtServerRequest ] = spec.buildRequests(bidRequests, {}); - expect(builtServerRequests).to.deep.equal([serverRequest]); + validateBuiltServerRequest(builtServerRequest, serverRequest); }); }); - context('when price floor module is not enabled in bidRequest', function() { + context('when price floor module is not enabled for banner in bidRequest', function() { it('does not set any bidfloors in ttxRequest', function() { const ttxRequest = new TtxRequestBuilder() + .withBanner() + .withProduct() .build(); const serverRequest = new ServerRequestBuilder() .withData(ttxRequest) .build(); - const builtServerRequests = spec.buildRequests(bidRequests, {}); + const [ builtServerRequest ] = spec.buildRequests(bidRequests, {}); - expect(builtServerRequests).to.deep.equal([serverRequest]); + validateBuiltServerRequest(builtServerRequest, serverRequest); }); }); - context('when price floor module is enabled in bidRequest', function() { + context('when price floor module is enabled for banner in bidRequest', function() { it('does not set any bidfloors in ttxRequest if there is no floor', function() { bidRequests[0].getFloor = () => ({}); const ttxRequest = new TtxRequestBuilder() + .withBanner() + .withProduct() .build(); const serverRequest = new ServerRequestBuilder() .withData(ttxRequest) .build(); - const builtServerRequests = spec.buildRequests(bidRequests, {}); + const [ builtServerRequest ] = spec.buildRequests(bidRequests, {}); - expect(builtServerRequests).to.deep.equal([serverRequest]); + validateBuiltServerRequest(builtServerRequest, serverRequest); }); it('sets bidfloors in ttxRequest if there is a floor', function() { @@ -723,15 +1028,352 @@ describe('33acrossBidAdapter:', function () { }; const ttxRequest = new TtxRequestBuilder() - .withFormatFloors([ 1.0, 0.10 ]) + .withBanner() + .withProduct() + .withFormatFloors('banner', [ 1.0, 0.10 ]) + .build(); + + const serverRequest = new ServerRequestBuilder() + .withData(ttxRequest) + .build(); + const [ builtServerRequest ] = spec.buildRequests(bidRequests, {}); + + validateBuiltServerRequest(builtServerRequest, serverRequest); + }); + }); + + context('when mediaType has video only and context is instream', function() { + it('builds instream request with default params', function() { + const bidRequests = ( + new BidRequestsBuilder() + .withVideo({context: 'instream'}) + .build() + ); + + const ttxRequest = new TtxRequestBuilder() + .withVideo() + .withProduct('instream') .build(); + ttxRequest.imp[0].video.placement = 1; + ttxRequest.imp[0].video.startdelay = 0; + const serverRequest = new ServerRequestBuilder() .withData(ttxRequest) .build(); - const builtServerRequests = spec.buildRequests(bidRequests, {}); + const [ builtServerRequest ] = spec.buildRequests(bidRequests, {}); + + validateBuiltServerRequest(builtServerRequest, serverRequest); + }); + + it('builds instream request with params passed', function() { + const bidRequests = ( + new BidRequestsBuilder() + .withVideo({context: 'instream', startdelay: -2}) + .build() + ); + + const ttxRequest = new TtxRequestBuilder() + .withVideo({startdelay: -2, placement: 1}) + .withProduct('instream') + .build(); + + const [ builtServerRequest ] = spec.buildRequests(bidRequests, {}); + + expect(JSON.parse(builtServerRequest.data)).to.deep.equal(ttxRequest); + }); + }); + + context('when mediaType has video only and context is outstream', function() { + it('builds siab request with video only with default params', function() { + const bidRequests = ( + new BidRequestsBuilder() + .withVideo({context: 'outstream'}) + .build() + ); + + const ttxRequest = new TtxRequestBuilder() + .withVideo() + .withProduct('siab') + .build(); + + ttxRequest.imp[0].video.placement = 2; + + const serverRequest = new ServerRequestBuilder() + .withData(ttxRequest) + .build(); + const [ builtServerRequest ] = spec.buildRequests(bidRequests, {}); + + validateBuiltServerRequest(builtServerRequest, serverRequest); + }); + + it('builds siab request with video params passed', function() { + const bidRequests = ( + new BidRequestsBuilder() + .withVideo({context: 'outstream', placement: 3, playbackmethod: [2]}) + .build() + ); + + const ttxRequest = new TtxRequestBuilder() + .withVideo({placement: 3, playbackmethod: [2]}) + .withProduct('siab') + .build(); + + const serverRequest = new ServerRequestBuilder() + .withData(ttxRequest) + .build(); + const [ builtServerRequest ] = spec.buildRequests(bidRequests, {}); + + validateBuiltServerRequest(builtServerRequest, serverRequest); + }); + }); + + context('when mediaType has banner only', function() { + it('builds default siab request', function() { + const bidRequests = ( + new BidRequestsBuilder() + .withBanner() + .build() + ); + + const ttxRequest = new TtxRequestBuilder() + .withBanner() + .withProduct('siab') + .build(); + + const serverRequest = new ServerRequestBuilder() + .withData(ttxRequest) + .build(); + const [ builtServerRequest ] = spec.buildRequests(bidRequests, {}); + + validateBuiltServerRequest(builtServerRequest, serverRequest); + }); + + it('builds default inview request when product is set as such', function() { + const bidRequests = ( + new BidRequestsBuilder() + .withBanner() + .withProduct('inview') + .build() + ); + + const ttxRequest = new TtxRequestBuilder() + .withBanner() + .withProduct('inview') + .build(); + + const serverRequest = new ServerRequestBuilder() + .withData(ttxRequest) + .build(); + const [ builtServerRequest ] = spec.buildRequests(bidRequests, {}); + + validateBuiltServerRequest(builtServerRequest, serverRequest); + }); + }); + + context('when mediaType has banner and video', function() { + it('builds siab request with banner and outstream video', function() { + const bidRequests = ( + new BidRequestsBuilder() + .withBanner() + .withVideo({context: 'outstream'}) + .build() + ); + + const ttxRequest = new TtxRequestBuilder() + .withBanner() + .withVideo() + .withProduct('siab') + .build(); + + const serverRequest = new ServerRequestBuilder() + .withData(ttxRequest) + .build(); + const [ builtServerRequest ] = spec.buildRequests(bidRequests, {}); + + validateBuiltServerRequest(builtServerRequest, serverRequest); + }); + + it('builds siab request with banner and outstream video even when context is instream', function() { + const bidRequests = ( + new BidRequestsBuilder() + .withBanner() + .withVideo({context: 'instream'}) + .build() + ); + + const ttxRequest = new TtxRequestBuilder() + .withBanner() + .withVideo() + .withProduct('siab') + .build(); + + ttxRequest.imp[0].video.placement = 2; + + const serverRequest = new ServerRequestBuilder() + .withData(ttxRequest) + .build(); + const [ builtServerRequest ] = spec.buildRequests(bidRequests, {}); + + validateBuiltServerRequest(builtServerRequest, serverRequest); + }); + }); + + context('when price floor module is enabled for video in bidRequest', function() { + it('does not set any bidfloors in video if there is no floor', function() { + const bidRequests = ( + new BidRequestsBuilder() + .withVideo({context: 'outstream'}) + .build() + ); + + bidRequests[0].getFloor = () => ({}); + + const ttxRequest = new TtxRequestBuilder() + .withVideo() + .withProduct() + .build(); + + const [ builtServerRequest ] = spec.buildRequests(bidRequests, {}); + + expect(JSON.parse(builtServerRequest.data)).to.deep.equal(ttxRequest); + }); + + it('sets bidfloors in video if there is a floor', function() { + const bidRequests = ( + new BidRequestsBuilder() + .withVideo({context: 'outstream'}) + .build() + ); + + bidRequests[0].getFloor = ({size, currency, mediaType}) => { + const floor = (mediaType === 'video') ? 1.0 : 0.10 + return ( + { + floor, + currency: 'USD' + } + ); + }; + + const ttxRequest = new TtxRequestBuilder() + .withVideo() + .withProduct() + .withFloors('video', [ 1.0 ]) + .build(); + + const [ builtServerRequest ] = spec.buildRequests(bidRequests, {}); + + expect(JSON.parse(builtServerRequest.data)).to.deep.equal(ttxRequest); + }); + }); + + context('when user ID data exists as userIdAsEids Array in bidRequest', function() { + it('passes userIds in eids field in ORTB request', function() { + const eids = [ + { + 'source': 'x-device-vendor-x.com', + 'uids': [ + { + 'id': 'yyy', + 'atype': 1 + }, + { + 'id': 'zzz', + 'atype': 1 + }, + { + 'id': 'DB700403-9A24-4A4B-A8D5-8A0B4BE777D2', + 'atype': 2 + } + ], + 'ext': { + 'foo': 'bar' + } + } + ]; + + const bidRequests = ( + new BidRequestsBuilder() + .withUserIds(eids) + .build() + ); + + const ttxRequest = new TtxRequestBuilder() + .withUserIds(eids) + .withProduct() + .build(); + + const [ builtServerRequest ] = spec.buildRequests(bidRequests, {}); - expect(builtServerRequests).to.deep.equal([serverRequest]); + expect(JSON.parse(builtServerRequest.data)).to.deep.equal(ttxRequest); + }); + + it('does not validate eids ORTB', function() { + const eids = [1, 2, 3]; + + const bidRequests = ( + new BidRequestsBuilder() + .withUserIds(eids) + .build() + ); + + const ttxRequest = new TtxRequestBuilder() + .withUserIds(eids) + .withProduct() + .build(); + + const [ builtServerRequest ] = spec.buildRequests(bidRequests, {}); + + expect(JSON.parse(builtServerRequest.data)).to.deep.equal(ttxRequest); + }); + }); + + context('when user IDs do not exist under the userIdAsEids field in bidRequest as a non-empty Array', function() { + it('does not pass user IDs in the bidRequest ORTB', function() { + const eidsScenarios = [ + 'foo', + [], + {foo: 1} + ]; + + eidsScenarios.forEach((eids) => { + const bidRequests = ( + new BidRequestsBuilder() + .withUserIds(eids) + .build() + ); + bidRequests.userId = { + 'vendorx': { + 'source': 'x-device-vendor-x.com', + 'uids': [ + { + 'id': 'yyy', + 'atype': 1 + }, + { + 'id': 'zzz', + 'atype': 1 + }, + { + 'id': 'DB700403-9A24-4A4B-A8D5-8A0B4BE777D2', + 'atype': 2 + } + ], + 'ext': { + 'foo': 'bar' + } + } + }; + + const ttxRequest = new TtxRequestBuilder() + .withProduct() + .build(); + + const [ builtServerRequest ] = spec.buildRequests(bidRequests, {}); + + expect(JSON.parse(builtServerRequest.data)).to.deep.equal(ttxRequest); + }); }); }); }); @@ -741,6 +1383,8 @@ describe('33acrossBidAdapter:', function () { beforeEach(function() { ttxRequest = new TtxRequestBuilder() + .withBanner() + .withProduct() .withSite({ id: SITE_ID, page: 'https://test-url.com' @@ -757,7 +1401,7 @@ describe('33acrossBidAdapter:', function () { }); context('when exactly one bid is returned', function() { - it('interprets and returns the single bid response', function() { + it('interprets and returns the single banner bid response', function() { const serverResponse = { cur: 'USD', ext: {}, @@ -770,7 +1414,8 @@ describe('33acrossBidAdapter:', function () { crid: 1, h: 250, w: 300, - price: 0.0938 + price: 0.0938, + adomain: ['advertiserdomain.com'] }] } ] @@ -784,12 +1429,103 @@ describe('33acrossBidAdapter:', function () { ad: '

I am an ad

', ttl: 60, creativeId: 1, + mediaType: 'banner', currency: 'USD', - netRevenue: true + netRevenue: true, + meta: { + advertiserDomains: ['advertiserdomain.com'] + } }; expect(spec.interpretResponse({ body: serverResponse }, serverRequest)).to.deep.equal([bidResponse]); }); + + it('interprets and returns the single video bid response', function() { + const videoBid = ''; + const serverResponse = { + cur: 'USD', + ext: {}, + id: 'b1', + seatbid: [ + { + bid: [{ + id: '1', + adm: videoBid, + ext: { + ttx: { + mediaType: 'video', + vastType: 'xml' + } + }, + crid: 1, + h: 250, + w: 300, + price: 0.0938, + adomain: ['advertiserdomain.com'] + }] + } + ] + }; + const bidResponse = { + requestId: 'b1', + bidderCode: BIDDER_CODE, + cpm: 0.0938, + width: 300, + height: 250, + ad: videoBid, + ttl: 60, + creativeId: 1, + mediaType: 'video', + currency: 'USD', + netRevenue: true, + vastXml: videoBid, + meta: { + advertiserDomains: ['advertiserdomain.com'] + } + }; + + expect(spec.interpretResponse({ body: serverResponse }, serverRequest)).to.deep.equal([bidResponse]); + }); + + context('when the list of advertiser domains for block list checking is empty', function() { + it('doesn\'t include the empty list in the interpreted response', function() { + const serverResponse = { + cur: 'USD', + ext: {}, + id: 'b1', + seatbid: [ + { + bid: [{ + id: '1', + adm: '

I am an ad

', + crid: 1, + h: 250, + w: 300, + price: 0.0938, + adomain: [] // Empty list + }] + } + ] + }; + + // Bid response below doesn't contain meta.advertiserDomains + const bidResponse = { + requestId: 'b1', + bidderCode: BIDDER_CODE, + cpm: 0.0938, + width: 300, + height: 250, + ad: '

I am an ad

', + ttl: 60, + creativeId: 1, + mediaType: 'banner', + currency: 'USD', + netRevenue: true + }; + + expect(spec.interpretResponse({ body: serverResponse }, serverRequest)).to.deep.equal([bidResponse]); + }); + }); }); context('when no bids are returned', function() { @@ -852,6 +1588,7 @@ describe('33acrossBidAdapter:', function () { ad: '

I am an ad

', ttl: 60, creativeId: 1, + mediaType: 'banner', currency: 'USD', netRevenue: true }; @@ -886,9 +1623,13 @@ describe('33acrossBidAdapter:', function () { }, adUnitCode: 'div-id', auctionId: 'r1', - sizes: [ - [300, 250] - ], + mediaTypes: { + banner: { + sizes: [ + [300, 250] + ] + } + }, transactionId: 't1' }, { @@ -901,9 +1642,13 @@ describe('33acrossBidAdapter:', function () { }, adUnitCode: 'div-id', auctionId: 'r1', - sizes: [ - [300, 250] - ], + mediaTypes: { + banner: { + sizes: [ + [300, 250] + ] + } + }, transactionId: 't2' } ]; diff --git a/test/spec/modules/a4gBidAdapter_spec.js b/test/spec/modules/a4gBidAdapter_spec.js new file mode 100644 index 00000000000..cb05fa62ab6 --- /dev/null +++ b/test/spec/modules/a4gBidAdapter_spec.js @@ -0,0 +1,219 @@ +import { expect } from 'chai'; +import { spec } from 'modules/a4gBidAdapter.js'; +import * as utils from 'src/utils.js'; + +describe('a4gAdapterTests', function () { + describe('bidRequestValidity', function () { + it('bidRequest with zoneId and deliveryUrl params', function () { + expect(spec.isBidRequestValid({ + bidder: 'a4g', + params: { + zoneId: 59304, + deliveryUrl: 'http://dev01.ad4game.com/v1/bid' + } + })).to.equal(true); + }); + + it('bidRequest with only zoneId', function () { + expect(spec.isBidRequestValid({ + bidder: 'a4g', + params: { + zoneId: 59304 + } + })).to.equal(true); + }); + + it('bidRequest with only deliveryUrl', function () { + expect(spec.isBidRequestValid({ + bidder: 'a4g', + params: { + deliveryUrl: 'http://dev01.ad4game.com/v1/bid' + } + })).to.equal(false); + }); + }); + + describe('bidRequest', function () { + const DEFAULT_OPTION = { + refererInfo: { + referer: 'https://www.prebid.org', + canonicalUrl: 'https://www.prebid.org/the/link/to/the/page' + } + }; + + const bidRequests = [{ + 'bidder': 'a4g', + 'bidId': '51ef8751f9aead', + 'params': { + 'zoneId': 59304, + }, + 'adUnitCode': 'div-gpt-ad-1460505748561-0', + 'transactionId': 'd7b773de-ceaa-484d-89ca-d9f51b8d61ec', + 'mediaTypes': { + 'banner': { + 'sizes': [[300, 50], [300, 250], [300, 600]] + } + }, + 'sizes': [[320, 50], [300, 250], [300, 600]], + 'bidderRequestId': '418b37f85e772c', + 'auctionId': '18fd8b8b0bd757' + }, { + 'bidder': 'a4g', + 'bidId': '51ef8751f9aead', + 'params': { + 'zoneId': 59354, + 'deliveryUrl': '//dev01.ad4game.com/v1/bid' + }, + 'adUnitCode': 'div-gpt-ad-1460505748561-0', + 'transactionId': 'd7b773de-ceaa-484d-89ca-d9f51b8d61ec', + 'mediaTypes': { + 'banner': { + 'sizes': [[300, 50], [300, 250], [300, 600]] + } + }, + 'bidderRequestId': '418b37f85e772c', + 'auctionId': '18fd8b8b0bd757' + }]; + + it('bidRequest method', function () { + const request = spec.buildRequests(bidRequests, DEFAULT_OPTION); + expect(request.method).to.equal('GET'); + }); + + it('bidRequest url', function () { + const request = spec.buildRequests(bidRequests, DEFAULT_OPTION); + expect(request.url).to.match(new RegExp(`${bidRequests[1].params.deliveryUrl}`)); + }); + + it('bidRequest data', function () { + const request = spec.buildRequests(bidRequests, DEFAULT_OPTION); + expect(request.data).to.exist; + }); + + it('bidRequest zoneIds', function () { + const request = spec.buildRequests(bidRequests, DEFAULT_OPTION); + expect(request.data.zoneId).to.equal('59304;59354'); + }); + + it('bidRequest gdpr consent', function () { + const consentString = 'consentString'; + const bidderRequest = { + bidderCode: 'a4g', + auctionId: '18fd8b8b0bd757', + bidderRequestId: '418b37f85e772c', + timeout: 3000, + gdprConsent: { + consentString: consentString, + gdprApplies: true + }, + refererInfo: { + referer: 'https://www.prebid.org', + canonicalUrl: 'https://www.prebid.org/the/link/to/the/page' + } + }; + + const request = spec.buildRequests(bidRequests, bidderRequest); + + expect(request.data.gdpr).to.exist; + expect(request.data.gdpr.applies).to.exist.and.to.be.true; + expect(request.data.gdpr.consent).to.exist.and.to.equal(consentString); + }); + }); + + describe('interpretResponse', function () { + const bidRequest = [{ + 'bidder': 'a4g', + 'bidId': '51ef8751f9aead', + 'params': { + 'zoneId': 59304, + }, + 'adUnitCode': 'div-gpt-ad-1460505748561-0', + 'transactionId': 'd7b773de-ceaa-484d-89ca-d9f51b8d61ec', + 'mediaTypes': { + 'banner': { + 'sizes': [[300, 50], [300, 250], [300, 600]] + } + }, + 'bidderRequestId': '418b37f85e772c', + 'auctionId': '18fd8b8b0bd757' + }]; + + const bidResponse = { + body: [{ + 'id': '51ef8751f9aead', + 'ad': 'test ad', + 'width': 320, + 'height': 250, + 'cpm': 5.2, + 'crid': '111' + }], + headers: {} + }; + + it('should get correct bid response for banner ad', function () { + const expectedParse = [ + { + requestId: '51ef8751f9aead', + cpm: 5.2, + creativeId: '111', + width: 320, + height: 250, + ad: 'test ad', + currency: 'USD', + ttl: 120, + netRevenue: true + } + ]; + const result = spec.interpretResponse(bidResponse, bidRequest); + expect(result[0]).to.deep.equal(expectedParse[0]); + }); + + it('should set creativeId to default value if not provided', function () { + const bidResponseWithoutCrid = utils.deepClone(bidResponse); + delete bidResponseWithoutCrid.body[0].crid; + const expectedParse = [ + { + requestId: '51ef8751f9aead', + cpm: 5.2, + creativeId: '51ef8751f9aead', + width: 320, + height: 250, + ad: 'test ad', + currency: 'USD', + ttl: 120, + netRevenue: true + } + ]; + const result = spec.interpretResponse(bidResponseWithoutCrid, bidRequest); + expect(result[0]).to.deep.equal(expectedParse[0]); + }) + + it('required keys', function () { + const result = spec.interpretResponse(bidResponse, bidRequest); + + let requiredKeys = [ + 'requestId', + 'creativeId', + 'adId', + 'cpm', + 'width', + 'height', + 'currency', + 'netRevenue', + 'ttl', + 'ad' + ]; + + let resultKeys = Object.keys(result[0]); + resultKeys.forEach(function(key) { + expect(requiredKeys.indexOf(key) !== -1).to.equal(true); + }); + }) + + it('adId should not be equal to requestId', function () { + const result = spec.interpretResponse(bidResponse, bidRequest); + + expect(result[0].requestId).to.not.equal(result[0].adId); + }) + }); +}); diff --git a/test/spec/modules/ablidaBidAdapter_spec.js b/test/spec/modules/ablidaBidAdapter_spec.js index 0238c14fe5c..73109d8cf16 100644 --- a/test/spec/modules/ablidaBidAdapter_spec.js +++ b/test/spec/modules/ablidaBidAdapter_spec.js @@ -1,6 +1,7 @@ import {assert, expect} from 'chai'; import {spec} from 'modules/ablidaBidAdapter.js'; import {newBidder} from 'src/adapters/bidderFactory.js'; +import * as utils from 'src/utils.js'; const ENDPOINT_URL = 'https://bidder.ablida.net/prebid'; @@ -8,17 +9,23 @@ describe('ablidaBidAdapter', function () { const adapter = newBidder(spec); describe('isBidRequestValid', function () { let bid = { + adUnitCode: 'adunit-code', + auctionId: '69e8fef8-5105-4a99-b011-d5669f3bc7f0', + bidRequestsCount: 1, bidder: 'ablida', + bidderRequestId: '14d2939272a26a', + bidderRequestsCount: 1, + bidderWinsCount: 0, + bidId: '1234asdf1234', + mediaTypes: {banner: {sizes: [[300, 250]]}}, params: { placementId: 123 }, - adUnitCode: 'adunit-code', sizes: [ [300, 250] ], - bidId: '1234asdf1234', - bidderRequestId: '1234asdf1234asdf', - auctionId: '69e8fef8-5105-4a99-b011-d5669f3bc7f0' + src: 'client', + transactionId: '4781e6ac-93c4-42ba-86fe-ab5f278863cf' }; it('should return true where required params found', function () { expect(spec.isBidRequestValid(bid)).to.equal(true); @@ -27,22 +34,29 @@ describe('ablidaBidAdapter', function () { describe('buildRequests', function () { let bidRequests = [ { + adUnitCode: 'adunit-code', + auctionId: '69e8fef8-5105-4a99-b011-d5669f3bc7f0', + bidId: '23beaa6af6cdde', + bidRequestsCount: 1, bidder: 'ablida', + bidderRequestId: '14d2939272a26a', + bidderRequestsCount: 1, + bidderWinsCount: 0, + mediaTypes: {banner: {sizes: [[300, 250]]}}, params: { placementId: 123 }, sizes: [ [300, 250] ], - adUnitCode: 'adunit-code', - bidId: '23beaa6af6cdde', - bidderRequestId: '14d2939272a26a', - auctionId: '69e8fef8-5105-4a99-b011-d5669f3bc7f0', + src: 'client', + transactionId: '4781e6ac-93c4-42ba-86fe-ab5f278863cf' } ]; let bidderRequests = { refererInfo: { + canonicalUrl: '', numIframes: 0, reachedTop: true, referer: 'http://example.com', @@ -61,42 +75,53 @@ describe('ablidaBidAdapter', function () { method: 'POST', url: ENDPOINT_URL, data: { + adapterVersion: 5, + bidId: '2b8c4de0116e54', + categories: undefined, + device: 'desktop', + gdprConsent: undefined, + jaySupported: true, + mediaTypes: {banner: {sizes: [[300, 250]]}}, placementId: 'testPlacementId', width: 300, height: 200, - bidId: '2b8c4de0116e54', - jaySupported: true, - device: 'desktop', - referer: 'www.example.com', - adapterVersion: 2 + referer: 'www.example.com' } }; let serverResponse = { body: [{ - requestId: '2b8c4de0116e54', + ad: '', cpm: 1.00, - width: 300, - height: 250, creativeId: '2b8c4de0116e54', currency: 'EUR', + height: 250, + mediaType: 'banner', + meta: {}, netRevenue: true, + nurl: 'https://example.com/some-tracker', + originalCpm: '0.10', + originalCurrency: 'EUR', + requestId: '2b8c4de0116e54', ttl: 3000, - ad: '', - nurl: 'https://example.com/some-tracker' + width: 300 }] }; it('should get the correct bid response', function () { let expectedResponse = [{ - requestId: '2b8c4de0116e54', + ad: '', cpm: 1.00, - width: 300, - height: 250, creativeId: '2b8c4de0116e54', currency: 'EUR', + height: 250, + mediaType: 'banner', + meta: {}, netRevenue: true, + nurl: 'https://example.com/some-tracker', + originalCpm: '0.10', + originalCurrency: 'EUR', + requestId: '2b8c4de0116e54', ttl: 3000, - ad: '', - nurl: 'https://example.com/some-tracker' + width: 300 }]; let result = spec.interpretResponse(serverResponse, bidRequest[0]); expect(Object.keys(result)).to.deep.equal(Object.keys(expectedResponse)); @@ -104,16 +129,23 @@ describe('ablidaBidAdapter', function () { }); describe('onBidWon', function() { - it('Should not ajax call if bid does not contain nurl', function() { + beforeEach(function() { + sinon.stub(utils, 'triggerPixel'); + }); + afterEach(function() { + utils.triggerPixel.restore(); + }); + + it('Should not trigger pixel if bid does not contain nurl', function() { const result = spec.onBidWon({}); - expect(result).to.equal(false) + expect(utils.triggerPixel.callCount).to.equal(0) }) - it('Should ajax call if bid nurl', function() { + it('Should trigger pixel if bid nurl', function() { const result = spec.onBidWon({ nurl: 'https://example.com/some-tracker' }); - expect(result).to.equal(true) + expect(utils.triggerPixel.callCount).to.equal(1) }) }) }); diff --git a/test/spec/modules/adWMGBidAdapter_spec.js b/test/spec/modules/adWMGBidAdapter_spec.js new file mode 100644 index 00000000000..8b927ace84c --- /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://hb.adwmg.com/hb'); + expect(requests[1].url).to.equal('https://hb.adwmg.com/hb'); + }); + + 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://hb.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://hb.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 52b99f274d5..6ee42e47950 100644 --- a/test/spec/modules/adagioBidAdapter_spec.js +++ b/test/spec/modules/adagioBidAdapter_spec.js @@ -1,8 +1,20 @@ import find from 'core-js-pure/features/array/find.js'; -import { expect } from 'chai'; -import { _features, internal as adagio, adagioScriptFromLocalStorageCb, getAdagioScript, storage, spec, ENDPOINT, VERSION } from '../../../modules/adagioBidAdapter.js'; +import { expect, util } from 'chai'; +import { + _features, + internal as adagio, + adagioScriptFromLocalStorageCb, + getAdagioScript, + storage, + spec, + ENDPOINT, + VERSION, + RENDERER_URL +} from '../../../modules/adagioBidAdapter.js'; import { loadExternalScript } from '../../../src/adloader.js'; import * as utils from '../../../src/utils.js'; +import { config } from '../../../src/config.js'; +import { NATIVE } from '../../../src/mediaTypes.js'; const BidRequestBuilder = function BidRequestBuilder(options) { const defaults = { @@ -106,7 +118,7 @@ describe('Adagio bid adapter', () => { adagioMock = sinon.mock(adagio); utilsMock = sinon.mock(utils); - sandbox = sinon.sandbox.create(); + sandbox = sinon.createSandbox(); }); afterEach(() => { @@ -133,6 +145,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', @@ -218,7 +243,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', @@ -238,7 +264,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,11 +276,12 @@ 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 = { ...window.ADAGIO - } + }; spec.isBidRequestValid(bid01); spec.isBidRequestValid(bid02); @@ -263,8 +291,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); @@ -285,11 +327,13 @@ describe('Adagio bid adapter', () => { 'site', 'pageviewId', 'adUnits', - 'gdpr', + 'regs', + 'user', 'schain', 'prebidVersion', 'adapterVersion', - 'featuresVersion' + 'featuresVersion', + 'data' ]; it('groups requests by organizationId', function() { @@ -382,6 +426,81 @@ describe('Adagio bid adapter', () => { expect(requests[0].data.adUnits[0].features.url).to.not.exist; }); + describe('With video mediatype', function() { + context('Outstream video', function() { + it('should logWarn if user does not set renderer.backupOnly: true', function() { + sandbox.spy(utils, 'logWarn'); + const bid01 = new BidRequestBuilder({ + adUnitCode: 'adunit-code-01', + mediaTypes: { + banner: { sizes: [[300, 250]] }, + video: { + context: 'outstream', + playerSize: [[300, 250]], + renderer: { + url: 'https://url.tld', + render: () => true + } + } + }, + }).withParams().build(); + const bidderRequest = new BidderRequestBuilder().build(); + const request = spec.buildRequests([bid01], bidderRequest)[0]; + + expect(request.data.adUnits[0].mediaTypes.video.playerName).to.equal('other'); + sinon.assert.calledWith(utils.logWarn, 'Adagio: renderer.backupOnly has not been set. Adagio recommends to use its own player to get expected behavior.'); + }); + }); + + it('Update mediaTypes.video with OpenRTB options. Validate and sanitize whitelisted OpenRTB', function() { + sandbox.spy(utils, 'logWarn'); + const bid01 = new BidRequestBuilder({ + adUnitCode: 'adunit-code-01', + mediaTypes: { + banner: { sizes: [[300, 250]] }, + video: { + context: 'outstream', + playerSize: [[300, 250]], + mimes: ['video/mp4'], + api: 5, // will be removed because invalid + playbackmethod: [7], // will be removed because invalid + } + }, + }).withParams({ + // options in video, will overide + video: { + skip: 1, + skipafter: 4, + minduration: 10, + maxduration: 30, + placement: [3], + protocols: [8] + } + }).build(); + + const bidderRequest = new BidderRequestBuilder().build(); + const expected = { + context: 'outstream', + playerSize: [[300, 250]], + playerName: 'adagio', + mimes: ['video/mp4'], + skip: 1, + skipafter: 4, + minduration: 10, + maxduration: 30, + placement: [3], + protocols: [8], + w: 300, + h: 250 + }; + + const requests = spec.buildRequests([bid01], bidderRequest); + expect(requests).to.have.lengthOf(1); + expect(requests[0].data.adUnits[0].mediaTypes.video).to.deep.equal(expected); + sinon.assert.calledTwice(utils.logWarn); + }); + }); + describe('with sChain', function() { const schain = { ver: '1.0', @@ -450,7 +569,7 @@ describe('Adagio bid adapter', () => { const requests = spec.buildRequests([bid01], bidderRequest); - expect(requests[0].data.gdpr).to.deep.equal(expected); + expect(requests[0].data.regs.gdpr).to.deep.equal(expected); }); it('send data.gdpr object to the server from TCF v.2 cmp', function() { @@ -466,7 +585,7 @@ describe('Adagio bid adapter', () => { const requests = spec.buildRequests([bid01], bidderRequest); - expect(requests[0].data.gdpr).to.deep.equal(expected); + expect(requests[0].data.regs.gdpr).to.deep.equal(expected); }); }); @@ -485,7 +604,7 @@ describe('Adagio bid adapter', () => { const requests = spec.buildRequests([bid01], bidderRequest); - expect(requests[0].data.gdpr).to.deep.equal(expected); + expect(requests[0].data.regs.gdpr).to.deep.equal(expected); }); it('send data.gdpr object to the server from TCF v.2 cmp', function() { @@ -501,7 +620,7 @@ describe('Adagio bid adapter', () => { const requests = spec.buildRequests([bid01], bidderRequest); - expect(requests[0].data.gdpr).to.deep.equal(expected); + expect(requests[0].data.regs.gdpr).to.deep.equal(expected); }); }); @@ -510,10 +629,115 @@ describe('Adagio bid adapter', () => { const bidderRequest = new BidderRequestBuilder().build(); const requests = spec.buildRequests([bid01], bidderRequest); - expect(requests[0].data.gdpr).to.be.empty; + expect(requests[0].data.regs.gdpr).to.be.empty; }); }); }); + + describe('with COPPA', function() { + const bid01 = new BidRequestBuilder().withParams().build(); + + it('should send the Coppa "required" flag set to "1" in the request', function () { + const bidderRequest = new BidderRequestBuilder().build(); + + sinon.stub(config, 'getConfig') + .withArgs('coppa') + .returns(true); + + const requests = spec.buildRequests([bid01], bidderRequest); + + expect(requests[0].data.regs.coppa.required).to.equal(1); + + config.getConfig.restore(); + }); + }); + + describe('without COPPA', function() { + const bid01 = new BidRequestBuilder().withParams().build(); + + it('should send the Coppa "required" flag set to "0" in the request', function () { + const bidderRequest = new BidderRequestBuilder().build(); + + const requests = spec.buildRequests([bid01], bidderRequest); + + expect(requests[0].data.regs.coppa.required).to.equal(0); + }); + }); + + describe('with USPrivacy', function() { + const bid01 = new BidRequestBuilder().withParams().build(); + + const consent = 'Y11N'; + + it('should send the USPrivacy "ccpa.uspConsent" in the request', function () { + const bidderRequest = new BidderRequestBuilder({ + uspConsent: consent + }).build(); + + const requests = spec.buildRequests([bid01], bidderRequest); + + expect(requests[0].data.regs.ccpa.uspConsent).to.equal(consent); + }); + }); + + describe('without USPrivacy', function() { + const bid01 = new BidRequestBuilder().withParams().build(); + + it('should have an empty "ccpa" field in the request', function () { + const bidderRequest = new BidderRequestBuilder().build(); + + const requests = spec.buildRequests([bid01], bidderRequest); + + expect(requests[0].data.regs.ccpa).to.be.empty; + }); + }); + + describe('with userID modules', function() { + const userId = { + sharedid: {id: '01EAJWWNEPN3CYMM5N8M5VXY22', third: '01EAJWWNEPN3CYMM5N8M5VXY22'}, + unsuported: '666' + }; + + it('should send "user.eids" in the request for Prebid.js supported modules only', function() { + const bid01 = new BidRequestBuilder({ + userId + }).withParams().build(); + + const bidderRequest = new BidderRequestBuilder().build(); + + const requests = spec.buildRequests([bid01], bidderRequest); + + const expected = [{ + source: 'sharedid.org', + uids: [ + { + atype: 1, + ext: { + third: '01EAJWWNEPN3CYMM5N8M5VXY22' + }, + id: '01EAJWWNEPN3CYMM5N8M5VXY22' + } + ] + }]; + + expect(requests[0].data.user.eids).to.have.lengthOf(1); + expect(requests[0].data.user.eids).to.deep.equal(expected); + }); + + it('should send an empty "user.eids" array in the request if userId module is unsupported', function() { + const bid01 = new BidRequestBuilder({ + userId: { + unsuported: '666' + } + }).withParams().build(); + + const bidderRequest = new BidderRequestBuilder().build(); + + const requests = spec.buildRequests([bid01], bidderRequest); + + expect(requests[0].data.user.eids).to.be.empty; + }); + }); }); describe('interpretResponse()', function() { @@ -531,7 +755,14 @@ describe('Adagio bid adapter', () => { netRevenue: true, requestId: 'c180kg4267tyqz', ttl: 360, - width: 300 + width: 300, + aDomain: ['advertiser.com'], + mediaType: 'banner', + meta: { + advertiserId: '80', + advertiserName: 'An Advertiser', + networkId: '110' + } }] } }; @@ -548,7 +779,8 @@ describe('Adagio bid adapter', () => { pagetype: 'ARTICLE', category: 'NEWS', subcategory: 'SPORT', - environment: 'desktop' + environment: 'desktop', + supportIObs: true }, adUnitCode: 'adunit-code', mediaTypes: { @@ -596,13 +828,53 @@ describe('Adagio bid adapter', () => { pagetype: 'ARTICLE', category: 'NEWS', subcategory: 'SPORT', - environment: 'desktop' + environment: 'desktop', + aDomain: ['advertiser.com'], + mediaType: 'banner', + meta: { + advertiserId: '80', + advertiserName: 'An Advertiser', + advertiserDomains: ['advertiser.com'], + networkId: '110', + mediaType: 'banner' + } }]; expect(spec.interpretResponse(serverResponse, bidRequest)).to.be.an('array'); expect(spec.interpretResponse(serverResponse, bidRequest)).to.deep.equal(expectedResponse); }); + it('Meta props should be limited if no bid.meta is provided', function() { + const altServerResponse = utils.deepClone(serverResponse); + delete altServerResponse.body.bids[0].meta; + + let expectedResponse = [{ + ad: '
', + cpm: 1, + creativeId: 'creativeId', + currency: 'EUR', + height: 250, + netRevenue: true, + requestId: 'c180kg4267tyqz', + ttl: 360, + width: 300, + placement: 'PAVE_ATF', + site: 'SITE-NAME', + pagetype: 'ARTICLE', + category: 'NEWS', + subcategory: 'SPORT', + environment: 'desktop', + aDomain: ['advertiser.com'], + mediaType: 'banner', + meta: { + advertiserDomains: ['advertiser.com'], + mediaType: 'banner' + } + }]; + + expect(spec.interpretResponse(altServerResponse, bidRequest)).to.deep.equal(expectedResponse); + }); + it('should populate ADAGIO queue with ssp-data', function() { sandbox.stub(Date, 'now').returns(12345); @@ -625,6 +897,205 @@ describe('Adagio bid adapter', () => { utilsMock.verify(); }); + + describe('Response with video outstream', () => { + const bidRequestWithOutstream = utils.deepClone(bidRequest); + bidRequestWithOutstream.data.adUnits[0].mediaTypes.video = { + context: 'outstream', + playerSize: [[300, 250]], + mimes: ['video/mp4'], + skip: true + }; + + const serverResponseWithOutstream = utils.deepClone(serverResponse); + serverResponseWithOutstream.body.bids[0].vastXml = ''; + serverResponseWithOutstream.body.bids[0].mediaType = 'video'; + serverResponseWithOutstream.body.bids[0].outstream = { + bvwUrl: 'https://foo.baz', + impUrl: 'https://foo.bar' + }; + + it('should set a renderer in video outstream context', function() { + const bidResponse = spec.interpretResponse(serverResponseWithOutstream, bidRequestWithOutstream)[0]; + expect(bidResponse).to.have.any.keys('outstream', 'renderer', 'mediaType'); + expect(bidResponse.renderer).to.be.a('object'); + expect(bidResponse.renderer.url).to.equal(RENDERER_URL); + expect(bidResponse.renderer.config.bvwUrl).to.be.ok; + expect(bidResponse.renderer.config.impUrl).to.be.ok; + expect(bidResponse.renderer.loaded).to.not.be.ok; + expect(bidResponse.width).to.equal(300); + expect(bidResponse.height).to.equal(250); + expect(bidResponse.vastUrl).to.match(/^data:text\/xml;/) + }); + }); + + describe('Response with native add', () => { + const serverResponseWithNative = utils.deepClone(serverResponse) + serverResponseWithNative.body.bids[0].mediaType = 'native'; + serverResponseWithNative.body.bids[0].admNative = { + ver: '1.2', + link: { + url: 'https://i.am.a.click.url', + clickTrackers: [ + 'https://i.am.a.clicktracker.url' + ] + }, + privacy: 'http://www.myprivacyurl.url', + ext: { + bvw: 'test' + }, + eventtrackers: [ + { + event: 1, + method: 1, + url: 'https://eventrack.local/impression' + }, + { + event: 1, + method: 2, + url: 'https://eventrack.local/impression' + }, + { + event: 2, + method: 1, + url: 'https://eventrack.local/viewable-mrc50' + } + ], + assets: [ + { + required: 1, + title: { + text: 'My title' + } + }, + { + img: { + url: 'https://images.local/image.jpg', + w: 100, + h: 250 + } + }, + { + img: { + type: 1, + url: 'https://images.local/icon.png', + w: 40, + h: 40 + } + }, + { + data: { + type: 1, // sponsored + value: 'Adagio' + } + }, + { + data: { + type: 2, // desc / body + value: 'The super ad text' + } + }, + { + data: { + type: 3, // rating + value: '10 from 10' + } + }, + { + data: { + type: 11, // displayUrl + value: 'https://i.am.a.display.url' + } + } + ] + }; + + const bidRequestNative = utils.deepClone(bidRequest) + bidRequestNative.mediaTypes = { + native: { + sendTargetingKeys: false, + + clickUrl: { + required: true, + }, + title: { + required: true, + }, + body: { + required: true, + }, + sponsoredBy: { + required: false + }, + image: { + required: true + }, + icon: { + required: true + }, + privacyLink: { + required: false + }, + ext: { + adagio_bvw: {} + } + } + }; + + it('Should ignore native parsing due to missing raw admNative property', () => { + const alternateServerResponse = utils.deepClone(serverResponseWithNative); + delete alternateServerResponse.body.bids[0].admNative + const r = spec.interpretResponse(alternateServerResponse, bidRequestNative); + expect(r[0].mediaType).to.equal(NATIVE); + expect(r[0].native).not.ok; + utilsMock.expects('logError').once(); + }); + + it('Should ignore native parsing due to invalid raw admNative.assets property', () => { + const alternateServerResponse = utils.deepClone(serverResponseWithNative); + alternateServerResponse.body.bids[0].admNative.assets = { title: { text: 'test' } }; + const r = spec.interpretResponse(alternateServerResponse, bidRequestNative); + expect(r[0].mediaType).to.equal(NATIVE); + expect(r[0].native).not.ok; + utilsMock.expects('logError').once(); + }); + + it('Should handle and return a formated Native ad', () => { + const r = spec.interpretResponse(serverResponseWithNative, bidRequestNative); + const expected = { + displayUrl: 'https://i.am.a.display.url', + sponsoredBy: 'Adagio', + body: 'The super ad text', + rating: '10 from 10', + clickUrl: 'https://i.am.a.click.url', + title: 'My title', + impressionTrackers: [ + 'https://eventrack.local/impression' + ], + javascriptTrackers: '', + clickTrackers: [ + 'https://i.am.a.clicktracker.url' + ], + image: { + url: 'https://images.local/image.jpg', + width: 100, + height: 250 + }, + icon: { + url: 'https://images.local/icon.png', + width: 40, + height: 40 + }, + ext: { + adagio_bvw: 'test' + }, + privacyLink: 'http://www.myprivacyurl.url' + } + expect(r[0].mediaType).to.equal(NATIVE); + expect(r[0].native).ok; + expect(r[0].native).to.deep.equal(expected); + }); + }); }); describe('getUserSyncs()', function() { @@ -1088,7 +1559,7 @@ describe('Adagio bid adapter', () => { expect(loadExternalScript.called).to.be.false; expect(localStorage.getItem(ADAGIO_LOCALSTORAGE_KEY)).to.be.null; - }) + }); }); it('should verify valid hash with valid script', function () { diff --git a/test/spec/modules/addefendBidAdapter_spec.js b/test/spec/modules/addefendBidAdapter_spec.js new file mode 100644 index 00000000000..ac01750e98f --- /dev/null +++ b/test/spec/modules/addefendBidAdapter_spec.js @@ -0,0 +1,184 @@ +import {expect} from 'chai'; +import {spec} from 'modules/addefendBidAdapter.js'; + +describe('addefendBidAdapter', () => { + const defaultBidRequest = { + bidId: 'd66fa86787e0b0ca900a96eacfd5f0bb', + auctionId: 'ccc4c7cdfe11cfbd74065e6dd28413d8', + transactionId: 'd58851660c0c4461e4aa06344fc9c0c6', + sizes: [[300, 250], [300, 600]], + params: { + pageId: 'stringid1', + placementId: 'stringid2' + } + }; + + const deepClone = function (val) { + return JSON.parse(JSON.stringify(val)); + }; + + const buildRequest = (buildRequest, bidderRequest) => { + if (!Array.isArray(buildRequest)) { + buildRequest = [buildRequest]; + } + + return spec.buildRequests(buildRequest, { + ...bidderRequest || {}, + refererInfo: { + referer: 'https://referer.example.com' + } + })[0]; + }; + + describe('isBidRequestValid', () => { + it('should return true when required params found', () => { + const bidRequest = deepClone(defaultBidRequest); + expect(spec.isBidRequestValid(bidRequest)).to.equal(true); + }); + + it('pageId performs type checking', () => { + const bidRequest = deepClone(defaultBidRequest); + bidRequest.params.pageId = 1; // supposed to be a string + expect(spec.isBidRequestValid(bidRequest)).to.equal(false); + }); + + it('placementId performs type checking', () => { + const bidRequest = deepClone(defaultBidRequest); + bidRequest.params.placementId = 1; // supposed to be a string + expect(spec.isBidRequestValid(bidRequest)).to.equal(false); + }); + + it('should return false when required params are not passed', () => { + const bidRequest = deepClone(defaultBidRequest); + delete bidRequest.params; + expect(spec.isBidRequestValid(bidRequest)).to.equal(false); + }); + }); + + describe('buildRequests', () => { + const bidRequest = deepClone(defaultBidRequest); + const request = buildRequest(bidRequest); + + it('sends bid request to endpoint via https using post', () => { + expect(request.method).to.equal('POST'); + expect(request.url.indexOf('https://')).to.equal(0); + expect(request.url).to.equal(`${spec.hostname}/bid`); + }); + + it('contains prebid version parameter', () => { + expect(request.data.v).to.equal($$PREBID_GLOBAL$$.version); + }); + + it('contains correct referer', () => { + expect(request.data.referer).to.equal('https://referer.example.com'); + }); + + it('contains auctionId', () => { + expect(request.data.auctionId).to.equal('ccc4c7cdfe11cfbd74065e6dd28413d8'); + }); + + it('contains pageId', () => { + expect(request.data.pageId).to.equal('stringid1'); + }); + + it('sends correct bid parameters', () => { + const bidRequest = deepClone(defaultBidRequest); + expect(request.data.bids).to.deep.equal([ { + bidId: bidRequest.bidId, + placementId: bidRequest.params.placementId, + sizes: [ '300x250', '300x600' ], + transactionId: 'd58851660c0c4461e4aa06344fc9c0c6' + } ]); + }); + + it('handles empty gdpr object', () => { + const bidRequest = deepClone(defaultBidRequest); + const request = buildRequest(bidRequest, { + gdprConsent: {} + }); + expect(request.data.gdpr_consent).to.be.equal(''); + }); + + it('handles non-existent gdpr object', () => { + const bidRequest = deepClone(defaultBidRequest); + const request = buildRequest(bidRequest, { + gdprConsent: null + }); + expect(request.data.gdpr_consent).to.be.equal(''); + }); + + it('handles properly filled gdpr string', () => { + const bidRequest = deepClone(defaultBidRequest); + const consentString = 'GDPR_CONSENT_STRING'; + const request = buildRequest(bidRequest, { + gdprConsent: { + gdprApplies: true, + consentString: consentString + } + }); + + expect(request.data.gdpr_consent).to.be.equal(consentString); + }); + }); + + describe('interpretResponse', () => { + it('should get correct bid response', () => { + const serverResponse = [ + { + 'width': 300, + 'height': 250, + 'creativeId': '29681110', + 'ad': '', + 'cpm': 0.5, + 'requestId': 'ccc4c7cdfe11cfbd74065e6dd28413d8', + 'ttl': 120, + 'netRevenue': true, + 'currency': 'EUR', + 'advertiserDomains': ['advertiser.example.com'] + } + ]; + + const expectedResponse = [ + { + 'requestId': 'ccc4c7cdfe11cfbd74065e6dd28413d8', + 'cpm': 0.5, + 'creativeId': '29681110', + 'width': 300, + 'height': 250, + 'ttl': 120, + 'currency': 'EUR', + 'ad': '', + 'netRevenue': true, + 'advertiserDomains': ['advertiser.example.com'] + } + ]; + + const result = spec.interpretResponse({body: serverResponse}); + expect(result.length).to.equal(expectedResponse.length); + Object.keys(expectedResponse[0]).forEach((key) => { + expect(result[0][key]).to.deep.equal(expectedResponse[0][key]); + }); + }); + + it('handles incomplete server response', () => { + const serverResponse = [ + { + 'ad': '', + 'cpm': 0.5, + 'requestId': 'ccc4c7cdfe11cfbd74065e6dd28413d8', + 'ttl': 60 + } + ]; + const result = spec.interpretResponse({body: serverResponse}); + + expect(result.length).to.equal(0); + }); + + it('handles nobid responses', () => { + const serverResponse = []; + const result = spec.interpretResponse({body: serverResponse}); + + expect(result.length).to.equal(0); + }); + }); +}); diff --git a/test/spec/modules/adformOpenRTBBidAdapter_spec.js b/test/spec/modules/adfBidAdapter_spec.js similarity index 98% rename from test/spec/modules/adformOpenRTBBidAdapter_spec.js rename to test/spec/modules/adfBidAdapter_spec.js index 05788183e29..2c141d31bf8 100644 --- a/test/spec/modules/adformOpenRTBBidAdapter_spec.js +++ b/test/spec/modules/adfBidAdapter_spec.js @@ -1,14 +1,21 @@ // jshint esversion: 6, es3: false, node: true import {assert, expect} from 'chai'; -import {spec} from 'modules/adformOpenRTBBidAdapter.js'; +import {spec} from 'modules/adfBidAdapter.js'; import { NATIVE } from 'src/mediaTypes.js'; import { config } from 'src/config.js'; import { createEidsArray } from 'modules/userId/eids.js'; -describe('AdformOpenRTB adapter', function () { +describe('Adf adapter', function () { let serverResponse, bidRequest, bidResponses; let bids = []; + describe('backwards-compatibility', function () { + it('should have adformOpenRTB alias defined', function () { + assert.equal(spec.aliases[0].code, 'adformOpenRTB'); + assert.equal(spec.aliases[0].gvlid, 50); + }); + }); + describe('isBidRequestValid', function () { let bid = { 'bidder': 'adformOpenRTB', @@ -567,7 +574,6 @@ describe('AdformOpenRTB adapter', function () { assert.deepEqual(bids[0].netRevenue, false); assert.deepEqual(bids[0].currency, serverResponse.body.cur); assert.deepEqual(bids[0].mediaType, 'native'); - assert.deepEqual(bids[0].bidderCode, 'adformOpenRTB'); }); it('should set correct native params', function () { const bid = [ diff --git a/test/spec/modules/adformBidAdapter_spec.js b/test/spec/modules/adformBidAdapter_spec.js index 360979659de..79ea76da8dd 100644 --- a/test/spec/modules/adformBidAdapter_spec.js +++ b/test/spec/modules/adformBidAdapter_spec.js @@ -149,6 +149,45 @@ describe('Adform adapter', function () { }); }); + it('should allow to pass custom extended ids', function () { + bids[0].params.eids = 'some_id_value'; + let request = spec.buildRequests(bids); + let eids = parseUrl(request.url).query.eids; + + assert.equal(eids, 'some_id_value'); + }); + + it('should add parameter to global parameters if it exists in all bids', function () { + const _bids = []; + _bids.push(bids[0]); + _bids.push(bids[1]); + _bids.push(bids[2]); + _bids[0].params.mkv = 'key:value,key1:value1'; + _bids[1].params.mkv = 'key:value,key1:value1,keyR:removed'; + _bids[2].params.mkv = 'key:value,key1:value1,keyR:removed,key8:value1'; + _bids[0].params.mkw = 'targeting'; + _bids[1].params.mkw = 'targeting'; + _bids[2].params.mkw = 'targeting,targeting2'; + _bids[0].params.msw = 'search:word,search:word2'; + _bids[1].params.msw = 'search:word'; + _bids[2].params.msw = 'search:word,search:word5'; + let bidList = _bids; + let request = spec.buildRequests(bidList); + let parsedUrl = parseUrl(request.url); + assert.equal(parsedUrl.query.mkv, encodeURIComponent('key:value,key1:value1')); + assert.equal(parsedUrl.query.mkw, 'targeting'); + assert.equal(parsedUrl.query.msw, encodeURIComponent('search:word')); + assert.ok(!parsedUrl.items[0].mkv); + assert.ok(!parsedUrl.items[0].mkw); + assert.equal(parsedUrl.items[0].msw, 'search:word2'); + assert.equal(parsedUrl.items[1].mkv, 'keyR:removed'); + assert.ok(!parsedUrl.items[1].mkw); + assert.ok(!parsedUrl.items[1].msw); + assert.equal(parsedUrl.items[2].mkv, 'keyR:removed,key8:value1'); + assert.equal(parsedUrl.items[2].mkw, 'targeting2'); + assert.equal(parsedUrl.items[2].msw, 'search:word5'); + }); + describe('user privacy', function () { it('should send GDPR Consent data to adform if gdprApplies', function () { let request = spec.buildRequests([bids[0]], {gdprConsent: {gdprApplies: true, consentString: 'concentDataString'}}); @@ -219,6 +258,7 @@ describe('Adform adapter', function () { assert.equal(result.currency, 'EUR'); assert.equal(result.netRevenue, true); assert.equal(result.ttl, 360); + assert.deepEqual(result.meta.advertiserDomains, []) assert.equal(result.ad, ''); assert.equal(result.bidderCode, 'adform'); assert.equal(result.transactionId, '5f33781f-9552-4ca1'); 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 0743ddd211d..526102c51fe 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,25 +73,35 @@ 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).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' ] }); }); it('should include gdpr consent param', function () { @@ -107,10 +116,25 @@ 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': '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)).to.not.have.key('eids'); + }); + + it('should request vast content as url', function () { + let req = spec.buildRequests([ minimalBid() ], bidderRequest); - expect(JSON.parse(req.data).parameters).to.deep.include({ 'x5': [ 'ID5-1234567890' ] }); + expect(JSON.parse(req.data).vastContentAsUrl).to.equal(true); }); it('should include bids', function () { @@ -162,7 +186,7 @@ describe('AdheseAdapter', function () { body: '
', tracker: 'https://hosts-demo.adhese.com/rtb_gateway/handlers/client/track/?id=a2f39296-6dd0-4b3c-be85-7baa22e7ff4a', impressionCounter: 'https://hosts-demo.adhese.com/rtb_gateway/handlers/client/track/?id=a2f39296-6dd0-4b3c-be85-7baa22e7ff4a', - extension: {'prebid': {'cpm': {'amount': '1.000000', 'currency': 'USD'}}} + extension: {'prebid': {'cpm': {'amount': '1.000000', 'currency': 'USD'}}, mediaType: 'banner'} } ] }; @@ -209,7 +233,7 @@ describe('AdheseAdapter', function () { width: '640', height: '350', body: '', - extension: {'prebid': {'cpm': {'amount': '2.1', 'currency': 'USD'}}} + extension: {'prebid': {'cpm': {'amount': '2.1', 'currency': 'USD'}}, mediaType: 'video'} } ] }; @@ -235,6 +259,43 @@ describe('AdheseAdapter', function () { expect(spec.interpretResponse(sspVideoResponse, bidRequest)).to.deep.equal(expectedResponse); }); + it('should get correct ssp cache video response', () => { + let sspCachedVideoResponse = { + body: [ + { + origin: 'RUBICON', + ext: 'js', + slotName: '_main_page_-leaderboard', + adType: 'leaderboard', + width: '640', + height: '350', + cachedBodyUrl: 'https://ads-demo.adhese.com/content/38983ccc-4083-4c24-932c-96f798d969b3', + extension: {'prebid': {'cpm': {'amount': '2.1', 'currency': 'USD'}}, mediaType: 'video'} + } + ] + }; + + let expectedResponse = [{ + requestId: BID_ID, + vastUrl: 'https://ads-demo.adhese.com/content/38983ccc-4083-4c24-932c-96f798d969b3', + cpm: 2.1, + currency: 'USD', + creativeId: 'RUBICON', + dealId: '', + width: 640, + height: 350, + mediaType: 'video', + netRevenue: NET_REVENUE, + ttl: TTL, + adhese: { + origin: 'RUBICON', + originInstance: '', + originData: {} + } + }]; + expect(spec.interpretResponse(sspCachedVideoResponse, bidRequest)).to.deep.equal(expectedResponse); + }); + it('should get correct Adhese banner response', () => { const adheseBannerResponse = { body: [ @@ -272,7 +333,8 @@ describe('AdheseAdapter', function () { amount: '5.96', currency: 'USD' } - } + }, + mediaType: 'banner' } } ] @@ -334,7 +396,10 @@ describe('AdheseAdapter', function () { impressionCounter: 'https://hosts-demo.adhese.com/track/742898', origin: 'JERLICIA', originData: {}, - auctionable: true + auctionable: true, + extension: { + mediaType: 'video' + } } ] }; @@ -372,6 +437,70 @@ describe('AdheseAdapter', function () { expect(spec.interpretResponse(adheseVideoResponse, bidRequest)).to.deep.equal(expectedResponse); }); + it('should get correct Adhese cached video response', () => { + const adheseVideoResponse = { + body: [ + { + adType: 'preroll', + adFormat: '', + orderId: '22248', + adspaceId: '164196', + body: '', + height: '360', + width: '640', + extension: { + mediaType: 'video' + }, + cachedBodyUrl: 'https://ads-demo.adhese.com/content/38983ccc-4083-4c24-932c-96f798d969b3', + libId: '89860', + id: '742470', + advertiserId: '2263', + ext: 'advar', + orderName: 'Smartphoto EOY-20181112', + creativeName: 'PREROLL', + slotName: '_main_page_-leaderboard', + slotID: '41711', + impressionCounter: 'https://hosts-demo.adhese.com/track/742898', + origin: 'JERLICIA', + originData: {}, + auctionable: true + } + ] + }; + + let expectedResponse = [{ + requestId: BID_ID, + vastUrl: 'https://ads-demo.adhese.com/content/38983ccc-4083-4c24-932c-96f798d969b3', + adhese: { + origin: '', + originInstance: '', + originData: { + adFormat: '', + adId: '742470', + adType: 'preroll', + adspaceId: '164196', + libId: '89860', + orderProperty: undefined, + priority: undefined, + viewableImpressionCounter: undefined, + slotId: '41711', + slotName: '_main_page_-leaderboard', + advertiserId: '2263', + } + }, + cpm: 0, + currency: 'USD', + creativeId: '742470', + dealId: '22248', + width: 640, + height: 360, + mediaType: 'video', + netRevenue: NET_REVENUE, + ttl: TTL, + }]; + expect(spec.interpretResponse(adheseVideoResponse, bidRequest)).to.deep.equal(expectedResponse); + }); + it('should return no bids for empty adserver response', () => { let adserverResponse = { body: [] }; expect(spec.interpretResponse(adserverResponse, bidRequest)).to.be.empty; diff --git a/test/spec/modules/adkernelAdnAnalytics_spec.js b/test/spec/modules/adkernelAdnAnalytics_spec.js index e7ef831080c..7af96c9321c 100644 --- a/test/spec/modules/adkernelAdnAnalytics_spec.js +++ b/test/spec/modules/adkernelAdnAnalytics_spec.js @@ -1,6 +1,6 @@ -import analyticsAdapter, {ExpiringQueue, getUmtSource, storage} from 'modules/adkernelAdnAnalyticsAdapter.js'; +import analyticsAdapter, {ExpiringQueue, getUmtSource, storage} from 'modules/adkernelAdnAnalyticsAdapter'; import {expect} from 'chai'; -import adapterManager from 'src/adapterManager.js'; +import adapterManager from 'src/adapterManager'; import CONSTANTS from 'src/constants.json'; const events = require('../../../src/events'); @@ -158,11 +158,16 @@ describe('', function () { sizes: [[300, 250]], bidId: '208750227436c1', bidderRequestId: '1a6fc81528d0f6', - auctionId: '5018eb39-f900-4370-b71e-3bb5b48d324f' + auctionId: '5018eb39-f900-4370-b71e-3bb5b48d324f', + gdprConsent: { + consentString: 'CONSENT', + gdprApplies: true, + apiVersion: 2 + }, + uspConsent: '1---' }], auctionStart: 1509369418387, - timeout: 3000, - start: 1509369418389 + timeout: 3000 }; const RESPONSE = { @@ -202,6 +207,10 @@ describe('', function () { events.getEvents.restore(); }); + after(function() { + sandbox.restore(); + }); + it('should be configurable', function () { adapterManager.registerAnalyticsAdapter({ code: 'adkernelAdn', @@ -221,7 +230,7 @@ describe('', function () { }); it('should handle auction init event', function () { - events.emit(CONSTANTS.EVENTS.AUCTION_INIT, {config: {}, timeout: 3000}); + events.emit(CONSTANTS.EVENTS.AUCTION_INIT, {config: {}, bidderRequests: [REQUEST], timeout: 3000}); const ev = analyticsAdapter.context.queue.peekAll(); expect(ev).to.have.length(1); expect(ev[0]).to.be.eql({event: 'auctionInit'}); diff --git a/test/spec/modules/adkernelAdnBidAdapter_spec.js b/test/spec/modules/adkernelAdnBidAdapter_spec.js index 3d3e64aeec9..d3c9a2cf816 100644 --- a/test/spec/modules/adkernelAdnBidAdapter_spec.js +++ b/test/spec/modules/adkernelAdnBidAdapter_spec.js @@ -1,5 +1,6 @@ import {expect} from 'chai'; -import {spec} from 'modules/adkernelAdnBidAdapter.js'; +import {spec} from 'modules/adkernelAdnBidAdapter'; +import {config} from 'src/config'; describe('AdkernelAdn adapter', function () { const bid1_pub1 = { @@ -205,7 +206,6 @@ describe('AdkernelAdn adapter', function () { fullBidderRequest.bids = bidRequests; let pbRequests = spec.buildRequests(bidRequests, fullBidderRequest); let tagRequests = pbRequests.map(r => JSON.parse(r.data)); - return [pbRequests, tagRequests]; } @@ -263,6 +263,26 @@ describe('AdkernelAdn adapter', function () { expect(bidRequests[0].user).to.have.property('gdpr', 0); expect(bidRequests[0].user).to.not.have.property('consent'); }); + + it('should\'t contain consent string if gdpr isn\'t applied', function () { + config.setConfig({coppa: true}); + let [_, bidRequests] = buildRequest([bid1_pub1]); + config.resetConfig(); + expect(bidRequests[0]).to.have.property('user'); + expect(bidRequests[0].user).to.have.property('coppa', 1); + }); + + it('should set bidfloor if configured', function() { + let bid = Object.assign({}, bid1_pub1); + bid.getFloor = function() { + return { + currency: 'USD', + floor: 0.145 + } + }; + let [, tagRequests] = buildRequest([bid]); + expect(tagRequests[0].imp[0]).to.have.property('bidfloor', 0.145); + }); }); describe('video request building', () => { diff --git a/test/spec/modules/adkernelBidAdapter_spec.js b/test/spec/modules/adkernelBidAdapter_spec.js index 87504aa46af..b9d647238bf 100644 --- a/test/spec/modules/adkernelBidAdapter_spec.js +++ b/test/spec/modules/adkernelBidAdapter_spec.js @@ -1,8 +1,8 @@ import {expect} from 'chai'; -import {spec} from 'modules/adkernelBidAdapter.js'; -import * as utils from 'src/utils.js'; +import {spec} from 'modules/adkernelBidAdapter'; +import * as utils from 'src/utils'; import {NATIVE, BANNER, VIDEO} from 'src/mediaTypes'; -import {config} from 'src/config.js'; +import {config} from 'src/config'; describe('Adkernel adapter', function () { const bid1_zone1 = { @@ -175,7 +175,6 @@ describe('Adkernel adapter', function () { dealid: 'deal' }] }], - cur: 'USD', ext: { adk_usersync: [{type: 1, url: 'https://adk.sync.com/sync'}] } @@ -192,7 +191,6 @@ describe('Adkernel adapter', function () { cid: '16855' }] }], - cur: 'USD' }, usersyncOnlyResponse = { id: 'nobid1', ext: { @@ -222,12 +220,18 @@ describe('Adkernel adapter', function () { } }), adomain: ['displayurl.com'], + cat: ['IAB1-4', 'IAB8-16', 'IAB25-5'], cid: '1', - crid: '4' + crid: '4', + ext: { + 'advertiser_id': 777, + 'advertiser_name': 'advertiser', + 'agency_name': 'agency' + } }] }], bidid: 'pTuOlf5KHUo', - cur: 'USD' + cur: 'EUR' }; var sandbox; @@ -237,6 +241,7 @@ describe('Adkernel adapter', function () { afterEach(function () { sandbox.restore(); + config.resetConfig(); }); function buildBidderRequest(url = 'https://example.com/index.html', params = {}) { @@ -339,6 +344,14 @@ describe('Adkernel adapter', function () { expect(bidRequest.user.ext).to.be.eql({'consent': 'test-consent-string'}); }); + it('should contain coppa if configured', function () { + config.setConfig({coppa: true}); + let [_, bidRequests] = buildRequest([bid1_zone1]); + let bidRequest = bidRequests[0]; + expect(bidRequest).to.have.property('regs'); + expect(bidRequest.regs).to.have.property('coppa', 1); + }); + it('should\'t contain consent string if gdpr isn\'t applied', function () { let [_, bidRequests] = buildRequest([bid1_zone1], buildBidderRequest('https://example.com/index.html', {gdprConsent: {gdprApplies: false}})); let bidRequest = bidRequests[0]; @@ -353,9 +366,21 @@ describe('Adkernel adapter', function () { }); it('should forward default bidder timeout', function() { - let [_, bidRequests] = buildRequest([bid1_zone1], DEFAULT_BIDDER_REQUEST); + let [_, bidRequests] = buildRequest([bid1_zone1]); expect(bidRequests[0]).to.have.property('tmax', 3000); }); + + it('should set bidfloor if configured', function() { + let bid = Object.assign({}, bid1_zone1); + bid.getFloor = function() { + return { + currency: 'USD', + floor: 0.145 + } + }; + let [_, bidRequests] = buildRequest([bid]); + expect(bidRequests[0].imp[0]).to.have.property('bidfloor', 0.145); + }); }); describe('video request building', function () { @@ -552,8 +577,7 @@ describe('Adkernel adapter', function () { describe('adapter configuration', () => { it('should have aliases', () => { - expect(spec.aliases).to.have.lengthOf(6); - expect(spec.aliases).to.include.members(['headbidding', 'adsolut', 'oftmediahb', 'audiencemedia', 'waardex_ak', 'roqoon']); + expect(spec.aliases).to.be.an('array').that.is.not.empty; }); }); @@ -587,7 +611,13 @@ describe('Adkernel adapter', function () { let resp = spec.interpretResponse({body: nativeResponse}, pbRequests[0])[0]; expect(resp).to.have.property('requestId', 'Bid_01'); expect(resp).to.have.property('cpm', 2.25); - expect(resp).to.have.property('currency', 'USD'); + expect(resp).to.have.property('currency', 'EUR'); + expect(resp).to.have.property('meta'); + expect(resp.meta.advertiserId).to.be.eql(777); + expect(resp.meta.advertiserName).to.be.eql('advertiser'); + expect(resp.meta.agencyName).to.be.eql('agency'); + expect(resp.meta.advertiserDomains).to.be.eql(['displayurl.com']); + expect(resp.meta.secondaryCatIds).to.be.eql(['IAB1-4', 'IAB8-16', 'IAB25-5']); expect(resp).to.have.property('mediaType', NATIVE); expect(resp).to.have.property('native'); expect(resp.native).to.have.property('clickUrl', 'http://rtb.com/click?i=pTuOlf5KHUo_0'); diff --git a/test/spec/modules/adlooxAnalyticsAdapter_spec.js b/test/spec/modules/adlooxAnalyticsAdapter_spec.js new file mode 100644 index 00000000000..9cc9c4ded4d --- /dev/null +++ b/test/spec/modules/adlooxAnalyticsAdapter_spec.js @@ -0,0 +1,229 @@ +import adapterManager from 'src/adapterManager.js'; +import analyticsAdapter, { command as analyticsCommand, COMMAND } from 'modules/adlooxAnalyticsAdapter.js'; +import { AUCTION_COMPLETED } from 'src/auction.js'; +import { expect } from 'chai'; +import events from 'src/events.js'; +import { EVENTS } from 'src/constants.json'; +import * as utils from 'src/utils.js'; +import { loadExternalScriptStub } from 'test/mocks/adloaderStub.js'; + +const analyticsAdapterName = 'adloox'; + +describe('Adloox Analytics Adapter', function () { + let sandbox; + + const esplode = 'esplode'; + + const bid = { + bidder: 'dummy', + adUnitCode: 'ad-slot-1', + mediaType: 'display' + }; + + const auctionDetails = { + auctionStatus: AUCTION_COMPLETED, + bidsReceived: [ + bid + ] + }; + + const analyticsOptions = { + js: 'https://j.adlooxtracking.com/ads/js/tfav_adl_%%clientid%%.js', + client: 'adlooxtest', + clientid: 127, + platformid: 0, + tagid: 0, + params: { + dummy1: '%%client%%', + dummy2: '%%pbAdSlot%%', + dummy3: function(bid) { throw new Error(esplode) } + } + }; + + adapterManager.registerAnalyticsAdapter({ + code: analyticsAdapterName, + adapter: analyticsAdapter + }); + describe('enableAnalytics', function () { + describe('invalid options', function () { + it('should require options', function (done) { + adapterManager.enableAnalytics({ + provider: analyticsAdapterName + }); + expect(analyticsAdapter.context).is.null; + + done(); + }); + + it('should reject non-string options.js', function (done) { + const analyticsOptionsLocal = utils.deepClone(analyticsOptions); + analyticsOptionsLocal.js = function () { }; + + adapterManager.enableAnalytics({ + provider: analyticsAdapterName, + options: analyticsOptionsLocal + }); + expect(analyticsAdapter.context).is.null; + + done(); + }); + + it('should reject non-function options.toselector', function (done) { + const analyticsOptionsLocal = utils.deepClone(analyticsOptions); + analyticsOptionsLocal.toselector = esplode; + + adapterManager.enableAnalytics({ + provider: analyticsAdapterName, + options: analyticsOptionsLocal + }); + expect(analyticsAdapter.context).is.null; + + done(); + }); + + [ 'client', 'clientid', 'platformid', 'tagid' ].forEach(function (o) { + it('should require options.' + o, function (done) { + const analyticsOptionsLocal = utils.deepClone(analyticsOptions); + delete analyticsOptionsLocal[o]; + + adapterManager.enableAnalytics({ + provider: analyticsAdapterName, + options: analyticsOptionsLocal + }); + expect(analyticsAdapter.context).is.null; + + done(); + }); + }); + + it('should reject non-object options.params', function (done) { + const analyticsOptionsLocal = utils.deepClone(analyticsOptions); + analyticsOptionsLocal.params = esplode; + + adapterManager.enableAnalytics({ + provider: analyticsAdapterName, + options: analyticsOptionsLocal + }); + expect(analyticsAdapter.context).is.null; + + done(); + }); + }); + }); + + describe('process', function () { + beforeEach(function() { + sandbox = sinon.sandbox.create(); + + sandbox.stub(events, 'getEvents').returns([]); + + adapterManager.enableAnalytics({ + provider: analyticsAdapterName, + options: utils.deepClone(analyticsOptions) + }); + + expect(analyticsAdapter.context).is.not.null; + }); + + afterEach(function () { + analyticsAdapter.disableAnalytics(); + expect(analyticsAdapter.context).is.null; + + sandbox.restore(); + sandbox = undefined; + }); + + describe('event', function () { + it('should preload the script on AUCTION_END only once', function (done) { + const insertElementStub = sandbox.stub(utils, 'insertElement'); + + const uri = utils.parseUrl(analyticsAdapter.url(analyticsOptions.js)); + const isLinkPreloadAsScript = function(arg) { + const href_uri = utils.parseUrl(arg.href); // IE11 requires normalisation (hostname always includes port) + return arg.tagName === 'LINK' && arg.getAttribute('rel') === 'preload' && arg.getAttribute('as') === 'script' && href_uri.href === uri.href; + }; + + events.emit(EVENTS.AUCTION_END, auctionDetails); + expect(insertElementStub.calledWith(sinon.match(isLinkPreloadAsScript))).to.true; + + events.emit(EVENTS.AUCTION_END, auctionDetails); + expect(insertElementStub.callCount).to.equal(1); + + done(); + }); + + it('should inject verification JS on BID_WON', function (done) { + const url = analyticsAdapter.url(`${analyticsOptions.js}#`); + + const parent = document.createElement('div'); + + const slot = document.createElement('div'); + slot.id = bid.adUnitCode; + parent.appendChild(slot); + + const dummy = document.createElement('div'); + parent.appendChild(dummy); + + const querySelectorStub = sandbox.stub(document, 'querySelector'); + querySelectorStub.withArgs(`#${bid.adUnitCode}`).returns(slot); + + events.emit(EVENTS.BID_WON, bid); + + const [urlInserted, moduleCode] = loadExternalScriptStub.getCall(0).args; + + expect(urlInserted.substr(0, url.length)).to.equal(url); + expect(moduleCode).to.equal(analyticsAdapterName); + expect(/[#&]creatype=2(&|$)/.test(urlInserted)).is.true; // prebid 'display' -> adloox '2' + expect(new RegExp('[#&]dummy3=' + encodeURIComponent('ERROR: ' + esplode) + '(&|$)').test(urlInserted)).is.true; + + done(); + }); + }); + + describe('command', function () { + it('should return config', function (done) { + const expected = utils.deepClone(analyticsOptions); + delete expected.js; + delete expected.toselector; + delete expected.params; + + analyticsCommand(COMMAND.CONFIG, null, function (response) { + expect(utils.deepEqual(response, expected)).is.true; + + done(); + }); + }); + + it('should return expanded URL', function (done) { + const data = { + url: 'https://example.com?', + args: [ + [ 'client', '%%client%%' ] + ], + bid: bid, + ids: true + }; + const expected = `${data.url}${data.args[0][0]}=${analyticsOptions.client}`; + + analyticsCommand(COMMAND.URL, data, function (response) { + expect(response.substr(0, expected.length)).is.equal(expected); + + done(); + }); + }); + + it('should inject tracking event', function (done) { + const data = { + eventType: EVENTS.BID_WON, + args: bid + }; + + analyticsCommand(COMMAND.TRACK, data, function (response) { + expect(response).is.undefined; + + done(); + }); + }); + }); + }); +}); diff --git a/test/spec/modules/admixerBidAdapter_spec.js b/test/spec/modules/admixerBidAdapter_spec.js index 6d2e3059dc8..030e98d23f9 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.0.aspx'; +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,37 +77,53 @@ 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 () { let response = { - body: [{ - 'currency': 'USD', - 'cpm': 6.210000, - 'ad': '
ad
', - 'width': 300, - 'height': 600, - 'creativeId': 'ccca3e5e-0c54-4761-9667-771322fbdffc', - 'ttl': 360, - 'netRevenue': false, - 'bidId': '5e4e763b6bc60b' - }] + body: { + ads: [{ + 'currency': 'USD', + 'cpm': 6.210000, + 'ad': '
ad
', + 'width': 300, + 'height': 600, + 'creativeId': 'ccca3e5e-0c54-4761-9667-771322fbdffc', + 'ttl': 360, + 'netRevenue': false, + 'bidId': '5e4e763b6bc60b', + 'dealId': 'asd123', + 'meta': {'advertiserId': 123, 'networkId': 123, 'advertiserDomains': ['test.com']} + }] + } }; it('should get correct bid response', function () { - const body = response.body; + const ads = response.body.ads; let expectedResponse = [ { - 'requestId': body[0].bidId, - 'cpm': body[0].cpm, - 'creativeId': body[0].creativeId, - 'width': body[0].width, - 'height': body[0].height, - 'ad': body[0].ad, + 'requestId': ads[0].bidId, + 'cpm': ads[0].cpm, + 'creativeId': ads[0].creativeId, + 'width': ads[0].width, + 'height': ads[0].height, + 'ad': ads[0].ad, 'vastUrl': undefined, - 'currency': body[0].currency, - 'netRevenue': body[0].netRevenue, - 'ttl': body[0].ttl, + 'currency': ads[0].currency, + 'netRevenue': ads[0].netRevenue, + 'ttl': ads[0].ttl, + 'dealId': ads[0].dealId, + 'meta': {'advertiserId': 123, 'networkId': 123, 'advertiserDomains': ['test.com']} } ]; @@ -119,4 +138,34 @@ describe('AdmixerAdapter', function () { expect(result.length).to.equal(0); }); }); + + describe('getUserSyncs', function () { + let imgUrl = 'https://example.com/img1'; + let frmUrl = 'https://example.com/frm2'; + let responses = [{ + body: { + cm: { + pixels: [ + imgUrl + ], + iframes: [ + frmUrl + ], + } + } + }]; + + it('Returns valid values', function () { + let userSyncAll = spec.getUserSyncs({pixelEnabled: true, iframeEnabled: true}, responses); + let userSyncImg = spec.getUserSyncs({pixelEnabled: true, iframeEnabled: false}, responses); + let userSyncFrm = spec.getUserSyncs({pixelEnabled: false, iframeEnabled: true}, responses); + expect(userSyncAll).to.be.an('array').with.lengthOf(2); + expect(userSyncImg).to.be.an('array').with.lengthOf(1); + expect(userSyncImg[0].url).to.be.equal(imgUrl); + expect(userSyncImg[0].type).to.be.equal('image'); + expect(userSyncFrm).to.be.an('array').with.lengthOf(1); + expect(userSyncFrm[0].url).to.be.equal(frmUrl); + expect(userSyncFrm[0].type).to.be.equal('iframe'); + }); + }); }); diff --git a/test/spec/modules/admixerIdSystem_spec.js b/test/spec/modules/admixerIdSystem_spec.js new file mode 100644 index 00000000000..18107b780db --- /dev/null +++ b/test/spec/modules/admixerIdSystem_spec.js @@ -0,0 +1,81 @@ +import {admixerIdSubmodule} from 'modules/admixerIdSystem.js'; +import * as utils from 'src/utils.js'; +import {server} from 'test/mocks/xhr.js'; +import {getStorageManager} from '../../../src/storageManager.js'; + +export const storage = getStorageManager(); + +const pid = '4D393FAC-B6BB-4E19-8396-0A4813607316'; +const getIdParams = {params: {pid: pid}}; +describe('admixerId tests', function () { + let logErrorStub; + + beforeEach(function () { + logErrorStub = sinon.stub(utils, 'logError'); + }); + + afterEach(function () { + logErrorStub.restore(); + }); + + it('should log an error if pid configParam was not passed when getId', function () { + admixerIdSubmodule.getId(); + expect(logErrorStub.callCount).to.be.equal(1); + + admixerIdSubmodule.getId({}); + expect(logErrorStub.callCount).to.be.equal(2); + + admixerIdSubmodule.getId({params: {}}); + expect(logErrorStub.callCount).to.be.equal(3); + + admixerIdSubmodule.getId({params: {pid: 123}}); + expect(logErrorStub.callCount).to.be.equal(4); + }); + + it('should NOT call the admixer id endpoint if gdpr applies but consent string is missing', function () { + let submoduleCallback = admixerIdSubmodule.getId(getIdParams, { gdprApplies: true }); + expect(submoduleCallback).to.be.undefined; + }); + + it('should call the admixer id endpoint', function () { + let callBackSpy = sinon.spy(); + let submoduleCallback = admixerIdSubmodule.getId(getIdParams).callback; + submoduleCallback(callBackSpy); + let request = server.requests[0]; + expect(request.url).to.be.eq(`https://inv-nets.admixer.net/cntcm.aspx?ssp=${pid}`); + request.respond( + 200, + {}, + JSON.stringify({}) + ); + expect(callBackSpy.calledOnce).to.be.true; + }); + + it('should call callback with user id', function () { + let callBackSpy = sinon.spy(); + let submoduleCallback = admixerIdSubmodule.getId(getIdParams).callback; + submoduleCallback(callBackSpy); + let request = server.requests[0]; + expect(request.url).to.be.eq(`https://inv-nets.admixer.net/cntcm.aspx?ssp=${pid}`); + request.respond( + 200, + {}, + JSON.stringify({setData: {visitorid: '571058d70bce453b80e6d98b4f8a81e3'}}) + ); + expect(callBackSpy.calledOnce).to.be.true; + expect(callBackSpy.args[0][0]).to.be.eq('571058d70bce453b80e6d98b4f8a81e3'); + }); + + it('should continue to callback if ajax response 204', function () { + let callBackSpy = sinon.spy(); + let submoduleCallback = admixerIdSubmodule.getId(getIdParams).callback; + submoduleCallback(callBackSpy); + let request = server.requests[0]; + expect(request.url).to.be.eq(`https://inv-nets.admixer.net/cntcm.aspx?ssp=${pid}`); + request.respond( + 204 + ); + expect(callBackSpy.calledOnce).to.be.true; + expect(callBackSpy.args[0][0]).to.be.undefined; + }); +}); diff --git a/test/spec/modules/adnowBidAdapter_spec.js b/test/spec/modules/adnowBidAdapter_spec.js new file mode 100644 index 00000000000..a8013e3fa04 --- /dev/null +++ b/test/spec/modules/adnowBidAdapter_spec.js @@ -0,0 +1,163 @@ +import { expect } from 'chai'; +import { spec } from 'modules/adnowBidAdapter.js'; + +describe('adnowBidAdapter', function () { + describe('isBidRequestValid', function () { + it('Should return true', function() { + expect(spec.isBidRequestValid({ + bidder: 'adnow', + params: { + codeId: 12345 + } + })).to.equal(true); + }); + + it('Should return false when required params is not passed', function() { + expect(spec.isBidRequestValid({ + bidder: 'adnow', + params: {} + })).to.equal(false); + }); + }); + + describe('buildRequests', function() { + it('Common settings', function() { + const bidRequestData = [{ + bidId: 'bid12345', + params: { + codeId: 12345 + } + }]; + + const req = spec.buildRequests(bidRequestData); + const reqData = req[0].data; + + expect(reqData) + .to.match(/Id=12345/) + .to.match(/mediaType=native/) + .to.match(/out=prebid/) + .to.match(/requestid=bid12345/) + .to.match(/d_user_agent=.+/); + }); + + it('Banner sizes', function () { + const bidRequestData = [{ + bidId: 'bid12345', + params: { + codeId: 12345 + }, + mediaTypes: { + banner: { + sizes: [[300, 250]] + } + } + }]; + + const req = spec.buildRequests(bidRequestData); + const reqData = req[0].data; + + expect(reqData).to.match(/sizes=300x250/); + }); + + it('Native sizes', function () { + const bidRequestData = [{ + bidId: 'bid12345', + params: { + codeId: 12345 + }, + mediaTypes: { + native: { + image: { + sizes: [100, 100] + } + } + } + }]; + + const req = spec.buildRequests(bidRequestData); + const reqData = req[0].data; + + expect(reqData) + .to.match(/width=100/) + .to.match(/height=100/); + }); + }); + + describe('interpretResponse', function() { + const request = { + bidRequest: { + bidId: 'bid12345' + } + }; + + it('Response with native bid', function() { + const response = { + currency: 'USD', + cpm: 0.5, + native: { + title: 'Title', + body: 'Body', + sponsoredBy: 'AdNow', + clickUrl: '//click.url', + image: { + url: '//img.url', + height: 200, + width: 200 + } + }, + meta: { + mediaType: 'native' + } + }; + + const bids = spec.interpretResponse({ body: response }, request); + expect(bids).to.be.an('array').that.is.not.empty; + + const bid = bids[0]; + expect(bid).to.have.keys('requestId', 'cpm', 'currency', 'native', 'creativeId', 'netRevenue', 'meta', 'ttl'); + + const nativePart = bid.native; + + expect(nativePart.title).to.be.equal('Title'); + expect(nativePart.body).to.be.equal('Body'); + expect(nativePart.clickUrl).to.be.equal('//click.url'); + expect(nativePart.image.url).to.be.equal('//img.url'); + expect(nativePart.image.height).to.be.equal(200); + expect(nativePart.image.width).to.be.equal(200); + }); + + it('Response with banner bid', function() { + const response = { + currency: 'USD', + cpm: 0.5, + ad: '
Banner
', + meta: { + mediaType: 'banner' + } + }; + + const bids = spec.interpretResponse({ body: response }, request); + expect(bids).to.be.an('array').that.is.not.empty; + + const bid = bids[0]; + expect(bid).to.have.keys( + 'requestId', 'cpm', 'currency', 'ad', 'creativeId', 'netRevenue', 'meta', 'ttl', 'width', 'height' + ); + + expect(bid.ad).to.be.equal('
Banner
'); + }); + + it('Response with no bid should return an empty array', function() { + const noBidResponses = [ + false, + {}, + {body: false}, + {body: {}} + ]; + + noBidResponses.forEach(response => { + return expect(spec.interpretResponse(response, request)).to.be.an('array').that.is.empty; + }); + }); + }); +}); diff --git a/test/spec/modules/adnuntiusBidAdapter_spec.js b/test/spec/modules/adnuntiusBidAdapter_spec.js index 54ff038c083..62073fc6aaa 100644 --- a/test/spec/modules/adnuntiusBidAdapter_spec.js +++ b/test/spec/modules/adnuntiusBidAdapter_spec.js @@ -4,25 +4,36 @@ import { spec } from 'modules/adnuntiusBidAdapter.js'; import { newBidder } from 'src/adapters/bidderFactory.js'; describe('adnuntiusBidAdapter', function () { - const ENDPOINT_URL = 'https://delivery.adnuntius.com/i?tzo=-60&format=json'; + const tzo = new Date().getTimezoneOffset(); + const ENDPOINT_URL = `https://delivery.adnuntius.com/i?tzo=${tzo}&format=json`; const adapter = newBidder(spec); + const bidRequests = [ { + bidId: '123', bidder: 'adnuntius', params: { auId: '8b6bc', network: 'adnuntius', }, - bidId: '123' + } - ]; + ] + + const singleBidRequest = { + bid: [ + { + bidId: '123', + } + ] + } const serverResponse = { body: { 'adUnits': [ { 'auId': '000000000008b6bc', - 'targetId': '', + 'targetId': '123', 'html': '

hi!

', 'matchedAdCount': 1, 'responseId': 'adn-rsp-1460129238', @@ -67,8 +78,15 @@ describe('adnuntiusBidAdapter', function () { 'lineItemId': 'scyjdyv3mzgdsnpf', 'layoutId': 'sw6gtws2rdj1kwby', 'layoutName': 'Responsive image' - } + }, + ] + }, + { + 'auId': '000000000008b6bc', + 'targetId': '456', + 'matchedAdCount': 0, + 'responseId': 'adn-rsp-1460129238', } ] } @@ -96,22 +114,24 @@ describe('adnuntiusBidAdapter', function () { expect(request[0]).to.have.property('url'); expect(request[0].url).to.equal(ENDPOINT_URL); expect(request[0]).to.have.property('data'); - expect(request[0].data).to.equal('{\"adUnits\":[{\"auId\":\"8b6bc\"}]}'); + expect(request[0].data).to.equal('{\"adUnits\":[{\"auId\":\"8b6bc\",\"targetId\":\"123\"}]}'); }); }); describe('interpretResponse', function () { it('should return valid response when passed valid server response', function () { - const request = spec.buildRequests(bidRequests); - const interpretedResponse = spec.interpretResponse(serverResponse, request[0]); + const interpretedResponse = spec.interpretResponse(serverResponse, singleBidRequest); const ad = serverResponse.body.adUnits[0].ads[0] expect(interpretedResponse).to.have.lengthOf(1); expect(interpretedResponse[0].cpm).to.equal(ad.cpm.amount); expect(interpretedResponse[0].width).to.equal(Number(ad.creativeWidth)); expect(interpretedResponse[0].height).to.equal(Number(ad.creativeHeight)); expect(interpretedResponse[0].creativeId).to.equal(ad.creativeId); - expect(interpretedResponse[0].currency).to.equal(ad.cpm.currency); + expect(interpretedResponse[0].currency).to.equal(ad.bid.currency); expect(interpretedResponse[0].netRevenue).to.equal(false); + expect(interpretedResponse[0].meta).to.have.property('advertiserDomains'); + expect(interpretedResponse[0].meta.advertiserDomains).to.have.lengthOf(1); + expect(interpretedResponse[0].meta.advertiserDomains[0]).to.equal('google.com'); expect(interpretedResponse[0].ad).to.equal(serverResponse.body.adUnits[0].html); expect(interpretedResponse[0].ttl).to.equal(360); }); diff --git a/test/spec/modules/adotBidAdapter_spec.js b/test/spec/modules/adotBidAdapter_spec.js index 594fc4ac7b7..d580cd763a4 100644 --- a/test/spec/modules/adotBidAdapter_spec.js +++ b/test/spec/modules/adotBidAdapter_spec.js @@ -2062,10 +2062,11 @@ describe('Adot Adapter', function () { serverResponse.body.cur = 'USD'; const ads = spec.interpretResponse(serverResponse, serverRequest); + const admWithAuctionPriceReplaced = utils.replaceAuctionPrice(serverResponse.body.seatbid[0].bid[0].adm, serverResponse.body.seatbid[0].bid[0].price); expect(ads).to.be.an('array').and.to.have.length(1); expect(ads[0].requestId).to.exist.and.to.be.a('string').and.to.equal(serverRequest._adot_internal.impressions[0].bidId); - expect(ads[0].ad).to.exist.and.to.be.a('string').and.to.have.string(serverResponse.body.seatbid[0].bid[0].adm); + expect(ads[0].ad).to.exist.and.to.be.a('string').and.to.have.string(admWithAuctionPriceReplaced); expect(ads[0].adUrl).to.equal(null); expect(ads[0].vastXml).to.equal(null); expect(ads[0].vastUrl).to.equal(null); @@ -2087,10 +2088,13 @@ describe('Adot Adapter', function () { const ads = spec.interpretResponse(serverResponse, serverRequest); + const admWithAuctionPriceReplaced = utils.replaceAuctionPrice(serverResponse.body.seatbid[0].bid[0].adm, serverResponse.body.seatbid[0].bid[0].price); + const adm2WithAuctionPriceReplaced = utils.replaceAuctionPrice(serverResponse.body.seatbid[0].bid[1].adm, serverResponse.body.seatbid[0].bid[1].price); + expect(ads).to.be.an('array').and.to.have.length(2); expect(ads[0].requestId).to.exist.and.to.be.a('string').and.to.equal(serverRequest._adot_internal.impressions[0].bidId); - expect(ads[0].ad).to.exist.and.to.be.a('string').and.to.have.string(serverResponse.body.seatbid[0].bid[0].adm); + expect(ads[0].ad).to.exist.and.to.be.a('string').and.to.have.string(admWithAuctionPriceReplaced); expect(ads[0].adUrl).to.equal(null); expect(ads[0].vastXml).to.equal(null); expect(ads[0].vastUrl).to.equal(null); @@ -2104,7 +2108,7 @@ describe('Adot Adapter', function () { expect(ads[0].mediaType).to.exist.and.to.be.a('string').and.to.equal('banner'); expect(ads[0].renderer).to.equal(null); expect(ads[1].requestId).to.exist.and.to.be.a('string').and.to.equal(serverRequest._adot_internal.impressions[1].bidId); - expect(ads[1].ad).to.exist.and.to.be.a('string').and.to.have.string(serverResponse.body.seatbid[0].bid[1].adm); + expect(ads[1].ad).to.exist.and.to.be.a('string').and.to.have.string(adm2WithAuctionPriceReplaced); expect(ads[1].adUrl).to.equal(null); expect(ads[1].creativeId).to.exist.and.to.be.a('string').and.to.equal(serverResponse.body.seatbid[0].bid[1].crid); expect(ads[1].cpm).to.exist.and.to.be.a('number').and.to.equal(serverResponse.body.seatbid[0].bid[1].price); @@ -2592,10 +2596,11 @@ describe('Adot Adapter', function () { const serverResponse = examples.serverResponse_banner; const ads = spec.interpretResponse(serverResponse, serverRequest); + const admWithAuctionPriceReplaced = utils.replaceAuctionPrice(serverResponse.body.seatbid[0].bid[0].adm, serverResponse.body.seatbid[0].bid[0].price); expect(ads).to.be.an('array').and.to.have.length(1); expect(ads[0].requestId).to.exist.and.to.be.a('string').and.to.equal(serverRequest._adot_internal.impressions[0].bidId); - expect(ads[0].ad).to.exist.and.to.be.a('string').and.to.have.string(serverResponse.body.seatbid[0].bid[0].adm); + expect(ads[0].ad).to.exist.and.to.be.a('string').and.to.have.string(admWithAuctionPriceReplaced); expect(ads[0].adUrl).to.equal(null); expect(ads[0].vastXml).to.equal(null); expect(ads[0].vastUrl).to.equal(null); @@ -2617,10 +2622,11 @@ describe('Adot Adapter', function () { serverResponse.body.seatbid[0].bid[0].nurl = undefined; const ads = spec.interpretResponse(serverResponse, serverRequest); + const admWithAuctionPriceReplaced = utils.replaceAuctionPrice(serverResponse.body.seatbid[0].bid[0].adm, serverResponse.body.seatbid[0].bid[0].price); expect(ads).to.be.an('array').and.to.have.length(1); expect(ads[0].requestId).to.exist.and.to.be.a('string').and.to.equal(serverRequest._adot_internal.impressions[0].bidId); - expect(ads[0].ad).to.exist.and.to.be.a('string').and.to.equal(serverResponse.body.seatbid[0].bid[0].adm); + expect(ads[0].ad).to.exist.and.to.be.a('string').and.to.equal(admWithAuctionPriceReplaced); expect(ads[0].adUrl).to.equal(null); expect(ads[0].vastXml).to.equal(null); expect(ads[0].vastUrl).to.equal(null); @@ -2642,11 +2648,12 @@ describe('Adot Adapter', function () { serverResponse.body.seatbid[0].bid[0].adm = undefined; const ads = spec.interpretResponse(serverResponse, serverRequest); + const nurlWithAuctionPriceReplaced = utils.replaceAuctionPrice(serverResponse.body.seatbid[0].bid[0].nurl, serverResponse.body.seatbid[0].bid[0].price); expect(ads).to.be.an('array').and.to.have.length(1); expect(ads[0].requestId).to.exist.and.to.be.a('string').and.to.equal(serverRequest._adot_internal.impressions[0].bidId); expect(ads[0].ad).to.equal(null); - expect(ads[0].adUrl).to.exist.and.to.be.a('string').and.to.equal(serverResponse.body.seatbid[0].bid[0].nurl); + expect(ads[0].adUrl).to.exist.and.to.be.a('string').and.to.equal(nurlWithAuctionPriceReplaced); expect(ads[0].creativeId).to.exist.and.to.be.a('string').and.to.equal(serverResponse.body.seatbid[0].bid[0].crid); expect(ads[0].cpm).to.exist.and.to.be.a('number').and.to.equal(serverResponse.body.seatbid[0].bid[0].price); expect(ads[0].currency).to.exist.and.to.be.a('string').and.to.equal(serverResponse.body.cur); @@ -2721,12 +2728,13 @@ describe('Adot Adapter', function () { const serverResponse = examples.serverResponse_video_instream; const ads = spec.interpretResponse(serverResponse, serverRequest); + const admWithAuctionPriceReplaced = utils.replaceAuctionPrice(serverResponse.body.seatbid[0].bid[0].adm, serverResponse.body.seatbid[0].bid[0].price); expect(ads).to.be.an('array').and.to.have.length(1); expect(ads[0].requestId).to.exist.and.to.be.a('string').and.to.equal(serverRequest._adot_internal.impressions[0].bidId); - expect(ads[0].ad).to.exist.and.to.be.a('string').and.to.have.string(serverResponse.body.seatbid[0].bid[0].adm); + expect(ads[0].ad).to.exist.and.to.be.a('string').and.to.have.string(admWithAuctionPriceReplaced); expect(ads[0].adUrl).to.equal(null); - expect(ads[0].vastXml).to.equal(serverResponse.body.seatbid[0].bid[0].adm); + expect(ads[0].vastXml).to.equal(admWithAuctionPriceReplaced); expect(ads[0].vastUrl).to.equal(null); expect(ads[0].creativeId).to.exist.and.to.be.a('string').and.to.equal(serverResponse.body.seatbid[0].bid[0].crid); expect(ads[0].cpm).to.exist.and.to.be.a('number').and.to.equal(serverResponse.body.seatbid[0].bid[0].price); @@ -2745,12 +2753,13 @@ describe('Adot Adapter', function () { const serverResponse = examples.serverResponse_video_outstream; const ads = spec.interpretResponse(serverResponse, serverRequest); + const admWithAuctionPriceReplaced = utils.replaceAuctionPrice(serverResponse.body.seatbid[0].bid[0].adm, serverResponse.body.seatbid[0].bid[0].price); expect(ads).to.be.an('array').and.to.have.length(1); expect(ads[0].requestId).to.exist.and.to.be.a('string').and.to.equal(serverRequest._adot_internal.impressions[0].bidId); - expect(ads[0].ad).to.exist.and.to.be.a('string').and.to.have.string(serverResponse.body.seatbid[0].bid[0].adm); + expect(ads[0].ad).to.exist.and.to.be.a('string').and.to.have.string(admWithAuctionPriceReplaced); expect(ads[0].adUrl).to.equal(null); - expect(ads[0].vastXml).to.equal(serverResponse.body.seatbid[0].bid[0].adm); + expect(ads[0].vastXml).to.equal(admWithAuctionPriceReplaced); expect(ads[0].vastUrl).to.equal(null); expect(ads[0].creativeId).to.exist.and.to.be.a('string').and.to.equal(serverResponse.body.seatbid[0].bid[0].crid); expect(ads[0].cpm).to.exist.and.to.be.a('number').and.to.equal(serverResponse.body.seatbid[0].bid[0].price); @@ -2769,12 +2778,14 @@ describe('Adot Adapter', function () { const serverResponse = examples.serverResponse_video_instream_outstream; const ads = spec.interpretResponse(serverResponse, serverRequest); + const admWithAuctionPriceReplaced = utils.replaceAuctionPrice(serverResponse.body.seatbid[0].bid[0].adm, serverResponse.body.seatbid[0].bid[0].price); + const adm2WithAuctionPriceReplaced = utils.replaceAuctionPrice(serverResponse.body.seatbid[0].bid[1].adm, serverResponse.body.seatbid[0].bid[1].price); expect(ads).to.be.an('array').and.to.have.length(2); expect(ads[0].requestId).to.exist.and.to.be.a('string').and.to.equal(serverRequest._adot_internal.impressions[0].bidId); - expect(ads[0].ad).to.exist.and.to.be.a('string').and.to.have.string(serverResponse.body.seatbid[0].bid[0].adm); + expect(ads[0].ad).to.exist.and.to.be.a('string').and.to.have.string(admWithAuctionPriceReplaced); expect(ads[0].adUrl).to.equal(null); - expect(ads[0].vastXml).to.equal(serverResponse.body.seatbid[0].bid[0].adm); + expect(ads[0].vastXml).to.equal(admWithAuctionPriceReplaced); expect(ads[0].vastUrl).to.equal(null); expect(ads[0].creativeId).to.exist.and.to.be.a('string').and.to.equal(serverResponse.body.seatbid[0].bid[0].crid); expect(ads[0].cpm).to.exist.and.to.be.a('number').and.to.equal(serverResponse.body.seatbid[0].bid[0].price); @@ -2786,9 +2797,9 @@ describe('Adot Adapter', function () { expect(ads[0].mediaType).to.exist.and.to.be.a('string').and.to.equal('video'); expect(ads[0].renderer).to.equal(null); expect(ads[1].requestId).to.exist.and.to.be.a('string').and.to.equal(serverRequest._adot_internal.impressions[1].bidId); - expect(ads[1].ad).to.exist.and.to.be.a('string').and.to.have.string(serverResponse.body.seatbid[0].bid[1].adm); + expect(ads[1].ad).to.exist.and.to.be.a('string').and.to.have.string(adm2WithAuctionPriceReplaced); expect(ads[1].adUrl).to.equal(null); - expect(ads[1].vastXml).to.equal(serverResponse.body.seatbid[0].bid[0].adm); + expect(ads[1].vastXml).to.equal(adm2WithAuctionPriceReplaced); expect(ads[1].vastUrl).to.equal(null); expect(ads[1].creativeId).to.exist.and.to.be.a('string').and.to.equal(serverResponse.body.seatbid[0].bid[1].crid); expect(ads[1].cpm).to.exist.and.to.be.a('number').and.to.equal(serverResponse.body.seatbid[0].bid[1].price); @@ -2808,12 +2819,13 @@ describe('Adot Adapter', function () { serverResponse.body.seatbid[0].bid[0].nurl = undefined; const ads = spec.interpretResponse(serverResponse, serverRequest); + const admWithAuctionPriceReplaced = utils.replaceAuctionPrice(serverResponse.body.seatbid[0].bid[0].adm, serverResponse.body.seatbid[0].bid[0].price); expect(ads).to.be.an('array').and.to.have.length(1); expect(ads[0].requestId).to.exist.and.to.be.a('string').and.to.equal(serverRequest._adot_internal.impressions[0].bidId); - expect(ads[0].ad).to.exist.and.to.be.a('string').and.to.have.string(serverResponse.body.seatbid[0].bid[0].adm); + expect(ads[0].ad).to.exist.and.to.be.a('string').and.to.have.string(admWithAuctionPriceReplaced); expect(ads[0].adUrl).to.equal(null); - expect(ads[0].vastXml).to.equal(serverResponse.body.seatbid[0].bid[0].adm); + expect(ads[0].vastXml).to.equal(admWithAuctionPriceReplaced); expect(ads[0].vastUrl).to.equal(null); expect(ads[0].creativeId).to.exist.and.to.be.a('string').and.to.equal(serverResponse.body.seatbid[0].bid[0].crid); expect(ads[0].cpm).to.exist.and.to.be.a('number').and.to.equal(serverResponse.body.seatbid[0].bid[0].price); @@ -2833,13 +2845,14 @@ describe('Adot Adapter', function () { serverResponse.body.seatbid[0].bid[0].adm = undefined; const ads = spec.interpretResponse(serverResponse, serverRequest); + const nurlWithAuctionPriceReplaced = utils.replaceAuctionPrice(serverResponse.body.seatbid[0].bid[0].nurl, serverResponse.body.seatbid[0].bid[0].price); expect(ads).to.be.an('array').and.to.have.length(1); expect(ads[0].requestId).to.exist.and.to.be.a('string').and.to.equal(serverRequest._adot_internal.impressions[0].bidId); expect(ads[0].ad).to.equal(null); - expect(ads[0].adUrl).to.exist.and.to.be.a('string').and.to.have.string(serverResponse.body.seatbid[0].bid[0].nurl); + expect(ads[0].adUrl).to.exist.and.to.be.a('string').and.to.have.string(nurlWithAuctionPriceReplaced); expect(ads[0].vastXml).to.equal(null); - expect(ads[0].vastUrl).to.equal(serverResponse.body.seatbid[0].bid[0].nurl); + expect(ads[0].vastUrl).to.equal(nurlWithAuctionPriceReplaced); expect(ads[0].creativeId).to.exist.and.to.be.a('string').and.to.equal(serverResponse.body.seatbid[0].bid[0].crid); expect(ads[0].cpm).to.exist.and.to.be.a('number').and.to.equal(serverResponse.body.seatbid[0].bid[0].price); expect(ads[0].currency).to.exist.and.to.be.a('string').and.to.equal(serverResponse.body.cur); @@ -2858,12 +2871,13 @@ describe('Adot Adapter', function () { serverResponse.body.seatbid[0].bid[0].h = 500; const ads = spec.interpretResponse(serverResponse, serverRequest); + const admWithAuctionPriceReplaced = utils.replaceAuctionPrice(serverResponse.body.seatbid[0].bid[0].adm, serverResponse.body.seatbid[0].bid[0].price); expect(ads).to.be.an('array').and.to.have.length(1); expect(ads[0].requestId).to.exist.and.to.be.a('string').and.to.equal(serverRequest._adot_internal.impressions[0].bidId); - expect(ads[0].ad).to.exist.and.to.be.a('string').and.to.have.string(serverResponse.body.seatbid[0].bid[0].adm); + expect(ads[0].ad).to.exist.and.to.be.a('string').and.to.have.string(admWithAuctionPriceReplaced); expect(ads[0].adUrl).to.equal(null); - expect(ads[0].vastXml).to.equal(serverResponse.body.seatbid[0].bid[0].adm); + expect(ads[0].vastXml).to.equal(admWithAuctionPriceReplaced); expect(ads[0].vastUrl).to.equal(null); expect(ads[0].creativeId).to.exist.and.to.be.a('string').and.to.equal(serverResponse.body.seatbid[0].bid[0].crid); expect(ads[0].cpm).to.exist.and.to.be.a('number').and.to.equal(serverResponse.body.seatbid[0].bid[0].price); @@ -2883,12 +2897,13 @@ describe('Adot Adapter', function () { serverResponse.body.seatbid[0].bid[0].w = 500; const ads = spec.interpretResponse(serverResponse, serverRequest); + const admWithAuctionPriceReplaced = utils.replaceAuctionPrice(serverResponse.body.seatbid[0].bid[0].adm, serverResponse.body.seatbid[0].bid[0].price); expect(ads).to.be.an('array').and.to.have.length(1); expect(ads[0].requestId).to.exist.and.to.be.a('string').and.to.equal(serverRequest._adot_internal.impressions[0].bidId); - expect(ads[0].ad).to.exist.and.to.be.a('string').and.to.have.string(serverResponse.body.seatbid[0].bid[0].adm); + expect(ads[0].ad).to.exist.and.to.be.a('string').and.to.have.string(admWithAuctionPriceReplaced); expect(ads[0].adUrl).to.equal(null); - expect(ads[0].vastXml).to.equal(serverResponse.body.seatbid[0].bid[0].adm); + expect(ads[0].vastXml).to.equal(admWithAuctionPriceReplaced); expect(ads[0].vastUrl).to.equal(null); expect(ads[0].creativeId).to.exist.and.to.be.a('string').and.to.equal(serverResponse.body.seatbid[0].bid[0].crid); expect(ads[0].cpm).to.exist.and.to.be.a('number').and.to.equal(serverResponse.body.seatbid[0].bid[0].price); @@ -2909,12 +2924,13 @@ describe('Adot Adapter', function () { serverResponse.body.seatbid[0].bid[0].h = 400; const ads = spec.interpretResponse(serverResponse, serverRequest); + const admWithAuctionPriceReplaced = utils.replaceAuctionPrice(serverResponse.body.seatbid[0].bid[0].adm, serverResponse.body.seatbid[0].bid[0].price); expect(ads).to.be.an('array').and.to.have.length(1); expect(ads[0].requestId).to.exist.and.to.be.a('string').and.to.equal(serverRequest._adot_internal.impressions[0].bidId); - expect(ads[0].ad).to.exist.and.to.be.a('string').and.to.have.string(serverResponse.body.seatbid[0].bid[0].adm); + expect(ads[0].ad).to.exist.and.to.be.a('string').and.to.have.string(admWithAuctionPriceReplaced); expect(ads[0].adUrl).to.equal(null); - expect(ads[0].vastXml).to.equal(serverResponse.body.seatbid[0].bid[0].adm); + expect(ads[0].vastXml).to.equal(admWithAuctionPriceReplaced); expect(ads[0].vastUrl).to.equal(null); expect(ads[0].creativeId).to.exist.and.to.be.a('string').and.to.equal(serverResponse.body.seatbid[0].bid[0].crid); expect(ads[0].cpm).to.exist.and.to.be.a('number').and.to.equal(serverResponse.body.seatbid[0].bid[0].price); @@ -2934,12 +2950,13 @@ describe('Adot Adapter', function () { const serverResponse = utils.deepClone(examples.serverResponse_video_instream); const ads = spec.interpretResponse(serverResponse, serverRequest); + const admWithAuctionPriceReplaced = utils.replaceAuctionPrice(serverResponse.body.seatbid[0].bid[0].adm, serverResponse.body.seatbid[0].bid[0].price); expect(ads).to.be.an('array').and.to.have.length(1); expect(ads[0].requestId).to.exist.and.to.be.a('string').and.to.equal(serverRequest._adot_internal.impressions[0].bidId); - expect(ads[0].ad).to.exist.and.to.be.a('string').and.to.have.string(serverResponse.body.seatbid[0].bid[0].adm); + expect(ads[0].ad).to.exist.and.to.be.a('string').and.to.have.string(admWithAuctionPriceReplaced); expect(ads[0].adUrl).to.equal(null); - expect(ads[0].vastXml).to.equal(serverResponse.body.seatbid[0].bid[0].adm); + expect(ads[0].vastXml).to.equal(admWithAuctionPriceReplaced); expect(ads[0].vastUrl).to.equal(null); expect(ads[0].creativeId).to.exist.and.to.be.a('string').and.to.equal(serverResponse.body.seatbid[0].bid[0].crid); expect(ads[0].cpm).to.exist.and.to.be.a('number').and.to.equal(serverResponse.body.seatbid[0].bid[0].price); @@ -2959,12 +2976,13 @@ describe('Adot Adapter', function () { const serverResponse = utils.deepClone(examples.serverResponse_video_instream); const ads = spec.interpretResponse(serverResponse, serverRequest); + const admWithAuctionPriceReplaced = utils.replaceAuctionPrice(serverResponse.body.seatbid[0].bid[0].adm, serverResponse.body.seatbid[0].bid[0].price); expect(ads).to.be.an('array').and.to.have.length(1); expect(ads[0].requestId).to.exist.and.to.be.a('string').and.to.equal(serverRequest._adot_internal.impressions[0].bidId); - expect(ads[0].ad).to.exist.and.to.be.a('string').and.to.have.string(serverResponse.body.seatbid[0].bid[0].adm); + expect(ads[0].ad).to.exist.and.to.be.a('string').and.to.have.string(admWithAuctionPriceReplaced); expect(ads[0].adUrl).to.equal(null); - expect(ads[0].vastXml).to.equal(serverResponse.body.seatbid[0].bid[0].adm); + expect(ads[0].vastXml).to.equal(admWithAuctionPriceReplaced); expect(ads[0].vastUrl).to.equal(null); expect(ads[0].creativeId).to.exist.and.to.be.a('string').and.to.equal(serverResponse.body.seatbid[0].bid[0].crid); expect(ads[0].cpm).to.exist.and.to.be.a('number').and.to.equal(serverResponse.body.seatbid[0].bid[0].price); diff --git a/test/spec/modules/adponeBidAdapter_spec.js b/test/spec/modules/adponeBidAdapter_spec.js index 737f1c284e1..92fd672df47 100644 --- a/test/spec/modules/adponeBidAdapter_spec.js +++ b/test/spec/modules/adponeBidAdapter_spec.js @@ -110,122 +110,109 @@ describe('adponeBidAdapter', function () { expect(spec.isBidRequestValid(invalidBid)).to.be.false; }); }); -}); - -describe('interpretResponse', function () { - let serverResponse; - let bidRequest = { data: {id: '1234'} }; - - beforeEach(function () { - serverResponse = { - body: { - id: '2579e20c0bb89', - seatbid: [ - { - bid: [ - { - id: '613673EF-A07C-4486-8EE9-3FC71A7DC73D', - impid: '2579e20c0bb89_0', - price: 1, - adm: '', - adomain: [ - 'www.addomain.com' - ], - iurl: 'https://localhost11', - crid: 'creative111', - h: 250, - w: 300, - ext: { - dspid: 6 + describe('interpretResponse', function () { + let serverResponse; + let bidRequest = { data: {id: '1234'} }; + + beforeEach(function () { + serverResponse = { + body: { + id: '2579e20c0bb89', + seatbid: [ + { + bid: [ + { + id: '613673EF-A07C-4486-8EE9-3FC71A7DC73D', + impid: '2579e20c0bb89_0', + price: 1, + adm: '', + meta: { + adomain: [ + 'adpone.com' + ] + }, + iurl: 'https://localhost11', + crid: 'creative111', + h: 250, + w: 300, + ext: { + dspid: 6 + } } - } - ], - seat: 'adpone' - } - ], - cur: 'USD' - }, - }; - }); - - it('validate_response_params', function() { - const newResponse = spec.interpretResponse(serverResponse, bidRequest); - expect(newResponse[0].id).to.be.equal('613673EF-A07C-4486-8EE9-3FC71A7DC73D'); - expect(newResponse[0].requestId).to.be.equal('1234'); - expect(newResponse[0].cpm).to.be.equal(1); - expect(newResponse[0].width).to.be.equal(300); - expect(newResponse[0].height).to.be.equal(250); - expect(newResponse[0].currency).to.be.equal('USD'); - expect(newResponse[0].netRevenue).to.be.equal(true); - expect(newResponse[0].ttl).to.be.equal(300); - expect(newResponse[0].ad).to.be.equal(''); - }); + ], + seat: 'adpone' + } + ], + cur: 'USD' + }, + }; + }); - it('should correctly reorder the server response', function () { - const newResponse = spec.interpretResponse(serverResponse, bidRequest); - expect(newResponse.length).to.be.equal(1); - expect(newResponse[0]).to.deep.equal({ - id: '613673EF-A07C-4486-8EE9-3FC71A7DC73D', - requestId: '1234', - cpm: 1, - width: 300, - height: 250, - creativeId: 'creative111', - currency: 'USD', - netRevenue: true, - ttl: 300, - ad: '' + it('validate_response_params', function() { + const newResponse = spec.interpretResponse(serverResponse, bidRequest); + expect(newResponse[0].id).to.be.equal('613673EF-A07C-4486-8EE9-3FC71A7DC73D'); + expect(newResponse[0].requestId).to.be.equal('1234'); + expect(newResponse[0].cpm).to.be.equal(1); + expect(newResponse[0].width).to.be.equal(300); + expect(newResponse[0].height).to.be.equal(250); + expect(newResponse[0].currency).to.be.equal('USD'); + expect(newResponse[0].netRevenue).to.be.equal(true); + expect(newResponse[0].ttl).to.be.equal(300); + expect(newResponse[0].ad).to.be.equal(''); }); - }); - it('should not add responses if the cpm is 0 or null', function () { - serverResponse.body.seatbid[0].bid[0].price = 0; - let response = spec.interpretResponse(serverResponse, bidRequest); - expect(response).to.deep.equal([]); + it('should correctly reorder the server response', function () { + const newResponse = spec.interpretResponse(serverResponse, bidRequest); + expect(newResponse.length).to.be.equal(1); + expect(newResponse[0]).to.deep.equal({ + id: '613673EF-A07C-4486-8EE9-3FC71A7DC73D', + meta: { + advertiserDomains: [ + 'adpone.com' + ] + }, + requestId: '1234', + cpm: 1, + width: 300, + height: 250, + creativeId: 'creative111', + currency: 'USD', + netRevenue: true, + ttl: 300, + ad: '' + }); + }); - serverResponse.body.seatbid[0].bid[0].price = null; - response = spec.interpretResponse(serverResponse, bidRequest); - expect(response).to.deep.equal([]) - }); - it('should add responses if the cpm is valid', function () { - serverResponse.body.seatbid[0].bid[0].price = 0.5; - let response = spec.interpretResponse(serverResponse, bidRequest); - expect(response).to.not.deep.equal([]); - }); -}); + it('should not add responses if the cpm is 0 or null', function () { + serverResponse.body.seatbid[0].bid[0].price = 0; + let response = spec.interpretResponse(serverResponse, bidRequest); + expect(response).to.deep.equal([]); -describe('getUserSyncs', function () { - it('Verifies that getUserSyncs is a function', function () { - expect((typeof (spec.getUserSyncs)).should.equals('function')); - }); - it('Verifies getUserSyncs returns expected result', function () { - expect((typeof (spec.getUserSyncs)).should.equals('function')); - expect(spec.getUserSyncs({iframeEnabled: true})).to.deep.equal({ - type: 'iframe', - url: 'https://eu-ads.adpone.com' + serverResponse.body.seatbid[0].bid[0].price = null; + response = spec.interpretResponse(serverResponse, bidRequest); + expect(response).to.deep.equal([]) + }); + it('should add responses if the cpm is valid', function () { + serverResponse.body.seatbid[0].bid[0].price = 0.5; + let response = spec.interpretResponse(serverResponse, bidRequest); + expect(response).to.not.deep.equal([]); }); }); - it('Verifies that iframeEnabled: false returns an empty array', function () { - expect(spec.getUserSyncs({iframeEnabled: false})).to.deep.equal(EMPTY_ARRAY); - }); - it('Verifies that iframeEnabled: null returns an empty array', function () { - expect(spec.getUserSyncs(null)).to.deep.equal(EMPTY_ARRAY); - }); -}); -describe('test onBidWon function', function () { - beforeEach(function() { - sinon.stub(utils, 'triggerPixel'); - }); - afterEach(function() { - utils.triggerPixel.restore(); - }); - it('exists and is a function', () => { - expect(spec.onBidWon).to.exist.and.to.be.a('function'); - }); - it('should return nothing', function () { - var response = spec.onBidWon({}); - expect(response).to.be.an('undefined') - expect(utils.triggerPixel.called).to.equal(true); + describe('test onBidWon function', function () { + beforeEach(function() { + sinon.stub(utils, 'triggerPixel'); + }); + afterEach(function() { + utils.triggerPixel.restore(); + }); + it('exists and is a function', () => { + expect(spec.onBidWon).to.exist.and.to.be.a('function'); + }); + it('should return nothing', function () { + var response = spec.onBidWon({}); + expect(response).to.be.an('undefined') + expect(utils.triggerPixel.called).to.equal(true); + }); }); }); 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/adrelevantisBidAdapter_spec.js b/test/spec/modules/adrelevantisBidAdapter_spec.js new file mode 100644 index 00000000000..596f3bade5d --- /dev/null +++ b/test/spec/modules/adrelevantisBidAdapter_spec.js @@ -0,0 +1,771 @@ +import { expect } from 'chai'; +import { spec } from 'modules/adrelevantisBidAdapter.js'; +import { newBidder } from 'src/adapters/bidderFactory.js'; +import * as bidderFactory from 'src/adapters/bidderFactory.js'; +import { deepClone } from 'src/utils.js'; +import { config } from 'src/config.js'; + +const ENDPOINT = 'https://ssp.adrelevantis.com/prebid'; + +describe('AdrelevantisAdapter', function () { + const adapter = newBidder(spec); + + describe('inherited functions', function () { + it('exists and is a function', function () { + expect(adapter.callBids).to.exist.and.to.be.a('function'); + }); + }); + + describe('isBidRequestValid', function () { + let bid = { + 'bidder': 'adrelevantis', + 'params': { + 'placementId': '10433394' + }, + 'adUnitCode': 'adunit-code', + 'sizes': [[300, 250], [300, 600]], + 'bidId': '30b31c1838de1e', + 'bidderRequestId': '22edbae2733bf6', + 'auctionId': '1d1a030790a475', + }; + + it('should return true when required params found', function () { + expect(spec.isBidRequestValid(bid)).to.equal(true); + }); + + it('should return false when required params are not passed', function () { + let bid = Object.assign({}, bid); + delete bid.params; + bid.params = { + 'placementId': 0 + }; + expect(spec.isBidRequestValid(bid)).to.equal(false); + }); + }); + + describe('buildRequests', function () { + let bidRequests = [ + { + 'bidder': 'adrelevantis', + 'params': { + 'placementId': '10433394' + }, + 'adUnitCode': 'adunit-code', + 'sizes': [[300, 250], [300, 600]], + 'bidId': '30b31c1838de1e', + 'bidderRequestId': '22edbae2733bf6', + 'auctionId': '1d1a030790a475', + } + ]; + + it('should parse out private sizes', function () { + let bidRequest = Object.assign({}, + bidRequests[0], + { + params: { + placementId: '10433394', + privateSizes: [300, 250] + } + } + ); + + const request = spec.buildRequests([bidRequest]); + const payload = JSON.parse(request.data); + + expect(payload.tags[0].private_sizes).to.exist; + expect(payload.tags[0].private_sizes).to.deep.equal([{width: 300, height: 250}]); + }); + + it('should add source and verison to the tag', function () { + const request = spec.buildRequests(bidRequests); + const payload = JSON.parse(request.data); + expect(payload.sdk).to.exist; + expect(payload.sdk).to.deep.equal({ + source: 'pbjs', + version: '$prebid.version$' + }); + }); + + it('should populate the ad_types array on all requests', function () { + ['banner', 'video', 'native'].forEach(type => { + const bidRequest = Object.assign({}, bidRequests[0]); + bidRequest.mediaTypes = {}; + bidRequest.mediaTypes[type] = {}; + + const request = spec.buildRequests([bidRequest]); + const payload = JSON.parse(request.data); + + expect(payload.tags[0].ad_types).to.deep.equal([type]); + }); + }); + + it('should populate the ad_types array on outstream requests', function () { + const bidRequest = Object.assign({}, bidRequests[0]); + bidRequest.mediaTypes = {}; + bidRequest.mediaTypes.video = {context: 'outstream'}; + + const request = spec.buildRequests([bidRequest]); + const payload = JSON.parse(request.data); + + expect(payload.tags[0].ad_types).to.deep.equal(['video']); + }); + + it('sends bid request to ENDPOINT via POST', function () { + const request = spec.buildRequests(bidRequests); + expect(request.url).to.equal(ENDPOINT); + expect(request.method).to.equal('POST'); + }); + + it('should attach valid video params to the tag', function () { + let bidRequest = Object.assign({}, + bidRequests[0], + { + params: { + placementId: '10433394', + video: { + id: 123, + minduration: 100, + foobar: 'invalid' + } + } + } + ); + + const request = spec.buildRequests([bidRequest]); + const payload = JSON.parse(request.data); + expect(payload.tags[0].video).to.deep.equal({ + id: 123, + minduration: 100 + }); + }); + + it('should add video property when adUnit includes a renderer', function () { + const videoData = { + mediaTypes: { + video: { + context: 'outstream', + mimes: ['video/mp4'] + } + }, + params: { + placementId: '10433394', + video: { + skippable: true, + playback_method: ['auto_play_sound_off'] + } + } + }; + + let bidRequest1 = deepClone(bidRequests[0]); + bidRequest1 = Object.assign({}, bidRequest1, videoData, { + renderer: { + url: 'http://test.renderer.url', + render: function () {} + } + }); + + let bidRequest2 = deepClone(bidRequests[0]); + bidRequest2.adUnitCode = 'adUnit_code_2'; + bidRequest2 = Object.assign({}, bidRequest2, videoData); + + const request = spec.buildRequests([bidRequest1, bidRequest2]); + const payload = JSON.parse(request.data); + expect(payload.tags[0].video).to.deep.equal({ + skippable: true, + playback_method: ['auto_play_sound_off'], + custom_renderer_present: true + }); + expect(payload.tags[1].video).to.deep.equal({ + skippable: true, + playback_method: ['auto_play_sound_off'] + }); + }); + + it('should attach valid user params to the tag', function () { + let bidRequest = Object.assign({}, + bidRequests[0], + { + params: { + placementId: '10433394', + user: { + externalUid: '123', + foobar: 'invalid' + } + } + } + ); + + const request = spec.buildRequests([bidRequest]); + const payload = JSON.parse(request.data); + + expect(payload.user).to.exist; + expect(payload.user).to.deep.equal({ + externalUid: '123', + }); + }); + + it('should contain hb_source value for other media', function() { + let bidRequest = Object.assign({}, + bidRequests[0], + { + mediaType: 'banner', + params: { + sizes: [[300, 250], [300, 600]], + placementId: 10433394 + } + } + ); + const request = spec.buildRequests([bidRequest]); + const payload = JSON.parse(request.data); + expect(payload.tags[0].hb_source).to.deep.equal(1); + }); + + it('adds context data (category and keywords) to request when set', function() { + let bidRequest = Object.assign({}, bidRequests[0]); + sinon + .stub(config, 'getConfig') + .withArgs('ortb2') + .returns({ + site: { + keywords: 'US Open', + ext: { + data: { + category: 'sports/tennis' + } + } + } + }); + + const request = spec.buildRequests([bidRequest]); + const payload = JSON.parse(request.data); + + expect(payload.fpd.keywords).to.equal('US Open'); + expect(payload.fpd.category).to.equal('sports/tennis'); + + config.getConfig.restore(); + }); + + it('should attach native params to the request', function () { + let bidRequest = Object.assign({}, + bidRequests[0], + { + mediaType: 'native', + nativeParams: { + title: {required: true}, + body: {required: true}, + body2: {required: true}, + image: {required: true, sizes: [100, 100]}, + icon: {required: true}, + cta: {required: false}, + rating: {required: true}, + sponsoredBy: {required: true}, + privacyLink: {required: true}, + displayUrl: {required: true}, + address: {required: true}, + downloads: {required: true}, + likes: {required: true}, + phone: {required: true}, + price: {required: true}, + salePrice: {required: true} + } + } + ); + + const request = spec.buildRequests([bidRequest]); + const payload = JSON.parse(request.data); + + expect(payload.tags[0].native.layouts[0]).to.deep.equal({ + title: {required: true}, + description: {required: true}, + desc2: {required: true}, + main_image: {required: true, sizes: [{ width: 100, height: 100 }]}, + icon: {required: true}, + ctatext: {required: false}, + rating: {required: true}, + sponsored_by: {required: true}, + privacy_link: {required: true}, + displayurl: {required: true}, + address: {required: true}, + downloads: {required: true}, + likes: {required: true}, + phone: {required: true}, + price: {required: true}, + saleprice: {required: true}, + privacy_supported: true + }); + expect(payload.tags[0].hb_source).to.equal(1); + }); + + it('should always populated tags[].sizes with 1,1 for native if otherwise not defined', function () { + let bidRequest = Object.assign({}, + bidRequests[0], + { + mediaType: 'native', + nativeParams: { + image: { required: true } + } + } + ); + bidRequest.sizes = [[150, 100], [300, 250]]; + + let request = spec.buildRequests([bidRequest]); + let payload = JSON.parse(request.data); + expect(payload.tags[0].sizes).to.deep.equal([{width: 150, height: 100}, {width: 300, height: 250}]); + + delete bidRequest.sizes; + + request = spec.buildRequests([bidRequest]); + payload = JSON.parse(request.data); + + expect(payload.tags[0].sizes).to.deep.equal([{width: 1, height: 1}]); + }); + + it('should convert keyword params to proper form and attaches to request', function () { + let bidRequest = Object.assign({}, + bidRequests[0], + { + params: { + placementId: '10433394', + keywords: { + single: 'val', + singleArr: ['val'], + singleArrNum: [5], + multiValMixed: ['value1', 2, 'value3'], + singleValNum: 123, + emptyStr: '', + emptyArr: [''], + badValue: {'foo': 'bar'} // should be dropped + } + } + } + ); + + const request = spec.buildRequests([bidRequest]); + const payload = JSON.parse(request.data); + + expect(payload.tags[0].keywords).to.deep.equal([{ + 'key': 'single', + 'value': ['val'] + }, { + 'key': 'singleArr', + 'value': ['val'] + }, { + 'key': 'singleArrNum', + 'value': ['5'] + }, { + 'key': 'multiValMixed', + 'value': ['value1', '2', 'value3'] + }, { + 'key': 'singleValNum', + 'value': ['123'] + }, { + 'key': 'emptyStr' + }, { + 'key': 'emptyArr' + }]); + }); + + it('should add payment rules to the request', function () { + let bidRequest = Object.assign({}, + bidRequests[0], + { + params: { + placementId: '10433394', + usePaymentRule: true + } + } + ); + + const request = spec.buildRequests([bidRequest]); + const payload = JSON.parse(request.data); + + expect(payload.tags[0].use_pmt_rule).to.equal(true); + }); + + it('should add gdpr consent information to the request', function () { + let consentString = 'BOJ8RZsOJ8RZsABAB8AAAAAZ+A=='; + let bidderRequest = { + 'bidderCode': 'adrelevantis', + 'auctionId': '1d1a030790a475', + 'bidderRequestId': '22edbae2733bf6', + 'timeout': 3000, + 'gdprConsent': { + consentString: consentString, + gdprApplies: true + } + }; + bidderRequest.bids = bidRequests; + + const request = spec.buildRequests(bidRequests, bidderRequest); + const payload = JSON.parse(request.data); + + expect(payload.gdpr_consent).to.exist; + expect(payload.gdpr_consent.consent_string).to.exist.and.to.equal(consentString); + expect(payload.gdpr_consent.consent_required).to.exist.and.to.be.true; + }); + + it('supports sending hybrid mobile app parameters', function () { + let appRequest = Object.assign({}, + bidRequests[0], + { + params: { + placementId: '10433394', + app: { + id: 'B1O2W3M4AN.com.prebid.webview', + geo: { + lat: 40.0964439, + lng: -75.3009142 + }, + device_id: { + idfa: '4D12078D-3246-4DA4-AD5E-7610481E7AE', // Apple advertising identifier + aaid: '38400000-8cf0-11bd-b23e-10b96e40000d', // Android advertising identifier + md5udid: '5756ae9022b2ea1e47d84fead75220c8', // MD5 hash of the ANDROID_ID + sha1udid: '4DFAA92388699AC6539885AEF1719293879985BF', // SHA1 hash of the ANDROID_ID + windowsadid: '750c6be243f1c4b5c9912b95a5742fc5' // Windows advertising identifier + } + } + } + } + ); + const request = spec.buildRequests([appRequest]); + const payload = JSON.parse(request.data); + expect(payload.app).to.exist; + expect(payload.app).to.deep.equal({ + appid: 'B1O2W3M4AN.com.prebid.webview' + }); + expect(payload.device.device_id).to.exist; + expect(payload.device.device_id).to.deep.equal({ + aaid: '38400000-8cf0-11bd-b23e-10b96e40000d', + idfa: '4D12078D-3246-4DA4-AD5E-7610481E7AE', + md5udid: '5756ae9022b2ea1e47d84fead75220c8', + sha1udid: '4DFAA92388699AC6539885AEF1719293879985BF', + windowsadid: '750c6be243f1c4b5c9912b95a5742fc5' + }); + expect(payload.device.geo).to.exist; + expect(payload.device.geo).to.deep.equal({ + lat: 40.0964439, + lng: -75.3009142 + }); + }); + + it('should add referer info to payload', function () { + const bidRequest = Object.assign({}, bidRequests[0]) + const bidderRequest = { + refererInfo: { + referer: 'http://example.com/page.html', + reachedTop: true, + numIframes: 2, + stack: [ + 'http://example.com/page.html', + 'http://example.com/iframe1.html', + 'http://example.com/iframe2.html' + ] + } + } + const request = spec.buildRequests([bidRequest], bidderRequest); + const payload = JSON.parse(request.data); + + expect(payload.referrer_detection).to.exist; + expect(payload.referrer_detection).to.deep.equal({ + rd_ref: 'http%3A%2F%2Fexample.com%2Fpage.html', + rd_top: true, + rd_ifs: 2, + rd_stk: bidderRequest.refererInfo.stack.map((url) => encodeURIComponent(url)).join(',') + }); + }); + + it('should populate coppa if set in config', function () { + let bidRequest = Object.assign({}, bidRequests[0]); + sinon.stub(config, 'getConfig') + .withArgs('coppa') + .returns(true); + + const request = spec.buildRequests([bidRequest]); + const payload = JSON.parse(request.data); + + expect(payload.user.coppa).to.equal(true); + + config.getConfig.restore(); + }); + }) + + describe('interpretResponse', function () { + let bfStub; + before(function() { + bfStub = sinon.stub(bidderFactory, 'getIabSubCategory'); + }); + + after(function() { + bfStub.restore(); + }); + + let response = { + 'version': '3.0.0', + 'tags': [ + { + 'uuid': '3db3773286ee59', + 'tag_id': 10433394, + 'auction_id': '4534722592064951574', + 'nobid': false, + 'no_ad_url': 'http://lax1-ib.adnxs.com/no-ad', + 'timeout_ms': 10000, + 'ad_profile_id': 27079, + 'ads': [ + { + 'content_source': 'rtb', + 'ad_type': 'banner', + 'buyer_member_id': 958, + 'creative_id': 29681110, + 'media_type_id': 1, + 'media_subtype_id': 1, + 'cpm': 0.5, + 'cpm_publisher_currency': 0.5, + 'publisher_currency_code': '$', + 'client_initiated_ad_counting': true, + 'viewability': { + 'config': '' + }, + 'rtb': { + 'banner': { + 'content': '', + 'width': 300, + 'height': 250 + }, + 'trackers': [ + { + 'impression_urls': [ + 'http://lax1-ib.adnxs.com/impression' + ], + 'video_events': {} + } + ] + } + } + ] + } + ] + }; + + it('should get correct bid response', function () { + let expectedResponse = [ + { + 'requestId': '3db3773286ee59', + 'cpm': 0.5, + 'creativeId': 29681110, + 'dealId': undefined, + 'width': 300, + 'height': 250, + 'ad': '', + 'mediaType': 'banner', + 'currency': 'USD', + 'ttl': 300, + 'netRevenue': true, + 'adUnitCode': 'code', + 'adrelevantis': { + 'buyerMemberId': 958 + } + } + ]; + let bidderRequest = { + bids: [{ + bidId: '3db3773286ee59', + adUnitCode: 'code' + }] + } + let result = spec.interpretResponse({ body: response }, {bidderRequest}); + expect(Object.keys(result[0])).to.have.members(Object.keys(expectedResponse[0])); + }); + + it('handles nobid responses', function () { + let response = { + 'version': '0.0.1', + 'tags': [{ + 'uuid': '84ab500420319d', + 'tag_id': 5976557, + 'auction_id': '297492697822162468', + 'nobid': true + }] + }; + let bidderRequest; + + let result = spec.interpretResponse({ body: response }, {bidderRequest}); + expect(result.length).to.equal(0); + }); + + it('handles outstream video responses', function () { + let response = { + 'tags': [{ + 'uuid': '84ab500420319d', + 'ads': [{ + 'ad_type': 'video', + 'cpm': 0.500000, + 'notify_url': 'imptracker.com', + 'rtb': { + 'video': { + 'content': '' + } + }, + 'javascriptTrackers': '' + }] + }] + }; + let bidderRequest = { + bids: [{ + bidId: '84ab500420319d', + adUnitCode: 'code', + mediaTypes: { + video: { + context: 'outstream' + } + } + }] + } + + let result = spec.interpretResponse({ body: response }, {bidderRequest}); + expect(result[0]).to.have.property('vastXml'); + expect(result[0]).to.have.property('vastImpUrl'); + expect(result[0]).to.have.property('mediaType', 'video'); + }); + + it('handles instream video responses', function () { + let response = { + 'tags': [{ + 'uuid': '84ab500420319d', + 'ads': [{ + 'ad_type': 'video', + 'cpm': 0.500000, + 'notify_url': 'imptracker.com', + 'rtb': { + 'video': { + 'asset_url': 'https://sample.vastURL.com/here/vid' + } + }, + 'javascriptTrackers': '' + }] + }] + }; + let bidderRequest = { + bids: [{ + bidId: '84ab500420319d', + adUnitCode: 'code', + mediaTypes: { + video: { + context: 'instream' + } + } + }] + } + + let result = spec.interpretResponse({ body: response }, {bidderRequest}); + expect(result[0]).to.have.property('vastUrl'); + expect(result[0]).to.have.property('vastImpUrl'); + expect(result[0]).to.have.property('mediaType', 'video'); + }); + + it('handles native responses', function () { + let response1 = deepClone(response); + response1.tags[0].ads[0].ad_type = 'native'; + response1.tags[0].ads[0].rtb.native = { + 'title': 'Native Creative', + 'desc': 'Cool description great stuff', + 'desc2': 'Additional body text', + 'ctatext': 'Do it', + 'sponsored': 'AppNexus', + 'icon': { + 'width': 0, + 'height': 0, + 'url': 'https://cdn.adnxs.com/icon.png' + }, + 'main_img': { + 'width': 2352, + 'height': 1516, + 'url': 'https://cdn.adnxs.com/img.png' + }, + 'link': { + 'url': 'https://www.appnexus.com', + 'fallback_url': '', + 'click_trackers': ['https://nym1-ib.adnxs.com/click'] + }, + 'impression_trackers': ['https://example.com'], + 'rating': '5', + 'displayurl': 'https://AppNexus.com/?url=display_url', + 'likes': '38908320', + 'downloads': '874983', + 'price': '9.99', + 'saleprice': 'FREE', + 'phone': '1234567890', + 'address': '28 W 23rd St, New York, NY 10010', + 'privacy_link': 'https://appnexus.com/?url=privacy_url', + 'javascriptTrackers': '' + }; + let bidderRequest = { + bids: [{ + bidId: '3db3773286ee59', + adUnitCode: 'code' + }] + } + + let result = spec.interpretResponse({ body: response1 }, {bidderRequest}); + expect(result[0].native.title).to.equal('Native Creative'); + expect(result[0].native.body).to.equal('Cool description great stuff'); + expect(result[0].native.cta).to.equal('Do it'); + expect(result[0].native.image.url).to.equal('https://cdn.adnxs.com/img.png'); + }); + + it('supports configuring outstream renderers', function () { + const outstreamResponse = deepClone(response); + outstreamResponse.tags[0].ads[0].rtb.video = {}; + outstreamResponse.tags[0].ads[0].renderer_url = 'renderer.js'; + + const bidderRequest = { + bids: [{ + bidId: '3db3773286ee59', + renderer: { + options: { + adText: 'configured' + } + }, + mediaTypes: { + video: { + context: 'outstream' + } + } + }] + }; + + const result = spec.interpretResponse({ body: outstreamResponse }, {bidderRequest}); + expect(result[0].renderer.config).to.deep.equal( + bidderRequest.bids[0].renderer.options + ); + }); + + it('should add deal_priority and deal_code', function() { + let responseWithDeal = deepClone(response); + responseWithDeal.tags[0].ads[0].deal_priority = 'high'; + responseWithDeal.tags[0].ads[0].deal_code = '123'; + + let bidderRequest = { + bids: [{ + bidId: '3db3773286ee59', + adUnitCode: 'code' + }] + } + let result = spec.interpretResponse({ body: responseWithDeal }, {bidderRequest}); + expect(Object.keys(result[0].adrelevantis)).to.include.members(['buyerMemberId', 'dealPriority', 'dealCode']); + }); + + it('should add advertiser id', function() { + let responseAdvertiserId = deepClone(response); + responseAdvertiserId.tags[0].ads[0].advertiser_id = '123'; + + let bidderRequest = { + bids: [{ + bidId: '3db3773286ee59', + adUnitCode: 'code' + }] + } + let result = spec.interpretResponse({ body: responseAdvertiserId }, {bidderRequest}); + expect(Object.keys(result[0].meta)).to.include.members(['advertiserId']); + }) + }); +}); diff --git a/test/spec/modules/adriverBidAdapter_spec.js b/test/spec/modules/adriverBidAdapter_spec.js new file mode 100644 index 00000000000..c16bc5df5cb --- /dev/null +++ b/test/spec/modules/adriverBidAdapter_spec.js @@ -0,0 +1,323 @@ +import { expect } from 'chai'; +import { spec } from 'modules/adriverBidAdapter.js'; +import { newBidder } from 'src/adapters/bidderFactory.js'; +import * as bidderFactory from 'src/adapters/bidderFactory.js'; +import { auctionManager } from 'src/auctionManager.js'; +const ENDPOINT = 'https://pb.adriver.ru/cgi-bin/bid.cgi'; + +describe('adriverAdapter', function () { + const adapter = newBidder(spec); + + describe('inherited functions', function () { + it('exists and is a function', function () { + expect(adapter.callBids).to.exist.and.to.be.a('function'); + }); + }); + + describe('isBidRequestValid', function () { + let bid = { + 'bidder': 'adriver', + 'params': { + 'placementId': '55:test_placement', + 'siteid': 'testSiteID' + }, + 'adUnitCode': 'adunit-code', + 'sizes': [[300, 250], [300, 600], [300, 250]], + 'bidId': '30b31c1838de1e', + 'bidderRequestId': '22edbae2733bf6', + 'auctionId': '1d1a030790a475', + }; + + it('should return true when required params found', function () { + expect(spec.isBidRequestValid(bid)).to.equal(true); + }); + }); + + describe('buildRequests', function () { + let getAdUnitsStub; + const floor = 3; + + let bidRequests = [ + { + 'bidder': 'adriver', + 'params': { + 'placementId': '55:test_placement', + 'siteid': 'testSiteID', + 'dealid': 'dealidTest' + }, + 'adUnitCode': 'adunit-code', + 'sizes': [[300, 250], [300, 600], [300, 250]], + 'bidId': '30b31c1838de1e', + 'bidderRequestId': '22edbae2733bf6', + 'auctionId': '1d1a030790a475', + 'transactionId': '04f2659e-c005-4eb1-a57c-fa93145e3843' + } + ]; + + let floorTestData = { + 'currency': 'USD', + 'floor': floor + }; + bidRequests[0].getFloor = _ => { + return floorTestData; + }; + + beforeEach(function() { + getAdUnitsStub = sinon.stub(auctionManager, 'getAdUnits').callsFake(function() { + return []; + }); + }); + + afterEach(function() { + getAdUnitsStub.restore(); + }); + + it('should exist currency', function () { + const request = spec.buildRequests(bidRequests); + const payload = JSON.parse(request.data); + + expect(payload.cur).to.exist; + }); + + it('should exist at', function () { + const request = spec.buildRequests(bidRequests); + const payload = JSON.parse(request.data); + + expect(payload.at).to.exist; + expect(payload.at).to.deep.equal(1); + }); + + it('should parse imp', function () { + const request = spec.buildRequests(bidRequests); + const payload = JSON.parse(request.data); + + expect(payload.imp[0]).to.exist; + expect(payload.imp[0].id).to.deep.equal('55:test_placement'); + + expect(payload.imp[0].ext).to.exist; + expect(payload.imp[0].ext.query).to.deep.equal('bn=15&custom=111=' + '30b31c1838de1e'); + + expect(payload.imp[0].banner).to.exist; + expect(payload.imp[0].banner.w).to.deep.equal(300); + expect(payload.imp[0].banner.h).to.deep.equal(250); + }); + + it('should parse pmp', function () { + const request = spec.buildRequests(bidRequests); + const payload = JSON.parse(request.data); + + expect(payload.imp[0].pmp).to.exist; + + expect(payload.imp[0].pmp.deals).to.exist; + + expect(payload.imp[0].pmp.deals[0].bidfloor).to.exist; + expect(payload.imp[0].pmp.deals[0].bidfloor).to.deep.equal(3); + + expect(payload.imp[0].pmp.deals[0].bidfloorcur).to.exist; + expect(payload.imp[0].pmp.deals[0].bidfloorcur).to.deep.equal('RUB'); + }); + + it('sends bid request to ENDPOINT via POST', function () { + const request = spec.buildRequests(bidRequests); + expect(request.url).to.equal(ENDPOINT); + expect(request.method).to.equal('POST'); + }); + }); + + describe('interpretResponse', function () { + let bfStub; + before(function() { + bfStub = sinon.stub(bidderFactory, 'getIabSubCategory'); + }); + + after(function() { + bfStub.restore(); + }); + + let response = { + 'id': '221594457-1615288400-1-46-', + 'bidid': 'D8JW8XU8-L5m7qFMNQGs7i1gcuPvYMEDOKsktw6e9uLy5Eebo9HftVXb0VpKj4R2dXa93i6QmRhjextJVM4y1SqodMAh5vFOb_eVkHA', + 'seatbid': [{ + 'bid': [{ + 'id': '1', + 'impid': '/19968336/header-bid-tag-0', + 'price': 4.29, + 'h': 250, + 'w': 300, + 'adid': '7121351', + 'adomain': ['http://ikea.com'], + 'nurl': 'https://ad.adriver.ru/cgi-bin/erle.cgi?expid=D8JW8XU8-L5m7qFMNQGs7i1gcuPvYMEDOKsktw6e9uLy5Eebo9HftVXb0VpKj4R2dXa93i6QmRhjextJVM4y1SqodMAh5vFOb_eVkHA&bid=7121351&wprc=4.29&tuid=-1&custom=207=/19968336/header-bid-tag-0', + 'cid': '717570', + 'ext': '2c262a7058758d' + }] + }, { + 'bid': [{ + 'id': '1', + 'impid': '/19968336/header-bid-tag-0', + 'price': 17.67, + 'h': 600, + 'w': 300, + 'adid': '7121369', + 'adomain': ['http://ikea.com'], + 'nurl': 'https://ad.adriver.ru/cgi-bin/erle.cgi?expid=DdtToXX5cpTaMMxrJSEsOsUIXt3WmC3jOvuNI5DguDrY8edFG60Jg1M-iMkVNKQ4OiAdHSLPJLQQXMUXZfI9VbjMoGCb-zzOTPiMpshI&bid=7121369&wprc=17.67&tuid=-1&custom=207=/19968336/header-bid-tag-0', + 'cid': '717570', + 'ext': '2c262a7058758d' + }] + }], + 'cur': 'RUB' + }; + + it('should get correct bid response', function () { + let expectedResponse = [ + { + requestId: '2c262a7058758d', + cpm: 4.29, + width: 300, + height: 250, + creativeId: '/19968336/header-bid-tag-0', + currency: 'RUB', + netRevenue: true, + ttl: 3000, + meta: { + advertiserDomains: ['http://ikea.com'] + }, + ad: '' + } + ]; + let bidderRequest = { + bids: [{ + bidId: '3db3773286ee59', + adUnitCode: 'code' + }] + }; + let result = spec.interpretResponse({ body: response }, {bidderRequest}); + expect(Object.keys(result[0])).to.have.members(Object.keys(expectedResponse[0])); + }); + + it('handles nobid responses', function () { + let response = { + 'version': '0.0.1', + 'tags': [{ + 'uuid': '84ab500420319d', + 'tag_id': 5976557, + 'auction_id': '297492697822162468', + 'nobid': true + }] + }; + let bidderRequest; + + let result = spec.interpretResponse({ body: response }, {bidderRequest}); + expect(result.length).to.equal(0); + }); + }); + + describe('function _getFloor', function () { + let bidRequests = [ + { + bidder: 'adriver', + params: { + placementId: '55:test_placement', + siteid: 'testSiteID', + dealid: 'dealidTest', + }, + adUnitCode: 'adunit-code', + sizes: [[300, 250], [300, 600], [300, 250]], + bidId: '30b31c1838de1e', + bidderRequestId: '22edbae2733bf6', + auctionId: '1d1a030790a475', + transactionId: '04f2659e-c005-4eb1-a57c-fa93145e3843' + } + ]; + + const floorTestData = { + 'currency': 'RUB', + 'floor': 1.50 + }; + + const bitRequestStandard = JSON.parse(JSON.stringify(bidRequests)); + + bitRequestStandard[0].getFloor = () => { + return floorTestData; + }; + + it('valid BidRequests', function () { + const request = spec.buildRequests(bitRequestStandard); + const payload = JSON.parse(request.data); + + expect(typeof bitRequestStandard[0].getFloor).to.equal('function'); + expect(payload.imp[0].bidfloor).to.equal(1.50); + expect(payload.imp[0].bidfloorcur).to.equal('RUB'); + }); + + const bitRequestEmptyCurrency = JSON.parse(JSON.stringify(bidRequests)); + + const floorTestDataEmptyCurrency = { + 'currency': 'RUB', + 'floor': 1.50 + }; + + bitRequestEmptyCurrency[0].getFloor = () => { + return floorTestDataEmptyCurrency; + }; + + it('empty currency', function () { + const request = spec.buildRequests(bitRequestEmptyCurrency); + const payload = JSON.parse(request.data); + + expect(payload.imp[0].bidfloor).to.equal(1.50); + expect(payload.imp[0].bidfloorcur).to.equal('RUB'); + }); + + const bitRequestFloorNull = JSON.parse(JSON.stringify(bidRequests)); + + const floorTestDataFloorNull = { + 'currency': '', + 'floor': null + }; + + bitRequestFloorNull[0].getFloor = () => { + return floorTestDataFloorNull; + }; + + it('empty floor', function () { + const request = spec.buildRequests(bitRequestFloorNull); + const payload = JSON.parse(request.data); + + expect(payload.imp[0].bidfloor).to.equal(0); + }); + + const bitRequestGetFloorNotFunction = JSON.parse(JSON.stringify(bidRequests)); + + bitRequestGetFloorNotFunction[0].getFloor = 0; + + it('bid.getFloor is not a function', function () { + const request = spec.buildRequests(bitRequestGetFloorNotFunction); + const payload = JSON.parse(request.data); + + expect(payload.imp[0].bidfloor).to.equal(0); + expect(payload.imp[0].bidfloorcur).to.equal('RUB'); + }); + + const bitRequestGetFloorBySized = JSON.parse(JSON.stringify(bidRequests)); + + bitRequestGetFloorBySized[0].getFloor = (requestParams = {currency: 'USD', mediaType: '*', size: '*'}) => { + if (requestParams.size.length === 2 && requestParams.size[0] === 300 && requestParams.size[1] === 250) { + return { + 'currency': 'RUB', + 'floor': 3.33 + } + } else { + return {} + } + }; + + it('bid.getFloor get size', function () { + const request = spec.buildRequests(bitRequestGetFloorBySized); + const payload = JSON.parse(request.data); + + expect(payload.imp[0].bidfloor).to.equal(3.33); + expect(payload.imp[0].bidfloorcur).to.equal('RUB'); + expect(payload.imp[0].bidfloorcur).to.equal('RUB'); + }); + }); +}); diff --git a/test/spec/modules/adtelligentBidAdapter_spec.js b/test/spec/modules/adtelligentBidAdapter_spec.js index 9c694668703..e6916997133 100644 --- a/test/spec/modules/adtelligentBidAdapter_spec.js +++ b/test/spec/modules/adtelligentBidAdapter_spec.js @@ -2,6 +2,7 @@ import { expect } from 'chai'; import { spec } from 'modules/adtelligentBidAdapter.js'; import { newBidder } from 'src/adapters/bidderFactory.js'; import { config } from 'src/config.js'; +import { deepClone } from 'src/utils.js'; const EXPECTED_ENDPOINTS = [ 'https://ghb.adtelligent.com/v2/auction/', @@ -9,7 +10,16 @@ const EXPECTED_ENDPOINTS = [ 'https://ghb2.adtelligent.com/v2/auction/', 'https://ghb.adtelligent.com/v2/auction/' ]; +const aliasEP = { + appaloosa: 'https://ghb.hb.appaloosa.media/v2/auction/', + appaloosa_publisherSuffix: 'https://ghb.hb.appaloosa.media/v2/auction/', + onefiftytwomedia: 'https://ghb.ads.152media.com/v2/auction/', + mediafuse: 'https://ghb.hbmp.mediafuse.com/v2/auction/', + navelix: 'https://ghb.hb.navelix.com/v2/auction/', + bidsxchange: 'https://ghb.hbd.bidsxchange.com/v2/auction/', +}; +const DEFAULT_ADATPER_REQ = { bidderCode: 'adtelligent' }; const DISPLAY_REQUEST = { 'bidder': 'adtelligent', 'params': { @@ -79,6 +89,7 @@ const SERVER_DISPLAY_RESPONSE = { 'source': { 'aid': 12345, 'pubId': 54321 }, 'bids': [{ 'ad': '', + 'adUrl': 'adUrl', 'requestId': '2e41f65424c87c', 'creative_id': 342516, 'cmpId': 342516, @@ -162,6 +173,7 @@ const displayEqResponse = [{ netRevenue: true, currency: 'USD', ad: '', + adUrl: 'adUrl', height: 250, width: 300, ttl: 300, @@ -241,15 +253,24 @@ 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); }) + it('makes correct host for aliases', () => { + for (const alias in aliasEP) { + const bidReq = deepClone(DISPLAY_REQUEST) + bidReq.bidder = alias; + const [bidderRequest] = spec.buildRequests([bidReq], { bidderCode: alias }); + expect(bidderRequest.url).to.equal(aliasEP[alias]); + } + }) + it('building requests as arrays', () => { expect(videoRequest).to.be.a('array'); expect(displayRequest).to.be.a('array'); @@ -264,7 +285,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'); @@ -277,7 +298,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); }); @@ -289,7 +311,8 @@ describe('adtelligentBidAdapter', () => { CallbackId: '84ab500420319d', AdType: 'display', Aid: 12345, - Sizes: '300x250' + Sizes: '300x250', + PlacementId: 'adunit-code' }; expect(data.BidRequests[0]).to.deep.equal(eq); @@ -301,12 +324,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..8e1c872d460 --- /dev/null +++ b/test/spec/modules/adtrueBidAdapter_spec.js @@ -0,0 +1,431 @@ +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; + let bidResponses2; + 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', + ext: { + cookie_sync: [ + { + type: 1, + url: 'https://hb.adtrue.com/prebid/usersync?bidder=adtrue' + } + ] + } + } + }; + bidResponses2 = { + 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', + ext: { + cookie_sync: [ + { + type: 2, + url: 'https://hb.adtrue.com/prebid/usersync?bidder=adtrue&type=image' + }, + { + type: 1, + url: 'https://hb.adtrue.com/prebid/usersync?bidder=appnexus' + } + ] + } + } + }; + }); + + 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 sandbox; + beforeEach(function () { + sandbox = sinon.sandbox.create(); + }); + afterEach(function () { + sandbox.restore(); + }); + it('execute as per config', function () { + expect(spec.getUserSyncs({iframeEnabled: true}, [bidResponses], undefined, undefined)).to.deep.equal([{ + type: 'iframe', + url: 'https://hb.adtrue.com/prebid/usersync?bidder=adtrue&publisherId=1212&zoneId=21423&gdpr=0&gdpr_consent=&us_privacy=&coppa=0' + }]); + }); + // Multiple user sync output + it('execute as per config', function () { + expect(spec.getUserSyncs({iframeEnabled: true}, [bidResponses2], undefined, undefined)).to.deep.equal([ + { + type: 'image', + url: 'https://hb.adtrue.com/prebid/usersync?bidder=adtrue&type=image&publisherId=1212&zoneId=21423&gdpr=0&gdpr_consent=&us_privacy=&coppa=0' + }, + { + type: 'iframe', + url: 'https://hb.adtrue.com/prebid/usersync?bidder=appnexus&publisherId=1212&zoneId=21423&gdpr=0&gdpr_consent=&us_privacy=&coppa=0' + } + ]); + }); + }); +}); diff --git a/test/spec/modules/adxcgBidAdapter_spec.js b/test/spec/modules/adxcgBidAdapter_spec.js index 306914960c3..551d50b60e7 100644 --- a/test/spec/modules/adxcgBidAdapter_spec.js +++ b/test/spec/modules/adxcgBidAdapter_spec.js @@ -1,6 +1,7 @@ import {expect} from 'chai'; import {spec} from 'modules/adxcgBidAdapter.js'; import {deepClone, parseUrl} from 'src/utils.js'; +import * as utils from '../../../src/utils.js'; describe('AdxcgAdapter', function () { let bidBanner = { @@ -29,15 +30,15 @@ describe('AdxcgAdapter', function () { adzoneid: '20', video: { api: [2], - protocols: [1, 2], - mimes: ['video/mp4'], maxduration: 30 } }, mediaTypes: { video: { context: 'instream', - playerSize: [[640, 480]] + playerSize: [[640, 480]], + protocols: [1, 2], + mimes: ['video/mp4'], } }, adUnitCode: 'adunit-code', @@ -91,7 +92,7 @@ describe('AdxcgAdapter', function () { expect(spec.isBidRequestValid(bidBanner)).to.equal(true); }); - it('should return true when required params not found', function () { + it('should return false when required params not found', function () { expect(spec.isBidRequestValid({})).to.be.false; }); @@ -106,10 +107,6 @@ describe('AdxcgAdapter', function () { const simpleVideo = JSON.parse(JSON.stringify(bidVideo)); simpleVideo.params.adzoneid = 123; expect(spec.isBidRequestValid(simpleVideo)).to.be.false; - simpleVideo.params.mimes = [1, 2, 3]; - expect(spec.isBidRequestValid(simpleVideo)).to.be.false; - simpleVideo.params.mimes = 'bad type'; - expect(spec.isBidRequestValid(simpleVideo)).to.be.false; }); }); @@ -124,7 +121,7 @@ describe('AdxcgAdapter', function () { let query = parsedRequestUrl.search; expect(query.renderformat).to.equal('javascript'); - expect(query.ver).to.equal('r20191128PB30'); + expect(query.ver).to.equal('r20210330PB40'); expect(query.source).to.equal('pbjs10'); expect(query.pbjs).to.equal('$prebid.version$'); expect(query.adzoneid).to.equal('1'); @@ -158,7 +155,7 @@ describe('AdxcgAdapter', function () { let query = parsedRequestUrl.search; // general part expect(query.renderformat).to.equal('javascript'); - expect(query.ver).to.equal('r20191128PB30'); + expect(query.ver).to.equal('r20210330PB40'); expect(query.source).to.equal('pbjs10'); expect(query.pbjs).to.equal('$prebid.version$'); expect(query.adzoneid).to.equal('20'); @@ -196,7 +193,7 @@ describe('AdxcgAdapter', function () { let query = parsedRequestUrl.search; expect(query.renderformat).to.equal('javascript'); - expect(query.ver).to.equal('r20191128PB30'); + expect(query.ver).to.equal('r20210330PB40'); expect(query.source).to.equal('pbjs10'); expect(query.pbjs).to.equal('$prebid.version$'); expect(query.adzoneid).to.equal('2379'); @@ -281,7 +278,7 @@ describe('AdxcgAdapter', function () { let bid = deepClone([bidBanner]); let bidderRequests = {}; - bid[0].userId = {id5id: 'id5idsample'}; + bid[0].userId = {id5id: {uid: 'id5idsample'}}; it('should send pubcid if available', function () { let request = spec.buildRequests(bid, bidderRequests); @@ -327,123 +324,164 @@ describe('AdxcgAdapter', function () { }; let BANNER_RESPONSE = { - body: [ - { - bidId: '84ab500420319d', - bidderCode: 'adxcg', - width: 300, - height: 250, - creativeId: '42', - cpm: 0.45, - currency: 'USD', - netRevenue: true, - ad: '' - } - ], - header: {someheader: 'fakedata'} + body: { + id: 'auctionid', + bidid: '84ab500420319d', + seatbid: [{ + bid: [ + { + impid: '84ab500420319d', + price: 0.45, + crid: '42', + adm: '', + w: 300, + h: 250, + adomain: ['adomain.com'], + cat: ['IAB1-4', 'IAB8-16', 'IAB25-5'], + ext: { + crType: 'banner', + advertiser_id: '777', + advertiser_name: 'advertiser', + agency_name: 'agency' + } + } + ] + }], + cur: 'USD' + }, + headers: {someheader: 'fakedata'} }; let BANNER_RESPONSE_WITHDEALID = { - body: [ - { - bidId: '84ab500420319d', - bidderCode: 'adxcg', - width: 300, - height: 250, - deal_id: '7722', - creativeId: '42', - cpm: 0.45, - currency: 'USD', - netRevenue: true, - ad: '' - } - ], - header: {someheader: 'fakedata'} + body: { + id: 'auctionid', + bidid: '84ab500420319d', + seatbid: [{ + bid: [ + { + impid: '84ab500420319d', + price: 0.45, + crid: '42', + dealid: '7722', + adm: '', + w: 300, + h: 250, + adomain: ['adomain.com'], + ext: { + crType: 'banner' + } + } + ] + }], + cur: 'USD' + } }; let VIDEO_RESPONSE = { - body: [ - { - bidId: '84ab500420319d', - bidderCode: 'adxcg', - width: 640, - height: 360, - creativeId: '42', - cpm: 0.45, - currency: 'USD', - netRevenue: true, - vastUrl: 'vastContentUrl' - } - ], - header: {someheader: 'fakedata'} + body: { + id: 'auctionid', + bidid: '84ab500420319d', + seatbid: [{ + bid: [ + { + impid: '84ab500420319d', + price: 0.45, + crid: '42', + nurl: 'vastContentUrl', + adomain: ['adomain.com'], + w: 640, + h: 360, + ext: { + crType: 'video' + } + } + ] + }], + cur: 'USD' + }, + headers: {someheader: 'fakedata'} }; - let NATIVE_RESPONSE = { - body: [ + let NATIVE_RESPONSEob = { + assets: [ { - bidId: '84ab500420319d', - bidderCode: 'adxcg', - width: 0, - height: 0, - creativeId: '42', - cpm: 0.45, - currency: 'USD', - netRevenue: true, - nativeResponse: { - assets: [ - { - id: 1, - required: 0, - title: { - text: 'titleContent' - } - }, - { - id: 2, - required: 0, - img: { - url: 'imageContent', - w: 600, - h: 600 - } - }, - { - id: 3, - required: 0, - data: { - label: 'DESC', - value: 'descriptionContent' - } - }, - { - id: 0, - required: 0, - data: { - label: 'SPONSORED', - value: 'sponsoredByContent' - } - }, - { - id: 5, - required: 0, - icon: { - url: 'iconContent', - w: 400, - h: 400 - } - } - ], - link: { - url: 'linkContent' - }, - imptrackers: ['impressionTracker1', 'impressionTracker2'] + id: 1, + required: 0, + title: { + text: 'titleContent' + } + }, + { + id: 2, + required: 0, + img: { + url: 'imageContent', + w: 600, + h: 600 + } + }, + { + id: 3, + required: 0, + data: { + label: 'DESC', + value: 'descriptionContent' + } + }, + { + id: 0, + required: 0, + data: { + label: 'SPONSORED', + value: 'sponsoredByContent' + } + }, + { + id: 5, + required: 0, + icon: { + url: 'iconContent', + w: 400, + h: 400 } } ], - header: {someheader: 'fakedata'} + link: { + url: 'linkContent' + }, + imptrackers: ['impressionTracker1', 'impressionTracker2'] + } + + let NATIVE_RESPONSE = { + body: { + id: 'auctionid', + bidid: '84ab500420319d', + seatbid: [{ + bid: [ + { + impid: '84ab500420319d', + price: 0.45, + crid: '42', + w: 0, + h: 0, + adm: JSON.stringify(NATIVE_RESPONSEob), + adomain: ['adomain.com'], + ext: { + crType: 'native' + } + } + ] + }], + cur: 'USD' + }, + headers: {someheader: 'fakedata'} }; it('handles regular responses', function () { + expect(BANNER_RESPONSE).to.exist; + expect(BANNER_RESPONSE.body).to.exist; + expect(BANNER_RESPONSE.body.id).to.exist; + expect(BANNER_RESPONSE.body.seatbid[0]).to.exist; let result = spec.interpretResponse(BANNER_RESPONSE, BIDDER_REQUEST); expect(result).to.have.lengthOf(1); @@ -452,26 +490,30 @@ describe('AdxcgAdapter', function () { expect(result[0].width).to.equal(300); expect(result[0].height).to.equal(250); expect(result[0].creativeId).to.equal(42); - expect(result[0].cpm).to.equal(0.45); + expect(result[0].cpm).to.be.within(0.45, 0.46); expect(result[0].ad).to.equal(''); expect(result[0].currency).to.equal('USD'); expect(result[0].netRevenue).to.equal(true); expect(result[0].ttl).to.equal(300); expect(result[0].dealId).to.not.exist; + expect(result[0].meta.advertiserDomains[0]).to.equal('adomain.com'); + expect(result[0].meta.advertiserId).to.be.eql('777'); + expect(result[0].meta.advertiserName).to.be.eql('advertiser'); + expect(result[0].meta.agencyName).to.be.eql('agency'); + expect(result[0].meta.advertiserDomains).to.be.eql(['adomain.com']); + expect(result[0].meta.secondaryCatIds).to.be.eql(['IAB1-4', 'IAB8-16', 'IAB25-5']); }); it('handles regular responses with dealid', function () { - let result = spec.interpretResponse( - BANNER_RESPONSE_WITHDEALID, - BIDDER_REQUEST - ); + let result = spec.interpretResponse(BANNER_RESPONSE_WITHDEALID); expect(result).to.have.lengthOf(1); expect(result[0].width).to.equal(300); expect(result[0].height).to.equal(250); expect(result[0].creativeId).to.equal(42); - expect(result[0].cpm).to.equal(0.45); + // expect(result[0].cpm).to.equal(0.45); + expect(result[0].cpm).to.be.within(0.45, 0.46); expect(result[0].ad).to.equal(''); expect(result[0].currency).to.equal('USD'); expect(result[0].netRevenue).to.equal(true); @@ -479,7 +521,7 @@ describe('AdxcgAdapter', function () { }); it('handles video responses', function () { - let result = spec.interpretResponse(VIDEO_RESPONSE, BIDDER_REQUEST); + let result = spec.interpretResponse(VIDEO_RESPONSE); expect(result).to.have.lengthOf(1); expect(result[0].width).to.equal(640); @@ -494,17 +536,19 @@ describe('AdxcgAdapter', function () { }); it('handles native responses', function () { - let result = spec.interpretResponse(NATIVE_RESPONSE, BIDDER_REQUEST); + let result = spec.interpretResponse(NATIVE_RESPONSE); expect(result[0].width).to.equal(0); expect(result[0].height).to.equal(0); - expect(result[0].mediaType).to.equal('native'); + expect(result[0].creativeId).to.equal(42); expect(result[0].cpm).to.equal(0.45); expect(result[0].currency).to.equal('USD'); expect(result[0].netRevenue).to.equal(true); expect(result[0].ttl).to.equal(300); + expect(result[0].mediaType).to.equal('native'); + expect(result[0].native.clickUrl).to.equal('linkContent'); expect(result[0].native.impressionTrackers).to.deep.equal([ 'impressionTracker1', @@ -545,4 +589,65 @@ describe('AdxcgAdapter', function () { ); }); }); + + describe('on bidWon', function () { + beforeEach(function () { + sinon.stub(utils, 'triggerPixel'); + }); + afterEach(function () { + utils.triggerPixel.restore(); + }); + it('should replace burl for banner', function () { + const burl = 'burl=${' + 'AUCTION_PRICE}'; + const bid = { + 'bidderCode': 'adxcg', + 'width': 0, + 'height': 0, + 'statusMessage': 'Bid available', + 'adId': '3d0b6ff1dda89', + 'requestId': '2a423489e058a1', + 'mediaType': 'banner', + 'source': 'client', + 'ad': burl, + 'cpm': 0.66, + 'creativeId': '353538_591471', + 'currency': 'USD', + 'dealId': '', + 'netRevenue': true, + 'ttl': 300, + // 'nurl': nurl, + 'burl': burl, + 'isBurl': true, + 'auctionId': 'a92bffce-14d2-4f8f-a78a-7b9b5e4d28fa', + 'responseTimestamp': 1556867386065, + 'requestTimestamp': 1556867385916, + 'bidder': 'adxcg', + 'adUnitCode': 'div-gpt-ad-1555415275793-0', + 'timeToRespond': 149, + 'pbLg': '0.50', + 'pbMg': '0.60', + 'pbHg': '0.66', + 'pbAg': '0.65', + 'pbDg': '0.66', + 'pbCg': '', + 'size': '0x0', + 'adserverTargeting': { + 'hb_bidder': 'mgid', + 'hb_adid': '3d0b6ff1dda89', + 'hb_pb': '0.66', + 'hb_size': '0x0', + 'hb_source': 'client', + 'hb_format': 'banner', + 'hb_banner_title': 'TITLE', + 'hb_banner_image': 'hb_banner_image:3d0b6ff1dda89', + 'hb_banner_icon': 'IconURL', + 'hb_banner_linkurl': 'hb_banner_linkurl:3d0b6ff1dda89' + }, + 'status': 'targetingSet', + 'params': [{'adzoneid': '20'}] + }; + spec.onBidWon(bid); + expect(bid.burl).to.deep.equal(burl); + }); + }); }); diff --git a/test/spec/modules/adyoulikeBidAdapter_spec.js b/test/spec/modules/adyoulikeBidAdapter_spec.js index d2d4e10c17f..c432ad1b32d 100644 --- a/test/spec/modules/adyoulikeBidAdapter_spec.js +++ b/test/spec/modules/adyoulikeBidAdapter_spec.js @@ -58,12 +58,137 @@ 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 bidRequestWithNativeImageType = [ + { + 'bidId': 'bid_id_0', + 'bidder': 'adyoulike', + 'placementCode': 'adunit/hb-0', + 'params': { + 'placement': 'placement_0' + }, + 'sizes': '300x250', + 'mediaTypes': + { + 'native': { + 'type': 'image', + 'additional': { + 'will': 'be', + 'sent': ['300x250'] + } + }, + }, + '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 sentNativeImageType = { + 'additional': { + 'sent': [ + '300x250' + ], + 'will': 'be' + }, + 'body': { + 'required': false + }, + 'clickUrl': { + 'required': true + }, + 'cta': { + 'required': false + }, + 'icon': { + 'required': false + }, + 'image': { + 'required': true + }, + 'sponsoredBy': { + 'required': true + }, + 'title': { + 'required': true + }, + 'type': 'image' + }; + const bidRequestWithDCPlacement = [ { 'bidId': 'bid_id_0', @@ -165,11 +290,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 +340,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 +376,7 @@ describe('Adyoulike Adapter', function () { bid.params = { 'placement': 0 }; - expect(spec.isBidRequestValid(bid)).to.equal(false); + expect(!!spec.isBidRequestValid(bid)).to.equal(false); }); }); @@ -250,6 +395,23 @@ describe('Adyoulike Adapter', function () { canonicalQuery.restore(); }); + it('Should expand short native image config type', function() { + const request = spec.buildRequests(bidRequestWithNativeImageType, bidderRequest); + const payload = JSON.parse(request.data); + + expect(request.url).to.contain(getEndpoint()); + expect(request.method).to.equal('POST'); + expect(request.url).to.contains('CanonicalUrl=' + encodeURIComponent(canonicalUrl)); + expect(request.url).to.contains('RefererUrl=' + encodeURIComponent(referrerUrl)); + expect(request.url).to.contains('PublisherDomain=http%3A%2F%2Flocalhost%3A9876'); + + expect(payload.Version).to.equal('1.0'); + expect(payload.Bids['bid_id_0'].PlacementID).to.be.equal('placement_0'); + expect(payload.PageRefreshed).to.equal(false); + expect(payload.Bids['bid_id_0'].TransactionID).to.be.equal('bid_id_0_transaction_id'); + expect(payload.Bids['bid_id_0'].Native).deep.equal(sentNativeImageType); + }); + it('should add gdpr/usp consent information to the request', function () { let consentString = 'BOJ8RZsOJ8RZsABAB8AAAAAZ+A=='; let uspConsentData = '1YCC'; @@ -306,6 +468,7 @@ describe('Adyoulike Adapter', function () { expect(request.method).to.equal('POST'); expect(request.url).to.contains('CanonicalUrl=' + encodeURIComponent(canonicalUrl)); expect(request.url).to.contains('RefererUrl=' + encodeURIComponent(referrerUrl)); + expect(request.url).to.contains('PublisherDomain=http%3A%2F%2Flocalhost%3A9876'); expect(payload.Version).to.equal('1.0'); expect(payload.Bids['bid_id_0'].PlacementID).to.be.equal('placement_0'); @@ -383,7 +546,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); }); @@ -404,5 +567,48 @@ 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', + 'https://tracking.omnitagjs.com/tracking/pixel?event_kind=INSERTION&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/amxBidAdapter_spec.js b/test/spec/modules/amxBidAdapter_spec.js index 790f3bf2581..f502d631c17 100644 --- a/test/spec/modules/amxBidAdapter_spec.js +++ b/test/spec/modules/amxBidAdapter_spec.js @@ -1,7 +1,9 @@ import * as utils from 'src/utils.js'; +import { createEidsArray } from 'modules/userId/eids.js'; import { expect } from 'chai'; import { spec } from 'modules/amxBidAdapter.js'; import { BANNER, VIDEO } from 'src/mediaTypes.js'; +import { config } from 'src/config.js'; const sampleRequestId = '82c91e127a9b93e'; const sampleDisplayAd = (additionalImpressions) => `${additionalImpressions}`; @@ -14,6 +16,30 @@ const sampleVideoAd = (addlImpression) => ` const embeddedTrackingPixel = `https://1x1.a-mo.net/hbx/g_impression?A=sample&B=20903`; const sampleNurl = 'https://example.exchange/nurl'; +const sampleFPD = { + site: { + keywords: 'sample keywords', + ext: { + data: { + pageType: 'article' + } + } + }, + user: { + gender: 'O', + yob: 1982, + } +}; + +const stubConfig = (withStub) => { + const stub = sinon.stub(config, 'getConfig').callsFake( + (arg) => arg === 'ortb2' ? sampleFPD : null + ) + + withStub(); + stub.restore(); +}; + const sampleBidderRequest = { gdprConsent: { gdprApplies: true, @@ -34,6 +60,15 @@ const sampleBidRequestBase = { endpoint: 'https://httpbin.org/post', }, sizes: [[320, 50]], + getFloor(params) { + if (params.size == null || params.currency == null || params.mediaType == null) { + throw new Error(`getFloor called with incomplete params: ${JSON.stringify(params)}`) + } + return { + floor: 0.5, + currency: 'USD' + } + }, mediaTypes: { [BANNER]: { sizes: [[300, 250]] @@ -45,13 +80,28 @@ const sampleBidRequestBase = { auctionId: utils.getUniqueIdentifierStr(), }; +const schainConfig = { + ver: '1.0', + nodes: [{ + asi: 'greatnetwork.exchange', + sid: '000001', + hp: 1, + rid: 'bid_request_1', + domain: 'publisher.com' + }] +}; + const sampleBidRequestVideo = { ...sampleBidRequestBase, bidId: sampleRequestId + '_video', sizes: [[300, 150]], + schain: schainConfig, mediaTypes: { [VIDEO]: { - sizes: [[360, 250]] + sizes: [[360, 250]], + context: 'adpod', + adPodDurationSec: 90, + contentMode: 'live' } } }; @@ -80,6 +130,7 @@ const sampleServerResponse = { 'h': 600, 'id': '2014691335735134254', 'impid': '1', + 'exp': 90, 'price': 0.25, 'w': 300 }, @@ -99,6 +150,7 @@ const sampleServerResponse = { 'h': 1, 'id': '7735706981389902829', 'impid': '1', + 'exp': 90, 'price': 0.25, 'w': 1 }, @@ -120,8 +172,11 @@ describe('AmxBidAdapter', () => { expect(spec.isBidRequestValid({params: { tagId: 'test' }})).to.equal(true) }); - it('testMode is an optional boolean', () => { - expect(spec.isBidRequestValid({params: { testMode: 1 }})).to.equal(false) + it('testMode is an optional truthy value', () => { + expect(spec.isBidRequestValid({params: { testMode: 1 }})).to.equal(true) + expect(spec.isBidRequestValid({params: { testMode: 'true' }})).to.equal(true) + // ignore invalid values (falsy) + expect(spec.isBidRequestValid({params: { testMode: 'non-truthy-invalid-value' }})).to.equal(true) expect(spec.isBidRequestValid({params: { testMode: false }})).to.equal(true) }); @@ -155,6 +210,17 @@ describe('AmxBidAdapter', () => { expect(url).to.equal('https://prebid.a-mo.net/a/c') }); + it('will read the prebid version & global', () => { + const { data: { V: prebidVersion, vg: prebidGlobal } } = spec.buildRequests([{ + ...sampleBidRequestBase, + params: { + testMode: true + } + }], sampleBidderRequest); + expect(prebidVersion).to.equal('$prebid.version$') + expect(prebidGlobal).to.equal('$$PREBID_GLOBAL$$') + }); + it('reads test mode from the first bid request', () => { const { data } = spec.buildRequests([{ ...sampleBidRequestBase, @@ -165,12 +231,98 @@ describe('AmxBidAdapter', () => { expect(data.tm).to.equal(true); }); - it('handles referer data and GDPR, USP Consent', () => { + it('if prebid is in an iframe, will use the frame url as domain, if the topmost is not avialable', () => { + const { data } = spec.buildRequests([sampleBidRequestBase], { + ...sampleBidderRequest, + refererInfo: { + numIframes: 1, + referer: 'http://search-traffic-source.com', + stack: [] + } + }); + expect(data.do).to.equal('localhost') + expect(data.re).to.equal('http://search-traffic-source.com'); + }); + + it('if we are in AMP, make sure we use the canonical URL or the referrer (which is sourceUrl)', () => { + const { data } = spec.buildRequests([sampleBidRequestBase], { + ...sampleBidderRequest, + refererInfo: { + isAmp: true, + referer: 'http://real-publisher-site.com/content', + stack: [] + } + }); + expect(data.do).to.equal('real-publisher-site.com') + expect(data.re).to.equal('http://real-publisher-site.com/content'); + }) + + it('if prebid is in an iframe, will use the topmost url as domain', () => { + const { data } = spec.buildRequests([sampleBidRequestBase], { + ...sampleBidderRequest, + refererInfo: { + numIframes: 1, + referer: 'http://search-traffic-source.com', + stack: ['http://top-site.com', 'http://iframe.com'] + } + }); + expect(data.do).to.equal('top-site.com'); + expect(data.re).to.equal('http://search-traffic-source.com'); + }); + + it('handles referer data and GDPR, USP Consent, COPPA', () => { const { data } = spec.buildRequests([sampleBidRequestBase], sampleBidderRequest); delete data.m; // don't deal with "m" in this test expect(data.gs).to.equal(sampleBidderRequest.gdprConsent.gdprApplies) expect(data.gc).to.equal(sampleBidderRequest.gdprConsent.consentString) expect(data.usp).to.equal(sampleBidderRequest.uspConsent) + expect(data.cpp).to.equal(0) + }); + + it('will forward bid request count & wins count data', () => { + const bidderRequestsCount = Math.floor(Math.random() * 100) + const bidderWinsCount = Math.floor(Math.random() * 100) + const { data } = spec.buildRequests([{ + ...sampleBidRequestBase, + bidderRequestsCount, + bidderWinsCount + }], sampleBidderRequest); + + expect(data.brc).to.equal(bidderRequestsCount) + expect(data.bwc).to.equal(bidderWinsCount) + expect(data.trc).to.equal(0) + }); + it('will forward first-party data', () => { + stubConfig(() => { + const { data } = spec.buildRequests([sampleBidRequestBase], sampleBidderRequest); + expect(data.fpd2).to.deep.equal(sampleFPD) + }); + }); + + it('will collect & forward RTI user IDs', () => { + const randomRTI = `greatRTI${Math.floor(Math.random() * 100)}` + const userId = { + britepoolid: 'sample-britepool', + criteoId: 'sample-criteo', + digitrustid: {data: {id: 'sample-digitrust'}}, + id5id: {uid: 'sample-id5'}, + idl_env: 'sample-liveramp', + lipb: {lipbid: 'sample-liveintent'}, + netId: 'sample-netid', + parrableId: { eid: 'sample-parrable' }, + pubcid: 'sample-pubcid', + [randomRTI]: 'sample-unknown', + tdid: 'sample-ttd', + }; + + const eids = createEidsArray(userId); + const bid = { + ...sampleBidRequestBase, + userIdAsEids: eids + }; + + const { data } = spec.buildRequests([bid, bid], sampleBidderRequest); + expect(data.eids).to.deep.equal(eids) }); it('can build a banner request', () => { @@ -188,17 +340,35 @@ describe('AmxBidAdapter', () => { expect(Object.keys(data.m).length).to.equal(2); expect(data.m[sampleRequestId]).to.deep.equal({ av: true, + au: 'div-gpt-ad-example', + vd: {}, + ms: [ + [[320, 50]], + [[300, 250]], + [] + ], aw: 300, + sc: {}, ah: 250, tf: 0, + f: 0.5, vr: false }); expect(data.m[sampleRequestId + '_2']).to.deep.equal({ av: true, aw: 300, + au: 'div-gpt-ad-example', + sc: {}, + ms: [ + [[320, 50]], + [[300, 250]], + [] + ], i: 'example', ah: 250, + vd: {}, tf: 0, + f: 0.5, vr: false, }); }); @@ -207,10 +377,24 @@ describe('AmxBidAdapter', () => { const { data } = spec.buildRequests([sampleBidRequestVideo], sampleBidderRequest); expect(Object.keys(data.m).length).to.equal(1); expect(data.m[sampleRequestId + '_video']).to.deep.equal({ + au: 'div-gpt-ad-example', + ms: [ + [[300, 150]], + [], + [[360, 250]] + ], av: true, aw: 360, ah: 250, + sc: schainConfig, + vd: { + sizes: [[360, 250]], + context: 'adpod', + adPodDurationSec: 90, + contentMode: 'live' + }, tf: 0, + f: 0.5, vr: true }); }); @@ -255,9 +439,10 @@ describe('AmxBidAdapter', () => { ...baseBidResponse.meta, mediaType: BANNER, }, + mediaType: BANNER, width: 300, height: 600, // from the bid itself - ttl: 70, + ttl: 90, ad: sampleDisplayAd( `` + `` @@ -268,20 +453,14 @@ describe('AmxBidAdapter', () => { it('can parse a video ad', () => { const parsed = spec.interpretResponse({ body: sampleServerResponse }, baseRequest) expect(parsed.length).to.equal(2) - - // we should have display, video, display - const xml = parsed[1].vastXml - delete parsed[1].vastXml - - expect(xml).to.have.string(``) - expect(xml).to.have.string(``) - expect(parsed[1]).to.deep.equal({ ...baseBidResponse, meta: { ...baseBidResponse.meta, mediaType: VIDEO, }, + mediaType: VIDEO, + vastXml: sampleVideoAd(''), width: 300, height: 250, ttl: 90, diff --git a/test/spec/modules/aniviewBidAdapter_spec.js b/test/spec/modules/aniviewBidAdapter_spec.js index cca175c3388..2e1fdb56201 100644 --- a/test/spec/modules/aniviewBidAdapter_spec.js +++ b/test/spec/modules/aniviewBidAdapter_spec.js @@ -177,6 +177,33 @@ describe('ANIVIEW Bid Adapter Test', function () { let result = spec.interpretResponse(nobidResponse, bidRequest); expect(result.length).to.equal(0); }); + + it('should add renderer if outstream context', function () { + const bidRequest = spec.buildRequests([ + { + bidId: '253dcb69fb2577', + params: { + playerDomain: 'example.com', + AV_PUBLISHERID: '55b78633181f4603178b4568', + AV_CHANNELID: '55b7904d181f46410f8b4568' + }, + mediaTypes: { + video: { + playerSize: [[640, 480]], + context: 'outstream' + } + } + } + ])[0] + const bidResponse = spec.interpretResponse(serverResponse, bidRequest)[0] + + expect(bidResponse.renderer.url).to.equal('https://example.com/script/6.1/prebidRenderer.js') + expect(bidResponse.renderer.config.AV_PUBLISHERID).to.equal('55b78633181f4603178b4568') + expect(bidResponse.renderer.config.AV_CHANNELID).to.equal('55b7904d181f46410f8b4568') + expect(bidResponse.renderer.loaded).to.equal(false) + expect(bidResponse.width).to.equal(640) + expect(bidResponse.height).to.equal(480) + }) }); describe('getUserSyncs', function () { diff --git a/test/spec/modules/aolBidAdapter_spec.js b/test/spec/modules/aolBidAdapter_spec.js index dd10a57bbfe..50622180ad0 100644 --- a/test/spec/modules/aolBidAdapter_spec.js +++ b/test/spec/modules/aolBidAdapter_spec.js @@ -1,7 +1,7 @@ import {expect} from 'chai'; import * as utils from 'src/utils.js'; import {spec} from 'modules/aolBidAdapter.js'; -import {config} from 'src/config.js'; +import {createEidsArray} from '../../../modules/userId/eids.js'; const DEFAULT_AD_CONTENT = ''; @@ -80,6 +80,33 @@ describe('AolAdapter', function () { const NEXAGE_URL = 'https://c2shb.ssp.yahoo.com/bidRequest?'; const ONE_DISPLAY_TTL = 60; const ONE_MOBILE_TTL = 3600; + const SUPPORTED_USER_ID_SOURCES = { + 'adserver.org': '100', + 'criteo.com': '200', + 'id5-sync.com': '300', + 'intentiq.com': '400', + 'liveintent.com': '500', + 'quantcast.com': '600', + 'verizonmedia.com': '700', + 'liveramp.com': '800' + }; + + const USER_ID_DATA = { + criteoId: SUPPORTED_USER_ID_SOURCES['criteo.com'], + connectid: SUPPORTED_USER_ID_SOURCES['verizonmedia.com'], + idl_env: SUPPORTED_USER_ID_SOURCES['liveramp.com'], + lipb: { + lipbid: SUPPORTED_USER_ID_SOURCES['liveintent.com'], + segments: ['100', '200'] + }, + tdid: SUPPORTED_USER_ID_SOURCES['adserver.org'], + id5id: { + uid: SUPPORTED_USER_ID_SOURCES['id5-sync.com'], + ext: {foo: 'bar'} + }, + intentIqId: SUPPORTED_USER_ID_SOURCES['intentiq.com'], + quantcastId: SUPPORTED_USER_ID_SOURCES['quantcast.com'] + }; function createCustomBidRequest({bids, params} = {}) { var bidderRequest = getDefaultBidRequest(); @@ -133,6 +160,9 @@ describe('AolAdapter', function () { currency: 'USD', dealId: 'deal-id', netRevenue: true, + meta: { + advertiserDomains: [] + }, ttl: bidRequest.ttl }); }); @@ -339,18 +369,6 @@ describe('AolAdapter', function () { expect(request.url).not.to.contain('bidfloor='); }); - it('should return url with bidFloor option if it is present', function () { - let bidRequest = createCustomBidRequest({ - params: { - placement: 1234567, - network: '9599.1', - bidFloor: 0.80 - } - }); - let [request] = spec.buildRequests(bidRequest.bids); - expect(request.url).to.contain('bidfloor=0.8'); - }); - it('should return url with key values if keyValues param is present', function () { let bidRequest = createCustomBidRequest({ params: { @@ -463,6 +481,18 @@ describe('AolAdapter', function () { '¶m1=val1¶m2=val2¶m3=val3¶m4=val4'); }); + Object.keys(SUPPORTED_USER_ID_SOURCES).forEach(source => { + it(`should set the user ID query param for ${source}`, function () { + let bidRequest = createCustomBidRequest({ + params: getNexageGetBidParams() + }); + bidRequest.bids[0].userId = {}; + bidRequest.bids[0].userIdAsEids = createEidsArray(USER_ID_DATA); + let [request] = spec.buildRequests(bidRequest.bids); + expect(request.url).to.contain(`&eid${source}=${encodeURIComponent(SUPPORTED_USER_ID_SOURCES[source])}`); + }); + }); + it('should return request object for One Mobile POST endpoint when POST configuration is present', function () { let bidConfig = getNexagePostBidParams(); let bidRequest = createCustomBidRequest({ @@ -581,6 +611,22 @@ describe('AolAdapter', function () { } }); }); + + it('returns the bid object with eid array populated with PB set eids', () => { + let userIdBid = Object.assign({ + userId: {} + }, bid); + userIdBid.userIdAsEids = createEidsArray(USER_ID_DATA); + expect(spec.buildOpenRtbRequestData(userIdBid)).to.deep.equal({ + id: 'bid-id', + imp: [], + user: { + ext: { + eids: userIdBid.userIdAsEids + } + } + }); + }); }); describe('getUserSyncs()', function () { @@ -723,13 +769,6 @@ describe('AolAdapter', function () { }); expect(spec.formatMarketplaceDynamicParams()).to.be.equal('param1=val1;param2=val2;param3=val3;'); }); - - it('should return formatted bid floor param when it is present', function () { - let params = { - bidFloor: 0.45 - }; - expect(spec.formatMarketplaceDynamicParams(params)).to.be.equal('bidfloor=0.45;'); - }); }); describe('formatOneMobileDynamicParams()', function () { diff --git a/test/spec/modules/quantumdexBidAdapter_spec.js b/test/spec/modules/apacdexBidAdapter_spec.js similarity index 76% rename from test/spec/modules/quantumdexBidAdapter_spec.js rename to test/spec/modules/apacdexBidAdapter_spec.js index 82429cbedae..3a71833bc3e 100644 --- a/test/spec/modules/quantumdexBidAdapter_spec.js +++ b/test/spec/modules/apacdexBidAdapter_spec.js @@ -1,14 +1,15 @@ import { expect } from 'chai' -import { spec } from 'modules/quantumdexBidAdapter.js' +import { spec, validateGeoObject, getDomain } from '../../../modules/apacdexBidAdapter.js' import { newBidder } from 'src/adapters/bidderFactory.js' import { userSync } from '../../../src/userSync.js'; +import { config } from 'src/config.js'; -describe('QuantumdexBidAdapter', function () { +describe('ApacdexBidAdapter', function () { const adapter = newBidder(spec) describe('.code', function () { - it('should return a bidder code of quantumdex', function () { - expect(spec.code).to.equal('quantumdex') + it('should return a bidder code of apacdex', function () { + expect(spec.code).to.equal('apacdex') }) }) @@ -21,7 +22,7 @@ describe('QuantumdexBidAdapter', function () { describe('.isBidRequestValid', function () { it('should return false if there are no params', () => { const bid = { - 'bidder': 'quantumdex', + 'bidder': 'apacdex', 'adUnitCode': 'adunit-code', 'mediaTypes': { banner: { @@ -37,7 +38,7 @@ describe('QuantumdexBidAdapter', function () { it('should return false if there is no siteId param', () => { const bid = { - 'bidder': 'quantumdex', + 'bidder': 'apacdex', 'adUnitCode': 'adunit-code', params: { site_id: '1a2b3c4d5e6f1a2b3c4d', @@ -56,7 +57,7 @@ describe('QuantumdexBidAdapter', function () { it('should return false if there is no mediaTypes', () => { const bid = { - 'bidder': 'quantumdex', + 'bidder': 'apacdex', 'adUnitCode': 'adunit-code', params: { siteId: '1a2b3c4d5e6f1a2b3c4d' @@ -72,7 +73,7 @@ describe('QuantumdexBidAdapter', function () { it('should return true if the bid is valid', () => { const bid = { - 'bidder': 'quantumdex', + 'bidder': 'apacdex', 'adUnitCode': 'adunit-code', params: { siteId: '1a2b3c4d5e6f1a2b3c4d' @@ -92,7 +93,7 @@ describe('QuantumdexBidAdapter', function () { describe('banner', () => { it('should return false if there are no banner sizes', () => { const bid = { - 'bidder': 'quantumdex', + 'bidder': 'apacdex', 'adUnitCode': 'adunit-code', params: { siteId: '1a2b3c4d5e6f1a2b3c4d' @@ -111,7 +112,7 @@ describe('QuantumdexBidAdapter', function () { it('should return true if there is banner sizes', () => { const bid = { - 'bidder': 'quantumdex', + 'bidder': 'apacdex', 'adUnitCode': 'adunit-code', params: { siteId: '1a2b3c4d5e6f1a2b3c4d' @@ -132,7 +133,7 @@ describe('QuantumdexBidAdapter', function () { describe('video', () => { it('should return false if there is no playerSize defined in the video mediaType', () => { const bid = { - 'bidder': 'quantumdex', + 'bidder': 'apacdex', 'adUnitCode': 'adunit-code', params: { siteId: '1a2b3c4d5e6f1a2b3c4d', @@ -152,7 +153,7 @@ describe('QuantumdexBidAdapter', function () { it('should return true if there is playerSize defined on the video mediaType', () => { const bid = { - 'bidder': 'quantumdex', + 'bidder': 'apacdex', 'adUnitCode': 'adunit-code', params: { siteId: '1a2b3c4d5e6f1a2b3c4d', @@ -196,17 +197,40 @@ describe('QuantumdexBidAdapter', function () { }, ] }, - 'bidder': 'quantumdex', + 'bidder': 'apacdex', 'params': { 'siteId': '1a2b3c4d5e6f1a2b3c4d', + 'geo': {'lat': 123.13123456, 'lon': 54.23467311, 'accuracy': 60} }, 'adUnitCode': 'adunit-code-1', 'sizes': [[300, 250], [300, 600]], 'targetKey': 0, 'bidId': '30b31c1838de1f', + 'userIdAsEids': [{ + 'source': 'criteo.com', + 'uids': [{ + 'id': 'p0cCLF9JazY1ZUFjazJRb3NKbEprVTcwZ0IwRUlGalBjOG9laUZNbFJ0ZGpOSnVFbE9VMjBNMzNBTzladGt4cUVGQzBybDY2Y1FqT1dkUkFsMmJIWDRHNjlvNXJjbiUyQlZDd1dOTmt6VlV2TDhRd0F0RTlBcmpyZU5WRHBPU25GQXpyMnlT', + 'atype': 1 + }] + }, { + 'source': 'pubcid.org', + 'uids': [{ + 'id': '2ae366c2-2576-45e5-bd21-72ed10598f17', + 'atype': 1 + }] + }, { + 'source': 'sharedid.org', + 'uids': [{ + 'id': '01EZXQDVAPER4KE1VBS29XKV4Z', + 'atype': 1, + 'ext': { + 'third': '01EZXQDVAPER4KE1VBS29XKV4Z' + } + }] + }], }, { - 'bidder': 'quantumdex', + 'bidder': 'apacdex', 'params': { 'ad_unit': '/7780971/sparks_prebid_LB', 'sizes': [[300, 250], [300, 600]], @@ -235,25 +259,25 @@ describe('QuantumdexBidAdapter', function () { it('should return a properly formatted request', function () { const bidRequests = spec.buildRequests(bidRequest, bidderRequests) - expect(bidRequests.url).to.equal('https://useast.quantumdex.io/auction/quantumdex') + expect(bidRequests.url).to.equal('https://useast.quantumdex.io/auction/apacdex') expect(bidRequests.method).to.equal('POST') expect(bidRequests.bidderRequests).to.eql(bidRequest); }) it('should return a properly formatted request with GDPR applies set to true', function () { const bidRequests = spec.buildRequests(bidRequest, bidderRequests) - expect(bidRequests.url).to.equal('https://useast.quantumdex.io/auction/quantumdex') + expect(bidRequests.url).to.equal('https://useast.quantumdex.io/auction/apacdex') expect(bidRequests.method).to.equal('POST') - expect(bidRequests.data.gdpr.gdprApplies).to.equal('true') + expect(bidRequests.data.gdpr.gdprApplies).to.equal(true) expect(bidRequests.data.gdpr.consentString).to.equal('BOJ/P2HOJ/P2HABABMAAAAAZ+A==') }) it('should return a properly formatted request with GDPR applies set to false', function () { bidderRequests.gdprConsent.gdprApplies = false; const bidRequests = spec.buildRequests(bidRequest, bidderRequests) - expect(bidRequests.url).to.equal('https://useast.quantumdex.io/auction/quantumdex') + expect(bidRequests.url).to.equal('https://useast.quantumdex.io/auction/apacdex') expect(bidRequests.method).to.equal('POST') - expect(bidRequests.data.gdpr.gdprApplies).to.equal('false') + expect(bidRequests.data.gdpr.gdprApplies).to.equal(false) expect(bidRequests.data.gdpr.consentString).to.equal('BOJ/P2HOJ/P2HABABMAAAAAZ+A==') }) it('should return a properly formatted request with GDPR applies set to false with no consent_string param', function () { @@ -271,9 +295,9 @@ describe('QuantumdexBidAdapter', function () { } }; const bidRequests = spec.buildRequests(bidRequest, bidderRequests) - expect(bidRequests.url).to.equal('https://useast.quantumdex.io/auction/quantumdex') + expect(bidRequests.url).to.equal('https://useast.quantumdex.io/auction/apacdex') expect(bidRequests.method).to.equal('POST') - expect(bidRequests.data.gdpr.gdprApplies).to.equal('false') + expect(bidRequests.data.gdpr.gdprApplies).to.equal(false) expect(bidRequests.data.gdpr).to.not.include.keys('consentString') }) it('should return a properly formatted request with GDPR applies set to true with no consentString param', function () { @@ -291,25 +315,45 @@ describe('QuantumdexBidAdapter', function () { } }; const bidRequests = spec.buildRequests(bidRequest, bidderRequests) - expect(bidRequests.url).to.equal('https://useast.quantumdex.io/auction/quantumdex') + expect(bidRequests.url).to.equal('https://useast.quantumdex.io/auction/apacdex') expect(bidRequests.method).to.equal('POST') - expect(bidRequests.data.gdpr.gdprApplies).to.equal('true') + expect(bidRequests.data.gdpr.gdprApplies).to.equal(true) expect(bidRequests.data.gdpr).to.not.include.keys('consentString') }) it('should return a properly formatted request with schain defined', function () { const bidRequests = spec.buildRequests(bidRequest, bidderRequests); - expect(JSON.parse(bidRequests.data.schain)).to.deep.equal(bidRequest[0].schain) + expect(bidRequests.data.schain).to.deep.equal(bidRequest[0].schain) + }); + it('should return a properly formatted request with eids defined', function () { + const bidRequests = spec.buildRequests(bidRequest, bidderRequests); + expect(bidRequests.data.eids).to.deep.equal(bidRequest[0].userIdAsEids) + }); + it('should return a properly formatted request with geo defined', function () { + const bidRequests = spec.buildRequests(bidRequest, bidderRequests); + expect(bidRequests.data.geo).to.deep.equal(bidRequest[0].params.geo) }); it('should return a properly formatted request with us_privacy included', function () { const bidRequests = spec.buildRequests(bidRequest, bidderRequests); expect(bidRequests.data.us_privacy).to.equal('someCCPAString'); }); + describe('debug test', function() { + beforeEach(function() { + config.setConfig({debug: true}); + }); + afterEach(function() { + config.setConfig({debug: false}); + }); + it('should return a properly formatted request with pbjs_debug is true', function () { + const bidRequests = spec.buildRequests(bidRequest, bidderRequests); + expect(bidRequests.data.test).to.equal(1); + }); + }); }); describe('.interpretResponse', function () { const bidRequests = { 'method': 'POST', - 'url': 'https://useast.quantumdex.io/auction/quantumdex', + 'url': 'https://useast.quantumdex.io/auction/apacdex', 'withCredentials': true, 'data': { 'device': { @@ -328,7 +372,7 @@ describe('QuantumdexBidAdapter', function () { }, 'bidderRequests': [ { - 'bidder': 'quantumdex', + 'bidder': 'apacdex', 'params': { 'siteId': '343' }, @@ -363,7 +407,7 @@ describe('QuantumdexBidAdapter', function () { } }, { - 'bidder': 'quantumdex', + 'bidder': 'apacdex', 'params': { 'siteId': '343' }, @@ -398,7 +442,7 @@ describe('QuantumdexBidAdapter', function () { } }, { - 'bidder': 'quantumdex', + 'bidder': 'apacdex', 'params': { 'siteId': '343' }, @@ -464,12 +508,12 @@ describe('QuantumdexBidAdapter', function () { 'cpm': 1.07, 'width': 160, 'height': 600, - 'ad': `
Quantumdex AD
`, + 'ad': `
Apacdex AD
`, 'ttl': 500, 'creativeId': '1234abcd', 'netRevenue': true, 'currency': 'USD', - 'dealId': 'quantumdex', + 'dealId': 'apacdex', 'mediaType': 'banner' }, { @@ -477,12 +521,12 @@ describe('QuantumdexBidAdapter', function () { 'cpm': 1, 'width': 300, 'height': 250, - 'ad': `
Quantumdex AD
`, + 'ad': `
Apacdex AD
`, 'ttl': 500, 'creativeId': '1234abcd', 'netRevenue': true, 'currency': 'USD', - 'dealId': 'quantumdex', + 'dealId': 'apacdex', 'mediaType': 'banner' }, { @@ -490,12 +534,12 @@ describe('QuantumdexBidAdapter', function () { 'cpm': 1.25, 'width': 300, 'height': 250, - 'vastXml': 'quantumdex', + 'vastXml': 'apacdex', 'ttl': 500, 'creativeId': '30292e432662bd5f86d90774b944b038', 'netRevenue': true, 'currency': 'USD', - 'dealId': 'quantumdex', + 'dealId': 'apacdex', 'mediaType': 'video' } ], @@ -512,12 +556,12 @@ describe('QuantumdexBidAdapter', function () { 'cpm': 1.07, 'width': 160, 'height': 600, - 'ad': `
Quantumdex AD
`, + 'ad': `
Apacdex AD
`, 'ttl': 500, 'creativeId': '1234abcd', 'netRevenue': true, 'currency': 'USD', - 'dealId': 'quantumdex', + 'dealId': 'apacdex', 'mediaType': 'banner' }, { @@ -525,12 +569,12 @@ describe('QuantumdexBidAdapter', function () { 'cpm': 1, 'width': 300, 'height': 250, - 'ad': `
Quantumdex AD
`, + 'ad': `
Apacdex AD
`, 'ttl': 500, 'creativeId': '1234abcd', 'netRevenue': true, 'currency': 'USD', - 'dealId': 'quantumdex', + 'dealId': 'apacdex', 'mediaType': 'banner' }, { @@ -538,12 +582,12 @@ describe('QuantumdexBidAdapter', function () { 'cpm': 1.25, 'width': 300, 'height': 250, - 'vastXml': 'quantumdex', + 'vastXml': 'apacdex', 'ttl': 500, 'creativeId': '30292e432662bd5f86d90774b944b038', 'netRevenue': true, 'currency': 'USD', - 'dealId': 'quantumdex', + 'dealId': 'apacdex', 'mediaType': 'video' } ]; @@ -561,10 +605,10 @@ describe('QuantumdexBidAdapter', function () { expect(resp.currency).to.equal(prebidResponse[i].currency); expect(resp.dealId).to.equal(prebidResponse[i].dealId); if (resp.mediaType === 'video') { - expect(resp.vastXml.indexOf('quantumdex')).to.be.greaterThan(0); + expect(resp.vastXml.indexOf('apacdex')).to.be.greaterThan(0); } if (resp.mediaType === 'banner') { - expect(resp.ad.indexOf('Quantumdex AD')).to.be.greaterThan(0); + expect(resp.ad.indexOf('Apacdex AD')).to.be.greaterThan(0); } }); }); @@ -601,4 +645,66 @@ describe('QuantumdexBidAdapter', function () { expect(spec.getUserSyncs({ pixelEnabled: true }, [])).to.have.length(0); }); }); + + describe('validateGeoObject', function () { + it('should return true if the geo object is valid', () => { + let geoObject = { + lat: 123.5624234, + lon: 23.6712341, + accuracy: 20 + }; + expect(validateGeoObject(geoObject)).to.equal(true); + }); + + it('should return false if the geo object is not plain object', () => { + let geoObject = [{ + lat: 123.5624234, + lon: 23.6712341, + accuracy: 20 + }]; + expect(validateGeoObject(geoObject)).to.equal(false); + }); + + it('should return false if the geo object is missing lat attribute', () => { + let geoObject = { + lon: 23.6712341, + accuracy: 20 + }; + expect(validateGeoObject(geoObject)).to.equal(false); + }); + + it('should return false if the geo object is missing lon attribute', () => { + let geoObject = { + lat: 123.5624234, + accuracy: 20 + }; + expect(validateGeoObject(geoObject)).to.equal(false); + }); + + it('should return false if the geo object is missing accuracy attribute', () => { + let geoObject = { + lat: 123.5624234, + lon: 23.6712341 + }; + expect(validateGeoObject(geoObject)).to.equal(false); + }); + }); + + describe('getDomain', function () { + it('should return valid domain from publisherDomain config', () => { + let pageUrl = 'https://www.example.com/page/prebid/exam.html'; + config.setConfig({publisherDomain: pageUrl}); + expect(getDomain(pageUrl)).to.equal('example.com'); + }); + it('should return valid domain from pageUrl argument', () => { + let pageUrl = 'https://www.example.com/page/prebid/exam.html'; + config.setConfig({publisherDomain: ''}); + expect(getDomain(pageUrl)).to.equal('example.com'); + }); + it('should return undefined if pageUrl and publisherDomain not config', () => { + let pageUrl; + config.setConfig({publisherDomain: ''}); + expect(getDomain(pageUrl)).to.equal(pageUrl); + }); + }); }); diff --git a/test/spec/modules/appnexusBidAdapter_spec.js b/test/spec/modules/appnexusBidAdapter_spec.js index 7e985045c45..c875cba12bc 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,9 +278,40 @@ describe('AppNexusAdapter', function () { expect(payload.user).to.exist; expect(payload.user).to.deep.equal({ external_uid: '123', + segments: [{id: 123}, {id: 987, value: 876}] }); }); + it('should attach reserve param when either bid param or getFloor function exists', function () { + let getFloorResponse = { currency: 'USD', floor: 3 }; + let request, payload = null; + let bidRequest = deepClone(bidRequests[0]); + + // 1 -> reserve not defined, getFloor not defined > empty + request = spec.buildRequests([bidRequest]); + payload = JSON.parse(request.data); + + expect(payload.tags[0].reserve).to.not.exist; + + // 2 -> reserve is defined, getFloor not defined > reserve is used + bidRequest.params = { + 'placementId': '10433394', + 'reserve': 0.5 + }; + request = spec.buildRequests([bidRequest]); + payload = JSON.parse(request.data); + + expect(payload.tags[0].reserve).to.exist.and.to.equal(0.5); + + // 3 -> reserve is defined, getFloor is defined > getFloor is used + bidRequest.getFloor = () => getFloorResponse; + + request = spec.buildRequests([bidRequest]); + payload = JSON.parse(request.data); + + expect(payload.tags[0].reserve).to.exist.and.to.equal(3); + }); + it('should duplicate adpod placements into batches and set correct maxduration', function() { let bidRequest = Object.assign({}, bidRequests[0], @@ -627,6 +659,17 @@ describe('AppNexusAdapter', function () { expect(payload.tags[0].use_pmt_rule).to.equal(true); }); + it('should add gpid to the request', function () { + let testGpid = '/12345/my-gpt-tag-0'; + let bidRequest = deepClone(bidRequests[0]); + bidRequest.ortb2Imp = { ext: { data: { pbadslot: testGpid } } }; + + const request = spec.buildRequests([bidRequest]); + const payload = JSON.parse(request.data); + + expect(payload.tags[0].gpid).to.exist.and.equal(testGpid) + }); + it('should add gdpr consent information to the request', function () { let consentString = 'BOJ8RZsOJ8RZsABAB8AAAAAZ+A=='; let bidderRequest = { @@ -642,7 +685,7 @@ describe('AppNexusAdapter', function () { bidderRequest.bids = bidRequests; const request = spec.buildRequests(bidRequests, bidderRequest); - expect(request.options).to.be.empty; + expect(request.options).to.deep.equal({withCredentials: true}); const payload = JSON.parse(request.data); expect(payload.gdpr_consent).to.exist; @@ -782,7 +825,25 @@ describe('AppNexusAdapter', function () { config.getConfig.restore(); }); - it('should set withCredentials to false if purpose 1 consent is not given', function () { + it('should set the X-Is-Test customHeader if test flag is enabled', function () { + let bidRequest = Object.assign({}, bidRequests[0]); + sinon.stub(config, 'getConfig') + .withArgs('apn_test') + .returns(true); + + const request = spec.buildRequests([bidRequest]); + expect(request.options.customHeaders).to.deep.equal({'X-Is-Test': 1}); + + config.getConfig.restore(); + }); + + it('should always set withCredentials: true on the request.options', function () { + let bidRequest = Object.assign({}, bidRequests[0]); + const request = spec.buildRequests([bidRequest]); + expect(request.options.withCredentials).to.equal(true); + }); + + it('should set simple domain variant if purpose 1 consent is not given', function () { let consentString = 'BOJ8RZsOJ8RZsABAB8AAAAAZ+A=='; let bidderRequest = { 'bidderCode': 'appnexus', @@ -805,14 +866,16 @@ describe('AppNexusAdapter', function () { bidderRequest.bids = bidRequests; const request = spec.buildRequests(bidRequests, bidderRequest); - expect(request.options).to.deep.equal({withCredentials: false}); + expect(request.url).to.equal('https://ib.adnxs-simple.com/ut/v3/prebid'); }); - it('should populate eids array 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 +891,62 @@ 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 () { + // with bid.params.frameworks + let bidRequest_A = Object.assign({}, bidRequests[0], { + params: { + frameworks: [1, 2, 5, 6], + video: { + frameworks: [1, 2, 5, 6] + } + } + }); + let request = spec.buildRequests([bidRequest_A]); + let payload = JSON.parse(request.data); + expect(payload.iab_support).to.be.an('object'); + expect(payload.iab_support).to.deep.equal({ + omidpn: 'Appnexus', + omidpv: '$prebid.version$' + }); + expect(payload.tags[0].banner_frameworks).to.be.an('array'); + expect(payload.tags[0].banner_frameworks).to.deep.equal([1, 2, 5, 6]); + expect(payload.tags[0].video_frameworks).to.be.an('array'); + expect(payload.tags[0].video_frameworks).to.deep.equal([1, 2, 5, 6]); + expect(payload.tags[0].video.frameworks).to.not.exist; + + // without bid.params.frameworks + const bidRequest_B = Object.assign({}, bidRequests[0]); + request = spec.buildRequests([bidRequest_B]); + payload = JSON.parse(request.data); + expect(payload.iab_support).to.not.exist; + expect(payload.tags[0].banner_frameworks).to.not.exist; + expect(payload.tags[0].video_frameworks).to.not.exist; + + // with video.frameworks but it is not an array + const bidRequest_C = Object.assign({}, bidRequests[0], { + params: { + video: { + frameworks: "'1', '2', '3', '6'" + } + } + }); + request = spec.buildRequests([bidRequest_C]); + payload = JSON.parse(request.data); + expect(payload.iab_support).to.not.exist; + expect(payload.tags[0].banner_frameworks).to.not.exist; + expect(payload.tags[0].video_frameworks).to.not.exist; }); }) @@ -1161,6 +1280,21 @@ describe('AppNexusAdapter', function () { } let result = spec.interpretResponse({ body: responseAdvertiserId }, {bidderRequest}); expect(Object.keys(result[0].meta)).to.include.members(['advertiserId']); - }) + }); + + it('should add advertiserDomains', function() { + let responseAdvertiserId = deepClone(response); + responseAdvertiserId.tags[0].ads[0].adomain = ['123']; + + let bidderRequest = { + bids: [{ + bidId: '3db3773286ee59', + adUnitCode: 'code' + }] + } + let result = spec.interpretResponse({ body: responseAdvertiserId }, {bidderRequest}); + expect(Object.keys(result[0].meta)).to.include.members(['advertiserDomains']); + expect(Object.keys(result[0].meta.advertiserDomains)).to.deep.equal([]); + }); }); }); diff --git a/test/spec/modules/apstreamBidAdapter_spec.js b/test/spec/modules/apstreamBidAdapter_spec.js index c6546a3bd83..e640c009989 100644 --- a/test/spec/modules/apstreamBidAdapter_spec.js +++ b/test/spec/modules/apstreamBidAdapter_spec.js @@ -31,6 +31,22 @@ describe('AP Stream adapter', function() { } }; + let mockConfig; + beforeEach(function () { + mockConfig = { + apstream: { + publisherId: '4321' + } + }; + sinon.stub(config, 'getConfig').callsFake((key) => { + return utils.deepAccess(mockConfig, key); + }); + }); + + afterEach(function () { + config.getConfig.restore(); + }); + it('should return true when publisherId is configured and one media type', function() { bid.params.publisherId = '1234'; assert(spec.isBidRequestValid(bid)) @@ -40,6 +56,12 @@ describe('AP Stream adapter', function() { bid.mediaTypes.video = {sizes: [300, 250]}; assert.isFalse(spec.isBidRequestValid(bid)) }); + + it('should return true when publisherId is configured via config', function() { + delete bid.mediaTypes.video; + delete bid.params.publisherId; + assert.isTrue(spec.isBidRequestValid(bid)) + }); }); describe('buildRequests', 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 849ccf88c51..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.spy(atsAnalyticsAdapter, 'track'); + 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,77 +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/automatadBidAdapter_spec.js b/test/spec/modules/automatadBidAdapter_spec.js index a6de8810284..9d828bad4c3 100644 --- a/test/spec/modules/automatadBidAdapter_spec.js +++ b/test/spec/modules/automatadBidAdapter_spec.js @@ -86,6 +86,11 @@ describe('automatadBidAdapter', function () { expect(rdata.imp.length).to.equal(1) }) + it('should include placement', function () { + let r = rdata.imp[0] + expect(r.placement !== null).to.be.true + }) + it('should include media types', function () { let r = rdata.imp[0] expect(r.media_types !== null).to.be.true @@ -106,6 +111,57 @@ describe('automatadBidAdapter', function () { it('should get the correct bid response', function () { let result = spec.interpretResponse(expectedResponse[0]) expect(result).to.be.an('array').that.is.not.empty + expect(result[0].meta.advertiserDomains[0]).to.equal('someAdDomain'); + }) + + it('should interpret multiple bids in seatbid', function () { + let multipleBidResponse = [{ + 'body': { + 'id': 'abc-321', + 'seatbid': [ + { + 'bid': [ + { + 'adm': '', + 'adomain': [ + 'someAdDomain' + ], + 'crid': 123, + 'h': 600, + 'id': 'bid1', + 'impid': 'imp1', + 'nurl': 'https://example/win', + 'price': 0.5, + 'w': 300 + } + ] + }, { + 'bid': [ + { + 'adm': '', + 'adomain': [ + 'someAdDomain' + ], + 'crid': 321, + 'h': 600, + 'id': 'bid1', + 'impid': 'imp2', + 'nurl': 'https://example/win', + 'price': 0.5, + 'w': 300 + } + ] + } + ] + } + }] + let result = spec.interpretResponse(multipleBidResponse[0]).map(bid => { + const {requestId} = bid; + return [ requestId ]; + }); + + assert.equal(result.length, 2); + assert.deepEqual(result, [[ 'imp1' ], [ 'imp2' ]]); }) it('handles empty bid response', function () { diff --git a/test/spec/modules/avocetBidAdapter_spec.js b/test/spec/modules/avocetBidAdapter_spec.js index 4cfd8ab89d4..2a2f29e48d2 100644 --- a/test/spec/modules/avocetBidAdapter_spec.js +++ b/test/spec/modules/avocetBidAdapter_spec.js @@ -69,7 +69,9 @@ describe('Avocet adapter', function () { placement: '012345678901234567890123', }, userId: { - id5id: 'test' + id5id: { + uid: 'test' + } } }, { diff --git a/test/spec/modules/axonixBidAdapter_spec.js b/test/spec/modules/axonixBidAdapter_spec.js new file mode 100644 index 00000000000..a952d527600 --- /dev/null +++ b/test/spec/modules/axonixBidAdapter_spec.js @@ -0,0 +1,378 @@ +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 = { + body: [{ + 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 = { + body: [{ + 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('effectiveType').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('effectiveType').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({ body: [{ 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.body[0]); + }); + + 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.body[0]); + }); + + 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(BANNER_RESPONSE.body[0]); + 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 5711e111e30..fc74ec8a2aa 100644 --- a/test/spec/modules/beachfrontBidAdapter_spec.js +++ b/test/spec/modules/beachfrontBidAdapter_spec.js @@ -1,5 +1,4 @@ import { expect } from 'chai'; -import sinon from 'sinon'; import { spec, VIDEO_ENDPOINT, BANNER_ENDPOINT, OUTSTREAM_SRC, DEFAULT_MIMES } from 'modules/beachfrontBidAdapter.js'; import { parseUrl } from 'src/utils.js'; @@ -172,6 +171,24 @@ describe('BeachfrontAdapter', function () { expect(data.cur).to.deep.equal(['USD']); }); + it('must read from the floors module if available', function () { + const bidRequest = bidRequests[0]; + bidRequest.mediaTypes = { video: {} }; + bidRequest.getFloor = () => ({ currency: 'USD', floor: 1.16 }); + const requests = spec.buildRequests([ bidRequest ]); + const data = requests[0].data; + expect(data.imp[0].bidfloor).to.equal(1.16); + }); + + it('must use the bid floor param if no value is returned from the floors module', function () { + const bidRequest = bidRequests[0]; + bidRequest.mediaTypes = { video: {} }; + bidRequest.getFloor = () => ({}); + const requests = spec.buildRequests([ bidRequest ]); + const data = requests[0].data; + expect(data.imp[0].bidfloor).to.equal(bidRequest.params.bidfloor); + }); + it('must parse bid size from a nested array', function () { const width = 640; const height = 480; @@ -223,17 +240,33 @@ describe('BeachfrontAdapter', function () { expect(data.imp[0].video).to.deep.contain({ w: width, h: height }); }); - it('must override video targeting params', function () { + it('must set video params from the standard object', function () { const bidRequest = bidRequests[0]; const mimes = ['video/webm']; const playbackmethod = 2; const maxduration = 30; const placement = 4; - bidRequest.mediaTypes = { video: {} }; - bidRequest.params.video = { mimes, playbackmethod, maxduration, placement }; + const skip = 1; + bidRequest.mediaTypes = { + video: { mimes, playbackmethod, maxduration, placement, skip } + }; + const requests = spec.buildRequests([ bidRequest ]); + const data = requests[0].data; + expect(data.imp[0].video).to.deep.contain({ mimes, playbackmethod, maxduration, placement, skip }); + }); + + it('must override video params from the bidder object', function () { + const bidRequest = bidRequests[0]; + const mimes = ['video/webm']; + const playbackmethod = 2; + const maxduration = 30; + const placement = 4; + const skip = 1; + bidRequest.mediaTypes = { video: { placement: 3, skip: 0 } }; + bidRequest.params.video = { mimes, playbackmethod, maxduration, placement, skip }; const requests = spec.buildRequests([ bidRequest ]); const data = requests[0].data; - expect(data.imp[0].video).to.deep.contain({ mimes, playbackmethod, maxduration, placement }); + expect(data.imp[0].video).to.deep.contain({ mimes, playbackmethod, maxduration, placement, skip }); }); it('must add US privacy data to the request', function () { @@ -262,6 +295,27 @@ describe('BeachfrontAdapter', function () { expect(data.user.ext.consent).to.equal(consentString); }); + it('must add schain data to the request', () => { + const schain = { + ver: '1.0', + complete: 1, + nodes: [ + { + asi: 'directseller.com', + sid: '00001', + rid: 'BidRequest1', + hp: 1 + } + ] + }; + const bidRequest = bidRequests[0]; + bidRequest.mediaTypes = { video: {} }; + bidRequest.schain = schain; + const requests = spec.buildRequests([ bidRequest ]); + const data = requests[0].data; + expect(data.source.ext.schain).to.deep.equal(schain); + }); + it('must add the Trade Desk User ID to the request', () => { const tdid = '4321'; const bidRequest = bidRequests[0]; @@ -279,6 +333,42 @@ describe('BeachfrontAdapter', function () { }] }); }); + + it('must add the IdentityLink ID to the request', () => { + const idl_env = '4321'; + const bidRequest = bidRequests[0]; + bidRequest.mediaTypes = { video: {} }; + bidRequest.userId = { idl_env }; + const requests = spec.buildRequests([ bidRequest ]); + const data = requests[0].data; + expect(data.user.ext.eids[0]).to.deep.equal({ + source: 'liveramp.com', + uids: [{ + id: idl_env, + ext: { + rtiPartner: 'idl' + } + }] + }); + }); + + it('must add the Unified ID 2.0 to the request', () => { + const uid2 = { id: '4321' }; + const bidRequest = bidRequests[0]; + bidRequest.mediaTypes = { video: {} }; + bidRequest.userId = { uid2 }; + const requests = spec.buildRequests([ bidRequest ]); + const data = requests[0].data; + expect(data.user.ext.eids[0]).to.deep.equal({ + source: 'uidapi.com', + uids: [{ + id: uid2.id, + ext: { + rtiPartner: 'UID2' + } + }] + }); + }); }); describe('for banner bids', function () { @@ -302,6 +392,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 ] @@ -320,6 +411,7 @@ describe('BeachfrontAdapter', function () { slot: bidRequest.adUnitCode, id: bidRequest.params.appId, bidfloor: bidRequest.params.bidfloor, + tagid: bidRequest.params.tagid, sizes: [{ w: width, h: height }] } ]); @@ -329,6 +421,24 @@ describe('BeachfrontAdapter', function () { expect(data.ua).to.equal(navigator.userAgent); }); + it('must read from the floors module if available', function () { + const bidRequest = bidRequests[0]; + bidRequest.mediaTypes = { banner: {} }; + bidRequest.getFloor = () => ({ currency: 'USD', floor: 1.16 }); + const requests = spec.buildRequests([ bidRequest ]); + const data = requests[0].data; + expect(data.slots[0].bidfloor).to.equal(1.16); + }); + + it('must use the bid floor param if no value is returned from the floors module', function () { + const bidRequest = bidRequests[0]; + bidRequest.mediaTypes = { banner: {} }; + bidRequest.getFloor = () => ({}); + const requests = spec.buildRequests([ bidRequest ]); + const data = requests[0].data; + expect(data.slots[0].bidfloor).to.equal(bidRequest.params.bidfloor); + }); + it('must parse bid size from a nested array', function () { const width = 300; const height = 250; @@ -410,6 +520,27 @@ describe('BeachfrontAdapter', function () { expect(data.gdprConsent).to.equal(consentString); }); + it('must add schain data to the request', () => { + const schain = { + ver: '1.0', + complete: 1, + nodes: [ + { + asi: 'directseller.com', + sid: '00001', + rid: 'BidRequest1', + hp: 1 + } + ] + }; + const bidRequest = bidRequests[0]; + bidRequest.mediaTypes = { banner: {} }; + bidRequest.schain = schain; + const requests = spec.buildRequests([ bidRequest ]); + const data = requests[0].data; + expect(data.schain).to.deep.equal(schain); + }); + it('must add the Trade Desk User ID to the request', () => { const tdid = '4321'; const bidRequest = bidRequests[0]; @@ -419,6 +550,26 @@ describe('BeachfrontAdapter', function () { const data = requests[0].data; expect(data.tdid).to.equal(tdid); }); + + it('must add the IdentityLink ID to the request', () => { + const idl_env = '4321'; + const bidRequest = bidRequests[0]; + bidRequest.mediaTypes = { banner: {} }; + bidRequest.userId = { idl_env }; + const requests = spec.buildRequests([ bidRequest ]); + const data = requests[0].data; + expect(data.idl).to.equal(idl_env); + }); + + it('must add the Unified ID 2.0 to the request', () => { + const uid2 = { id: '4321' }; + const bidRequest = bidRequests[0]; + bidRequest.mediaTypes = { banner: {} }; + bidRequest.userId = { uid2 }; + const requests = spec.buildRequests([ bidRequest ]); + const data = requests[0].data; + expect(data.uid2).to.equal(uid2.id); + }); }); describe('for multi-format bids', function () { @@ -486,16 +637,6 @@ describe('BeachfrontAdapter', function () { expect(bidResponse.length).to.equal(0); }); - it('should return no bids if the response "url" is missing', function () { - const bidRequest = bidRequests[0]; - bidRequest.mediaTypes = { video: {} }; - const serverResponse = { - bidPrice: 5.00 - }; - const bidResponse = spec.interpretResponse({ body: serverResponse }, { bidRequest }); - expect(bidResponse.length).to.equal(0); - }); - it('should return no bids if the response "bidPrice" is missing', function () { const bidRequest = bidRequests[0]; bidRequest.mediaTypes = { video: {} }; @@ -518,6 +659,7 @@ describe('BeachfrontAdapter', function () { const serverResponse = { bidPrice: 5.00, url: 'http://reachms.bfmio.com/getmu?aid=bid:19c4a196-fb21-4c81-9a1a-ecc5437a39da', + vast: '', crid: '123abc' }; const bidResponse = spec.interpretResponse({ body: serverResponse }, { bidRequest }); @@ -527,7 +669,7 @@ describe('BeachfrontAdapter', function () { cpm: serverResponse.bidPrice, creativeId: serverResponse.crid, vastUrl: serverResponse.url, - vastXml: undefined, + vastXml: serverResponse.vast, width: width, height: height, renderer: null, @@ -558,7 +700,7 @@ describe('BeachfrontAdapter', function () { }); }); - it('should return vast xml if found on the bid response', () => { + it('should return only vast url if the response type is "nurl"', () => { const width = 640; const height = 480; const bidRequest = bidRequests[0]; @@ -567,6 +709,7 @@ describe('BeachfrontAdapter', function () { playerSize: [ width, height ] } }; + bidRequest.params.video = { responseType: 'nurl' }; const serverResponse = { bidPrice: 5.00, url: 'http://reachms.bfmio.com/getmu?aid=bid:19c4a196-fb21-4c81-9a1a-ecc5437a39da', @@ -574,86 +717,49 @@ describe('BeachfrontAdapter', function () { crid: '123abc' }; const bidResponse = spec.interpretResponse({ body: serverResponse }, { bidRequest }); - expect(bidResponse).to.deep.contain({ - vastUrl: serverResponse.url, - vastXml: serverResponse.vast - }); - }); - - it('should return a renderer for outstream video bids', function () { - const bidRequest = bidRequests[0]; - bidRequest.mediaTypes = { - video: { - context: 'outstream' - } - }; - const serverResponse = { - bidPrice: 5.00, - url: 'http://reachms.bfmio.com/getmu?aid=bid:19c4a196-fb21-4c81-9a1a-ecc5437a39da', - crid: '123abc' - }; - const bidResponse = spec.interpretResponse({ body: serverResponse }, { bidRequest }); - expect(bidResponse.renderer).to.deep.contain({ - id: bidRequest.bidId, - url: OUTSTREAM_SRC - }); + expect(bidResponse.vastUrl).to.equal(serverResponse.url); + expect(bidResponse.vastXml).to.equal(undefined); }); - it('should initialize a player for outstream bids', () => { + it('should return only vast xml if the response type is "adm"', () => { const width = 640; const height = 480; const bidRequest = bidRequests[0]; bidRequest.mediaTypes = { video: { - context: 'outstream', playerSize: [ width, height ] } }; + bidRequest.params.video = { responseType: 'adm' }; const serverResponse = { bidPrice: 5.00, url: 'http://reachms.bfmio.com/getmu?aid=bid:19c4a196-fb21-4c81-9a1a-ecc5437a39da', + vast: '', crid: '123abc' }; const bidResponse = spec.interpretResponse({ body: serverResponse }, { bidRequest }); - window.Beachfront = { Player: sinon.spy() }; - bidResponse.adUnitCode = bidRequest.adUnitCode; - bidResponse.renderer.render(bidResponse); - sinon.assert.calledWith(window.Beachfront.Player, bidResponse.adUnitCode, sinon.match({ - adTagUrl: bidResponse.vastUrl, - width: bidResponse.width, - height: bidResponse.height, - expandInView: false, - collapseOnComplete: true - })); - delete window.Beachfront; - }); - - it('should configure outstream player settings from the bidder params', () => { - const width = 640; - const height = 480; + expect(bidResponse.vastUrl).to.equal(undefined); + expect(bidResponse.vastXml).to.equal(serverResponse.vast); + }); + + it('should return a renderer for outstream video bids', function () { const bidRequest = bidRequests[0]; bidRequest.mediaTypes = { video: { - context: 'outstream', - playerSize: [ width, height ] + context: 'outstream' } }; - bidRequest.params.player = { - expandInView: true, - collapseOnComplete: false, - progressColor: 'green' - }; const serverResponse = { bidPrice: 5.00, url: 'http://reachms.bfmio.com/getmu?aid=bid:19c4a196-fb21-4c81-9a1a-ecc5437a39da', crid: '123abc' }; const bidResponse = spec.interpretResponse({ body: serverResponse }, { bidRequest }); - window.Beachfront = { Player: sinon.spy() }; - bidResponse.adUnitCode = bidRequest.adUnitCode; - bidResponse.renderer.render(bidResponse); - sinon.assert.calledWith(window.Beachfront.Player, bidResponse.adUnitCode, sinon.match(bidRequest.params.player)); - delete window.Beachfront; + expect(bidResponse.renderer).to.deep.contain({ + id: bidRequest.bidId, + url: OUTSTREAM_SRC + }); + expect(bidResponse.renderer.render).to.be.a('function'); }); }); diff --git a/test/spec/modules/betweenBidAdapter_spec.js b/test/spec/modules/betweenBidAdapter_spec.js index 94db41ba014..0e772e7be02 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,87 @@ 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']) + }); + + it('check sharedId with id and third', function() { + const bidRequestData = [{ + bidId: 'bid123', + bidder: 'between', + mediaTypes: { + banner: { + sizes: [[728, 90]] + } + }, + params: { + s: 1112, + }, + userId: { + sharedid: { + id: '01EXQE7JKNDRDDVATB0S2GX1NT', + third: '01EXQE7JKNDRDDVATB0S2GX1NT' + } + } + }]; + const shid = JSON.parse(spec.buildRequests(bidRequestData).data)[0].data.shid; + const shid3 = JSON.parse(spec.buildRequests(bidRequestData).data)[0].data.shid3; + expect(shid).to.equal('01EXQE7JKNDRDDVATB0S2GX1NT') && expect(shid3).to.equal('01EXQE7JKNDRDDVATB0S2GX1NT'); + }); + it('check sharedId with only id', function() { + const bidRequestData = [{ + bidId: 'bid123', + bidder: 'between', + mediaTypes: { + banner: { + sizes: [[728, 90]] + } + }, + params: { + s: 1112, + }, + userId: { + sharedid: { + id: '01EXQE7JKNDRDDVATB0S2GX1NT', + } + } + }]; + const shid = JSON.parse(spec.buildRequests(bidRequestData).data)[0].data.shid; + const shid3 = JSON.parse(spec.buildRequests(bidRequestData).data)[0].data.shid3; + expect(shid).to.equal('01EXQE7JKNDRDDVATB0S2GX1NT') && expect(shid3).to.equal(''); + }); + it('check adomain', function() { + const serverResponse = { + body: [{ + bidid: 'bid1234', + cpm: 1.12, + w: 240, + h: 400, + currency: 'USD', + ad: 'Ad html', + adomain: ['domain1.com', 'domain2.com'] + }] + }; + const bids = spec.interpretResponse(serverResponse); + expect(bids).to.have.lengthOf(1); + const bid = bids[0]; + expect(bid.meta.advertiserDomains).to.deep.equal(['domain1.com', 'domain2.com']); + }); + it('check server response without adomain', function() { + const serverResponse = { + body: [{ + bidid: 'bid1234', + cpm: 1.12, + w: 240, + h: 400, + currency: 'USD', + ad: 'Ad html', + }] + }; + const bids = spec.interpretResponse(serverResponse); + expect(bids).to.have.lengthOf(1); + const bid = bids[0]; + expect(bid.meta.advertiserDomains).to.deep.equal([]); }); }); 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/bizzclickBidAdapter_spec.js b/test/spec/modules/bizzclickBidAdapter_spec.js new file mode 100644 index 00000000000..e0698c9eda8 --- /dev/null +++ b/test/spec/modules/bizzclickBidAdapter_spec.js @@ -0,0 +1,396 @@ +import { expect } from 'chai'; +import { spec } from 'modules/bizzclickBidAdapter.js'; +import {config} from 'src/config.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: 'bizzclick', + params: { + placementId: 'hash', + accountId: 'accountId' + }, + timeout: 1000 + +}; + +const BANNER_BID_REQUEST = { + code: 'banner_example', + mediaTypes: { + banner: { + sizes: [[300, 250], [300, 600]] + } + }, + bidder: 'bizzclick', + params: { + placementId: 'hash', + accountId: 'accountId' + }, + timeout: 1000, + gdprConsent: { + consentString: 'BOEFEAyOEFEAyAHABDENAI4AAAB9vABAASA', + gdprApplies: 1, + }, + uspConsent: 'uspConsent' +} + +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: 'bizzclick', + params: { + placementId: 'hash', + accountId: 'accountId' + }, + timeout: 1000 + +} + +const BANNER_BID_RESPONSE = { + id: 'request_id', + bidid: 'request_imp_id', + seatbid: [{ + bid: [{ + id: 'bid_id', + impid: 'request_imp_id', + price: 5, + adomain: ['example.com'], + adm: 'admcode', + crid: 'crid', + ext: { + mediaType: 'banner' + } + }], + }], +}; + +const VIDEO_BID_RESPONSE = { + id: 'request_id', + bidid: 'request_imp_id', + seatbid: [{ + bid: [{ + id: 'bid_id', + impid: 'request_imp_id', + price: 5, + adomain: ['example.com'], + adm: 'admcode', + crid: 'crid', + ext: { + mediaType: 'video', + vastUrl: 'http://example.vast', + } + }], + }], +}; + +let imgData = { + url: `https://example.com/image`, + w: 1200, + h: 627 +}; + +const NATIVE_BID_RESPONSE = { + id: 'request_id', + bidid: 'request_imp_id', + seatbid: [{ + bid: [{ + id: 'bid_id', + impid: 'request_imp_id', + price: 5, + adomain: ['example.com'], + adm: { native: + { + assets: [ + {id: 0, title: 'dummyText'}, + {id: 3, image: imgData}, + { + id: 5, + data: {value: 'organization.name'} + } + ], + link: {url: 'example.com'}, + imptrackers: ['tracker1.com', 'tracker2.com', 'tracker3.com'], + jstracker: 'tracker1.com' + } + }, + crid: 'crid', + ext: { + mediaType: 'native' + } + }], + }], +}; + +describe('BizzclickAdapter', function() { + describe('with COPPA', function() { + beforeEach(function() { + sinon.stub(config, 'getConfig') + .withArgs('coppa') + .returns(true); + }); + afterEach(function() { + config.getConfig.restore(); + }); + + it('should send the Coppa "required" flag set to "1" in the request', function () { + let serverRequest = spec.buildRequests([BANNER_BID_REQUEST]); + expect(serverRequest.data[0].regs.coppa).to.equal(1); + }); + }); + + 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.bizzclick.com/bid?rtb_seat_id=prebidjs&secret_key=accountId'); + }); + + 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('check consent and ccpa string is set properly', function() { + expect(request.data[0].regs.ext.gdpr).to.equal(1); + expect(request.data[0].user.ext.consent).to.equal(BANNER_BID_REQUEST.gdprConsent.consentString); + expect(request.data[0].regs.ext.us_privacy).to.equal(BANNER_BID_REQUEST.uspConsent); + }) + + it('Returns valid URL', function () { + expect(request.url).to.equal('https://us-e-node1.bizzclick.com/bid?rtb_seat_id=prebidjs&secret_key=accountId'); + }); + }); + + 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.bizzclick.com/bid?rtb_seat_id=prebidjs&secret_key=accountId'); + }); + }); + + 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/bluebillywigBidAdapter_spec.js b/test/spec/modules/bluebillywigBidAdapter_spec.js index 73bd4803358..3aee6c69438 100644 --- a/test/spec/modules/bluebillywigBidAdapter_spec.js +++ b/test/spec/modules/bluebillywigBidAdapter_spec.js @@ -40,6 +40,13 @@ describe('BlueBillywigAdapter', () => { expect(spec.isBidRequestValid(baseValidBid)).to.equal(true); }); + it('should return false when params missing', () => { + const bid = deepClone(baseValidBid); + delete bid.params; + + expect(spec.isBidRequestValid(bid)).to.equal(false); + }); + it('should return false when publicationName is missing', () => { const bid = deepClone(baseValidBid); delete bid.params.publicationName; @@ -184,6 +191,44 @@ describe('BlueBillywigAdapter', () => { bid.mediaTypes[VIDEO].context = 'instream'; expect(spec.isBidRequestValid(bid)).to.equal(false); }); + + it('should fail if video is specified but is not an object', () => { + const bid = deepClone(baseValidBid); + + bid.params.video = null; + expect(spec.isBidRequestValid(bid)).to.equal(false); + + bid.params.video = 'string'; + expect(spec.isBidRequestValid(bid)).to.equal(false); + + bid.params.video = 123; + expect(spec.isBidRequestValid(bid)).to.equal(false); + + bid.params.video = false; + expect(spec.isBidRequestValid(bid)).to.equal(false); + + bid.params.video = void (0); + expect(spec.isBidRequestValid(bid)).to.equal(false); + }); + + it('should fail if rendererSettings is specified but is not an object', () => { + const bid = deepClone(baseValidBid); + + bid.params.rendererSettings = null; + expect(spec.isBidRequestValid(bid)).to.equal(false); + + bid.params.rendererSettings = 'string'; + expect(spec.isBidRequestValid(bid)).to.equal(false); + + bid.params.rendererSettings = 123; + expect(spec.isBidRequestValid(bid)).to.equal(false); + + bid.params.rendererSettings = false; + expect(spec.isBidRequestValid(bid)).to.equal(false); + + bid.params.rendererSettings = void (0); + expect(spec.isBidRequestValid(bid)).to.equal(false); + }); }); describe('buildRequests', () => { @@ -510,30 +555,64 @@ describe('BlueBillywigAdapter', () => { expect(deepAccess(payload, 'user.ext.eids')).to.be.undefined; }); - it('should set digitrust when present on bid', () => { - const digiTrust = {data: {id: 'DTID', keyv: 4, privacy: {optout: false}, producer: 'ABC', version: 2}}; + it('should set imp.0.video.[w|h|placement] by default', () => { + const newBaseValidBidRequests = deepClone(baseValidBidRequests); + + const request = spec.buildRequests(newBaseValidBidRequests, validBidderRequest); + const payload = JSON.parse(request.data); + + expect(deepAccess(payload, 'imp.0.video.w')).to.equal(768); + expect(deepAccess(payload, 'imp.0.video.h')).to.equal(432); + expect(deepAccess(payload, 'imp.0.video.placement')).to.equal(3); + }); + it('should update imp0.video.[w|h] when present in config', () => { const newBaseValidBidRequests = deepClone(baseValidBidRequests); - newBaseValidBidRequests[0].userId = { digitrustid: digiTrust }; + newBaseValidBidRequests[0].mediaTypes.video.playerSize = [1, 1]; const request = spec.buildRequests(newBaseValidBidRequests, validBidderRequest); const payload = JSON.parse(request.data); - expect(payload).to.have.nested.property('user.ext.digitrust'); - expect(payload.user.ext.digitrust.id).to.equal(digiTrust.data.id); - expect(payload.user.ext.digitrust.keyv).to.equal(digiTrust.data.keyv); + expect(deepAccess(payload, 'imp.0.video.w')).to.equal(1); + expect(deepAccess(payload, 'imp.0.video.h')).to.equal(1); }); - it('should not set digitrust when opted out', () => { - const digiTrust = {data: {id: 'DTID', keyv: 4, privacy: {optout: true}, producer: 'ABC', version: 2}}; + it('should allow overriding any imp0.video key through params.video', () => { + const newBaseValidBidRequests = deepClone(baseValidBidRequests); + newBaseValidBidRequests[0].params.video = { + w: 2, + h: 2, + placement: 1, + minduration: 15, + maxduration: 30 + }; + + const request = spec.buildRequests(newBaseValidBidRequests, validBidderRequest); + const payload = JSON.parse(request.data); + + expect(deepAccess(payload, 'imp.0.video.w')).to.equal(2); + expect(deepAccess(payload, 'imp.0.video.h')).to.equal(2); + expect(deepAccess(payload, 'imp.0.video.placement')).to.equal(1); + expect(deepAccess(payload, 'imp.0.video.minduration')).to.equal(15); + expect(deepAccess(payload, 'imp.0.video.maxduration')).to.equal(30); + }); + it('should not allow placing any non-OpenRTB 2.5 keys on imp.0.video through params.video', () => { const newBaseValidBidRequests = deepClone(baseValidBidRequests); - newBaseValidBidRequests[0].userId = { digitrustid: digiTrust }; + newBaseValidBidRequests[0].params.video = { + 'true': true, + 'testing': 'some', + 123: {}, + '': 'values' + }; const request = spec.buildRequests(newBaseValidBidRequests, validBidderRequest); const payload = JSON.parse(request.data); - expect(deepAccess(payload, 'user.ext.digitrust')).to.be.undefined; + expect(deepAccess(request, 'imp.0.video.true')).to.be.undefined; + expect(deepAccess(payload, 'imp.0.video.testing')).to.be.undefined; + expect(deepAccess(payload, 'imp.0.video.123')).to.be.undefined; + expect(deepAccess(payload, 'imp.0.video.')).to.be.undefined; }); }); describe('interpretResponse', () => { @@ -571,7 +650,7 @@ describe('BlueBillywigAdapter', () => { bidder: BB_CONSTANTS.BIDDER_CODE, bidderRequestId: '1a2345b67c8d9e0', params: baseValidBid.params, - sizes: [[768, 432], [640, 480], [630, 360]], + sizes: [[640, 480], [630, 360]], transactionId: '2b34c5de-f67a-8901-bcd2-34567efabc89' }], start: 11585918458869, @@ -759,6 +838,101 @@ describe('BlueBillywigAdapter', () => { expect(result.length).to.equal(0); } }); + + it('should take default width and height when w/h not present', () => { + const bidSizesMissing = deepClone(serverResponse); + + delete bidSizesMissing.body.seatbid[0].bid[0].w; + delete bidSizesMissing.body.seatbid[0].bid[0].h; + + const response = bidSizesMissing; + const request = spec.buildRequests(baseValidBidRequests, validBidderRequest); + const result = spec.interpretResponse(response, request); + + expect(deepAccess(result, '0.width')).to.equal(768); + expect(deepAccess(result, '0.height')).to.equal(432); + }); + + it('should take nurl value when adm not present', () => { + const bidAdmMissing = deepClone(serverResponse); + + delete bidAdmMissing.body.seatbid[0].bid[0].adm; + bidAdmMissing.body.seatbid[0].bid[0].nurl = 'https://bluebillywig.com'; + + const response = bidAdmMissing; + const request = spec.buildRequests(baseValidBidRequests, validBidderRequest); + const result = spec.interpretResponse(response, request); + + expect(deepAccess(result, '0.vastXml')).to.be.undefined; + expect(deepAccess(result, '0.vastUrl')).to.equal('https://bluebillywig.com'); + }); + + it('should not take nurl value when adm present', () => { + const bidAdmNurlPresent = deepClone(serverResponse); + + bidAdmNurlPresent.body.seatbid[0].bid[0].nurl = 'https://bluebillywig.com'; + + const response = bidAdmNurlPresent; + const request = spec.buildRequests(baseValidBidRequests, validBidderRequest); + const result = spec.interpretResponse(response, request); + + expect(deepAccess(result, '0.vastXml')).to.equal(bidAdmNurlPresent.body.seatbid[0].bid[0].adm); + expect(deepAccess(result, '0.vastUrl')).to.be.undefined; + }); + + it('should take ext.prebid.cache data when present, ignore ext.prebid.targeting and nurl', () => { + const bidExtPrebidCache = deepClone(serverResponse); + + delete bidExtPrebidCache.body.seatbid[0].bid[0].adm; + bidExtPrebidCache.body.seatbid[0].bid[0].nurl = 'https://notnurl.com'; + + bidExtPrebidCache.body.seatbid[0].bid[0].ext = { + prebid: { + cache: { + vastXml: { + url: 'https://bluebillywig.com', + cacheId: '12345' + } + }, + targeting: { + hb_uuid: '23456', + hb_cache_host: 'bluebillywig.com', + hb_cache_path: '/cache' + } + } + }; + + const response = bidExtPrebidCache; + const request = spec.buildRequests(baseValidBidRequests, validBidderRequest); + const result = spec.interpretResponse(response, request); + + expect(deepAccess(result, '0.vastUrl')).to.equal('https://bluebillywig.com'); + expect(deepAccess(result, '0.videoCacheKey')).to.equal('12345'); + }); + + it('should take ext.prebid.targeting data when ext.prebid.cache not present, and ignore nurl', () => { + const bidExtPrebidTargeting = deepClone(serverResponse); + + delete bidExtPrebidTargeting.body.seatbid[0].bid[0].adm; + bidExtPrebidTargeting.body.seatbid[0].bid[0].nurl = 'https://notnurl.com'; + + bidExtPrebidTargeting.body.seatbid[0].bid[0].ext = { + prebid: { + targeting: { + hb_uuid: '34567', + hb_cache_host: 'bluebillywig.com', + hb_cache_path: '/cache' + } + } + }; + + const response = bidExtPrebidTargeting; + const request = spec.buildRequests(baseValidBidRequests, validBidderRequest); + const result = spec.interpretResponse(response, request); + + expect(deepAccess(result, '0.vastUrl')).to.equal('https://bluebillywig.com/cache?uuid=34567'); + expect(deepAccess(result, '0.videoCacheKey')).to.equal('34567'); + }); }); describe('getUserSyncs', () => { const publicationName = 'bbprebid.dev'; diff --git a/test/spec/modules/bridgewellBidAdapter_spec.js b/test/spec/modules/bridgewellBidAdapter_spec.js index 644f468abe8..0860404355e 100644 --- a/test/spec/modules/bridgewellBidAdapter_spec.js +++ b/test/spec/modules/bridgewellBidAdapter_spec.js @@ -22,6 +22,16 @@ describe('bridgewellBidAdapter', function () { expect(spec.isBidRequestValid(validTag)).to.equal(true); }); + it('should return true when required params found', function () { + const validTag = { + 'bidder': 'bridgewell', + 'params': { + 'cid': 1234 + }, + }; + expect(spec.isBidRequestValid(validTag)).to.equal(true); + }); + it('should return false when required params not found', function () { const invalidTag = { 'bidder': 'bridgewell', @@ -39,6 +49,26 @@ describe('bridgewellBidAdapter', function () { }; expect(spec.isBidRequestValid(invalidTag)).to.equal(false); }); + + it('should return false when required params are empty', function () { + const invalidTag = { + 'bidder': 'bridgewell', + 'params': { + 'cid': '', + }, + }; + expect(spec.isBidRequestValid(invalidTag)).to.equal(false); + }); + + it('should return false when required param cid is not a number', function () { + const invalidTag = { + 'bidder': 'bridgewell', + 'params': { + 'cid': 'bad_cid', + }, + }; + expect(spec.isBidRequestValid(invalidTag)).to.equal(false); + }); }); describe('buildRequests', function () { @@ -113,12 +143,58 @@ describe('bridgewellBidAdapter', function () { expect(payload.url).to.exist.and.to.equal('https://www.bridgewell.com/'); for (let i = 0, max_i = payload.adUnits.length; i < max_i; i++) { expect(payload.adUnits[i]).to.have.property('ChannelID').that.is.a('string'); + expect(payload.adUnits[i]).to.not.have.property('cid'); + expect(payload.adUnits[i]).to.have.property('adUnitCode').and.to.equal('adunit-code-2'); + expect(payload.adUnits[i]).to.have.property('requestId').and.to.equal('3150ccb55da321'); + } + }); + + it('should attach valid params to the tag, part2', function() { + const bidderRequest = { + refererInfo: { + referer: 'https://www.bridgewell.com/' + } + } + const bidRequests2 = [ + { + 'bidder': 'bridgewell', + 'params': { + 'cid': 1234, + }, + 'adUnitCode': 'adunit-code-2', + 'mediaTypes': { + 'banner': { + 'sizes': [728, 90] + } + }, + 'bidId': '3150ccb55da321', + 'bidderRequestId': '22edbae2733bf6', + 'auctionId': '1d1a030790a475', + }, + ]; + + const request = spec.buildRequests(bidRequests2, bidderRequest); + const payload = request.data; + + expect(payload).to.be.an('object'); + expect(payload.adUnits).to.be.an('array'); + expect(payload.url).to.exist.and.to.equal('https://www.bridgewell.com/'); + for (let i = 0, max_i = payload.adUnits.length; i < max_i; i++) { + expect(payload.adUnits[i]).to.have.property('cid').that.is.a('number'); + expect(payload.adUnits[i]).to.not.have.property('ChannelID'); expect(payload.adUnits[i]).to.have.property('adUnitCode').and.to.equal('adunit-code-2'); + expect(payload.adUnits[i]).to.have.property('requestId').and.to.equal('3150ccb55da321'); } }); it('should attach validBidRequests to the tag', function () { - const request = spec.buildRequests(bidRequests); + const bidderRequest = { + refererInfo: { + referer: 'https://www.bridgewell.com/' + } + } + + const request = spec.buildRequests(bidRequests, bidderRequest); const validBidRequests = request.validBidRequests; expect(validBidRequests).to.deep.equal(bidRequests); }); @@ -168,6 +244,7 @@ describe('bridgewellBidAdapter', function () { }, ] }; + const nativeServerResponses = [ { 'id': '0e4048d3-5c74-4380-a21a-00ba35629f7d', @@ -175,6 +252,7 @@ describe('bridgewellBidAdapter', function () { 'cpm': 7.0, 'width': 1, 'height': 1, + 'adomain': ['response.com'], 'mediaType': 'native', 'native': { 'image': { @@ -199,6 +277,7 @@ describe('bridgewellBidAdapter', function () { 'currency': 'NTD' }, ]; + const bannerBidRequests = { validBidRequests: [ { @@ -211,6 +290,7 @@ describe('bridgewellBidAdapter', function () { }, ] }; + const bannerServerResponses = [ { 'id': 'e5b10774-32bf-4931-85ee-05095e8cff21', @@ -218,6 +298,7 @@ describe('bridgewellBidAdapter', function () { 'cpm': 5.0, 'width': 300, 'height': 250, + 'adomain': ['response.com'], 'mediaType': 'banner', 'ad': '
test 300x250
', 'ttl': 360, @@ -239,6 +320,7 @@ describe('bridgewellBidAdapter', function () { expect(result[0].currency).to.equal('NTD'); expect(result[0].mediaType).to.equal('native'); expect(result[0].native.image.url).to.equal('https://img.scupio.com/test/test-image.jpg'); + expect(String(result[0].meta.advertiserDomains)).to.equal('response.com'); }); it('should return all required parameters banner', function () { @@ -254,6 +336,7 @@ describe('bridgewellBidAdapter', function () { expect(result[0].currency).to.equal('NTD'); expect(result[0].mediaType).to.equal('banner'); expect(result[0].ad).to.equal('
test 300x250
'); + expect(String(result[0].meta.advertiserDomains)).to.equal('response.com'); }); it('should give up bid if server response is undefiend', function () { diff --git a/test/spec/modules/brightMountainMediaBidAdapter_spec.js b/test/spec/modules/brightMountainMediaBidAdapter_spec.js index 3b7e46e55a7..6c7ef816f4f 100644 --- a/test/spec/modules/brightMountainMediaBidAdapter_spec.js +++ b/test/spec/modules/brightMountainMediaBidAdapter_spec.js @@ -1,13 +1,18 @@ import { expect } from 'chai'; import { spec } from '../../../modules/brightMountainMediaBidAdapter.js'; +const BIDDER_CODE = 'bmtm'; +const ENDPOINT_URL = 'https://one.elitebidder.com/api/hb'; +const ENDPOINT_URL_SYNC = 'https://console.brightmountainmedia.com:8443/cookieSync'; +const PLACEMENT_ID = 329; + describe('brightMountainMediaBidAdapter_spec', function () { - let bid = { + let bidBanner = { bidId: '2dd581a2b6281d', - bidder: 'brightmountainmedia', + bidder: BIDDER_CODE, bidderRequestId: '145e1d6a7837c9', params: { - placement_id: '123qwerty' + placement_id: PLACEMENT_ID }, placementCode: 'placementid_0', auctionId: '74f78609-a92d-4cf1-869f-1b244bbfb5d2', @@ -18,8 +23,30 @@ describe('brightMountainMediaBidAdapter_spec', function () { }, transactionId: '3bb2f6da-87a6-4029-aeb0-bfe951372e62', }; + + let bidVideo = { + bidId: '2dd581a2b6281d', + bidder: BIDDER_CODE, + bidderRequestId: '145e1d6a7837c9', + params: { + placement_id: PLACEMENT_ID + }, + placementCode: 'placementid_0', + auctionId: '74f78609-a92d-4cf1-869f-1b244bbfb5d2', + mediaTypes: { + video: { + playerSizes: [[300, 250]], + context: 'outstream', + skip: 0, + playbackmethod: [1, 2], + mimes: ['video/mp4'] + } + }, + transactionId: '3bb2f6da-87a6-4029-aeb0-bfe951372e62', + }; + let bidderRequest = { - bidderCode: 'brightmountainmedia', + bidderCode: BIDDER_CODE, auctionId: 'fffffff-ffff-ffff-ffff-ffffffffffff', bidderRequestId: 'ffffffffffffff', start: 1472239426002, @@ -29,22 +56,20 @@ describe('brightMountainMediaBidAdapter_spec', function () { refererInfo: { referer: 'http://www.example.com', reachedTop: true, - }, - bids: [bid] - } + } + }; describe('isBidRequestValid', function () { it('Should return true when when required params found', function () { - expect(spec.isBidRequestValid(bid)).to.be.true; + expect(spec.isBidRequestValid(bidBanner)).to.be.true; }); it('Should return false when required params are not passed', function () { - bid.params = {} - expect(spec.isBidRequestValid(bid)).to.be.false; + bidBanner.params = {} + expect(spec.isBidRequestValid(bidBanner)).to.be.false; }); }); - describe('buildRequests', function () { - let serverRequest = spec.buildRequests([bid], bidderRequest); + function testServerRequestBody(serverRequest) { it('Creates a ServerRequest object with method, URL and data', function () { expect(serverRequest).to.exist; expect(serverRequest.method).to.exist; @@ -55,7 +80,7 @@ describe('brightMountainMediaBidAdapter_spec', function () { expect(serverRequest.method).to.equal('POST'); }); it('Returns valid URL', function () { - expect(serverRequest.url).to.equal('https://console.brightmountainmedia.com/hb/bid'); + expect(serverRequest.url).to.equal(ENDPOINT_URL); }); it('Returns valid data if array of bids is valid', function () { @@ -69,43 +94,31 @@ describe('brightMountainMediaBidAdapter_spec', function () { expect(data.host).to.be.a('string'); expect(data.page).to.be.a('string'); let placements = data['placements']; - for (let i = 0; i < placements.length; i++) { - let placement = placements[i]; - expect(placement).to.have.all.keys('placementId', 'bidId', 'traffic', 'sizes'); - expect(placement.placementId).to.be.a('string'); - expect(placement.bidId).to.be.a('string'); - expect(placement.traffic).to.be.a('string'); - expect(placement.sizes).to.be.an('array'); - } + expect(placements).to.be.an('array'); }); + } + + describe('buildRequests', function () { + bidderRequest['bids'] = [bidBanner]; + let serverRequest = spec.buildRequests([bidBanner], bidderRequest); + testServerRequestBody(serverRequest); + + bidderRequest['bids'] = [bidVideo]; + serverRequest = spec.buildRequests([bidVideo], bidderRequest); + testServerRequestBody(serverRequest); + 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 () { - let resObject = { - body: [{ - requestId: '123', - mediaType: 'banner', - cpm: 0.3, - width: 320, - height: 50, - ad: '

Hello ad

', - ttl: 1000, - creativeId: '123asd', - netRevenue: true, - currency: 'USD' - }] - }; - let serverResponses = spec.interpretResponse(resObject); + + function testServerResponse(serverResponses) { it('Returns an array of valid server responses if response object is valid', function () { expect(serverResponses).to.be.an('array').that.is.not.empty; for (let i = 0; i < serverResponses.length; i++) { let dataItem = serverResponses[i]; - expect(dataItem).to.have.all.keys('requestId', 'cpm', 'width', 'height', 'ad', 'ttl', 'creativeId', - 'netRevenue', 'currency', 'mediaType'); expect(dataItem.requestId).to.be.a('string'); expect(dataItem.cpm).to.be.a('number'); expect(dataItem.width).to.be.a('number'); @@ -117,10 +130,48 @@ describe('brightMountainMediaBidAdapter_spec', function () { expect(dataItem.currency).to.be.a('string'); expect(dataItem.mediaType).to.be.a('string'); } - it('Returns an empty array if invalid response is passed', function () { - serverResponses = spec.interpretResponse('invalid_response'); - expect(serverResponses).to.be.an('array').that.is.empty; - }); + }); + } + + describe('interpretResponse', function () { + let resObjectBanner = { + body: [{ + requestId: '123', + mediaType: 'banner', + cpm: 0.3, + width: 320, + height: 50, + ad: '

Hello ad

', + ttl: 1000, + creativeId: '123asd', + netRevenue: true, + currency: 'USD' + }] + }; + + let resObjectVideo = { + body: [{ + requestId: '123', + mediaType: 'video', + cpm: 1.5, + width: 320, + height: 50, + ad: '

Hello ad

', + ttl: 1000, + creativeId: '123asd', + netRevenue: true, + currency: 'USD' + }] + }; + let serverResponses = spec.interpretResponse(resObjectBanner); + testServerResponse(serverResponses); + + serverResponses = spec.interpretResponse(resObjectVideo); + testServerResponse(serverResponses); + + it('Returns an empty array if invalid response is passed', function () { + serverResponses = spec.interpretResponse('invalid_response'); + expect(serverResponses).to.be.an('array').that.is.empty; }); }); @@ -133,7 +184,7 @@ describe('brightMountainMediaBidAdapter_spec', function () { expect(spec.getUserSyncs(syncoptionsIframe)[0].type).to.exist; expect(spec.getUserSyncs(syncoptionsIframe)[0].url).to.exist; expect(spec.getUserSyncs(syncoptionsIframe)[0].type).to.equal('iframe') - expect(spec.getUserSyncs(syncoptionsIframe)[0].url).to.equal('https://console.brightmountainmedia.com:4444/cookieSync') + expect(spec.getUserSyncs(syncoptionsIframe)[0].url).to.equal(ENDPOINT_URL_SYNC) }); }); }); diff --git a/test/spec/modules/britepoolIdSystem_spec.js b/test/spec/modules/britepoolIdSystem_spec.js index 2c6dd234a90..ddb61806006 100644 --- a/test/spec/modules/britepoolIdSystem_spec.js +++ b/test/spec/modules/britepoolIdSystem_spec.js @@ -1,5 +1,5 @@ -import { expect } from 'chai'; import {britepoolIdSubmodule} from 'modules/britepoolIdSystem.js'; +import * as utils from '../../../src/utils.js'; describe('BritePool Submodule', () => { const api_key = '1111'; @@ -16,6 +16,32 @@ describe('BritePool Submodule', () => { }; }; + let triggerPixelStub; + + beforeEach(function (done) { + triggerPixelStub = sinon.stub(utils, 'triggerPixel'); + done(); + }); + + afterEach(function () { + triggerPixelStub.restore(); + }); + + it('trigger id resolution pixel when no identifiers set', () => { + britepoolIdSubmodule.getId({ params: {} }); + expect(triggerPixelStub.called).to.be.true; + }); + + it('trigger id resolution pixel when no identifiers set with api_key param', () => { + britepoolIdSubmodule.getId({ params: { api_key } }); + expect(triggerPixelStub.called).to.be.true; + }); + + it('does not trigger id resolution pixel when identifiers set', () => { + britepoolIdSubmodule.getId({ params: { api_key, aaid } }); + expect(triggerPixelStub.called).to.be.false; + }); + it('sends x-api-key in header and one identifier', () => { const { params, headers, url, errors } = britepoolIdSubmodule.createParams({ api_key, aaid }); assert(errors.length === 0, errors); @@ -43,12 +69,48 @@ describe('BritePool Submodule', () => { expect(params.url).to.be.undefined; }); + it('test gdpr consent string in url', () => { + const { params, headers, url, errors } = britepoolIdSubmodule.createParams({ api_key, aaid }, { gdprApplies: true, consentString: 'expectedConsentString' }); + expect(url).to.equal('https://api.britepool.com/v1/britepool/id?gdprString=expectedConsentString'); + }); + + it('test gdpr consent string not in url if gdprApplies false', () => { + const { params, headers, url, errors } = britepoolIdSubmodule.createParams({ api_key, aaid }, { gdprApplies: false, consentString: 'expectedConsentString' }); + expect(url).to.equal('https://api.britepool.com/v1/britepool/id'); + }); + + it('test gdpr consent string not in url if consent string undefined', () => { + const { params, headers, url, errors } = britepoolIdSubmodule.createParams({ api_key, aaid }, { gdprApplies: true, consentString: undefined }); + expect(url).to.equal('https://api.britepool.com/v1/britepool/id'); + }); + + it('dynamic pub params should be added to params', () => { + window.britepool_pubparams = { ppid: '12345' }; + const { params, headers, url, errors } = britepoolIdSubmodule.createParams({ api_key, aaid }); + expect(params).to.eql({ aaid, ppid: '12345' }); + window.britepool_pubparams = undefined; + }); + + it('dynamic pub params should override submodule params', () => { + window.britepool_pubparams = { ppid: '67890' }; + const { params, headers, url, errors } = britepoolIdSubmodule.createParams({ api_key, ppid: '12345' }); + expect(params).to.eql({ ppid: '67890' }); + window.britepool_pubparams = undefined; + }); + + it('if dynamic pub params undefined do nothing', () => { + window.britepool_pubparams = undefined; + const { params, headers, url, errors } = britepoolIdSubmodule.createParams({ api_key, aaid }); + expect(params).to.eql({ aaid }); + window.britepool_pubparams = undefined; + }); + it('test getter override with value', () => { const { params, headers, url, getter, errors } = britepoolIdSubmodule.createParams({ api_key, aaid, url: url_override, getter: getter_override }); expect(getter).to.equal(getter_override); // Making sure it did not become part of params expect(params.getter).to.be.undefined; - const response = britepoolIdSubmodule.getId({ api_key, aaid, url: url_override, getter: getter_override }); + const response = britepoolIdSubmodule.getId({ params: { api_key, aaid, url: url_override, getter: getter_override } }); assert.deepEqual(response, { id: { 'primaryBPID': bpid } }); }); @@ -57,7 +119,7 @@ describe('BritePool Submodule', () => { expect(getter).to.equal(getter_callback_override); // Making sure it did not become part of params expect(params.getter).to.be.undefined; - const response = britepoolIdSubmodule.getId({ api_key, aaid, url: url_override, getter: getter_callback_override }); + const response = britepoolIdSubmodule.getId({ params: { api_key, aaid, url: url_override, getter: getter_callback_override } }); expect(response.callback).to.not.be.undefined; response.callback(result => { assert.deepEqual(result, { 'primaryBPID': bpid }); diff --git a/test/spec/modules/browsiRtdProvider_spec.js b/test/spec/modules/browsiRtdProvider_spec.js new file mode 100644 index 00000000000..ee37d16905b --- /dev/null +++ b/test/spec/modules/browsiRtdProvider_spec.js @@ -0,0 +1,83 @@ +import * as browsiRTD from '../../../modules/browsiRtdProvider.js'; +import {makeSlot} from '../integration/faker/googletag.js'; + +describe('browsi Real time data sub module', function () { + const conf = { + 'auctionDelay': 250, + dataProviders: [{ + 'name': 'browsi', + 'params': { + 'url': 'testUrl.com', + 'siteKey': 'testKey', + 'pubKey': 'testPub', + 'keyName': 'bv' + } + }] + }; + + it('should init and return true', function () { + browsiRTD.collectData(); + expect(browsiRTD.browsiSubmodule.init(conf.dataProviders[0])).to.equal(true) + }); + + it('should create browsi script', function () { + const script = browsiRTD.addBrowsiTag('scriptUrl.com'); + expect(script.getAttribute('data-sitekey')).to.equal('testKey'); + expect(script.getAttribute('data-pubkey')).to.equal('testPub'); + expect(script.async).to.equal(true); + expect(script.prebidData.kn).to.equal(conf.dataProviders[0].params.keyName); + }); + + it('should match placement with ad unit', function () { + const slot = makeSlot({code: '/57778053/Browsi_Demo_300x250', divId: 'browsiAd_1'}); + + const test1 = browsiRTD.isIdMatchingAdUnit(slot, ['/57778053/Browsi_Demo_300x250']); // true + const test2 = browsiRTD.isIdMatchingAdUnit(slot, ['/57778053/Browsi_Demo_300x250', '/57778053/Browsi']); // true + const test3 = browsiRTD.isIdMatchingAdUnit(slot, ['/57778053/Browsi_Demo_Low']); // false + const test4 = browsiRTD.isIdMatchingAdUnit(slot, []); // true + + expect(test1).to.equal(true); + expect(test2).to.equal(true); + expect(test3).to.equal(false); + expect(test4).to.equal(true); + }); + + it('should return correct macro values', function () { + const slot = makeSlot({code: '/57778053/Browsi_Demo_300x250', divId: 'browsiAd_1'}); + + slot.setTargeting('test', ['test', 'value']); + // slot getTargeting doesn't act like GPT so we can't expect real value + const macroResult = browsiRTD.getMacroId({p: '/'}, slot); + expect(macroResult).to.equal('/57778053/Browsi_Demo_300x250/NA'); + + const macroResultB = browsiRTD.getMacroId({}, slot); + expect(macroResultB).to.equal('browsiAd_1'); + + const macroResultC = browsiRTD.getMacroId({p: '', s: {s: 0, e: 1}}, slot); + expect(macroResultC).to.equal('/'); + }); + + describe('should return data to RTD module', function () { + it('should return empty if no ad units defined', function () { + browsiRTD.setData({}); + expect(browsiRTD.browsiSubmodule.getTargetingData([])).to.eql({}); + }); + + it('should return NA if no prediction for ad unit', function () { + makeSlot({code: 'adMock', divId: 'browsiAd_2'}); + browsiRTD.setData({}); + expect(browsiRTD.browsiSubmodule.getTargetingData(['adMock'])).to.eql({adMock: {bv: 'NA'}}); + }); + + it('should return prediction from server', function () { + makeSlot({code: 'hasPrediction', divId: 'hasPrediction'}); + const data = { + p: {'hasPrediction': {p: 0.234}}, + kn: 'bv', + pmd: undefined + }; + browsiRTD.setData(data); + expect(browsiRTD.browsiSubmodule.getTargetingData(['hasPrediction'])).to.eql({hasPrediction: {bv: '0.20'}}); + }) + }) +}); diff --git a/test/spec/modules/bucksenseBidAdapter_spec.js b/test/spec/modules/bucksenseBidAdapter_spec.js index f49a63d2003..b977c3a9dd1 100644 --- a/test/spec/modules/bucksenseBidAdapter_spec.js +++ b/test/spec/modules/bucksenseBidAdapter_spec.js @@ -128,7 +128,10 @@ describe('Bucksense Adapter', function() { 'creativeId': 'creative002', 'currency': 'USD', 'netRevenue': false, - 'ad': '
' + 'ad': '
', + 'meta': { + 'advertiserDomains': ['http://www.bucksense.com/'] + } } }; }); diff --git a/test/spec/modules/ccxBidAdapter_spec.js b/test/spec/modules/ccxBidAdapter_spec.js index f14612629b1..d346a14d38a 100644 --- a/test/spec/modules/ccxBidAdapter_spec.js +++ b/test/spec/modules/ccxBidAdapter_spec.js @@ -337,7 +337,10 @@ describe('ccxAdapter', function () { netRevenue: false, ttl: 5, currency: 'PLN', - ad: '' + ad: '', + meta: { + advertiserDomains: ['clickonometrics.com'] + } }, { requestId: '2e56e1af51a5d8', @@ -348,7 +351,10 @@ describe('ccxAdapter', function () { netRevenue: false, ttl: 5, currency: 'PLN', - vastXml: '' + vastXml: '', + meta: { + advertiserDomains: ['clickonometrics.com'] + } } ]; expect(spec.interpretResponse({body: response})).to.deep.have.same.members(bidResponses); @@ -366,7 +372,10 @@ describe('ccxAdapter', function () { netRevenue: false, ttl: 5, currency: 'PLN', - ad: '' + ad: '', + meta: { + advertiserDomains: ['clickonometrics.com'] + } } ]; expect(spec.interpretResponse({body: response})).to.deep.have.same.members(bidResponses); @@ -425,4 +434,58 @@ describe('ccxAdapter', function () { expect(spec.getUserSyncs(syncOptions, [{body: response}])).to.be.empty; }); }); + describe('mediaTypesVideoParams', function () { + it('Valid video mediaTypes', function () { + let bids = [ + { + adUnitCode: 'video', + auctionId: '0b9de793-8eda-481e-a548-c187d58b28d9', + bidId: '3u94t90ut39tt3t', + bidder: 'ccx', + bidderRequestId: '23ur20r239r2r', + mediaTypes: { + video: { + playerSize: [[640, 480]], + protocols: [2, 3, 5, 6], + mimes: ['video/mp4', 'video/x-flv'], + playbackmethod: [1, 2, 3, 4], + skip: 1, + skipafter: 5 + } + }, + params: { + placementId: 608 + }, + sizes: [[640, 480]], + transactionId: 'aefddd38-cfa0-48ab-8bdd-325de4bab5f9' + } + ]; + + let imps = [ + { + video: { + w: 640, + h: 480, + protocols: [2, 3, 5, 6], + mimes: ['video/mp4', 'video/x-flv'], + playbackmethod: [1, 2, 3, 4], + skip: 1, + skipafter: 5 + }, + id: '3u94t90ut39tt3t', + secure: 1, + ext: { + pid: 608 + } + } + ]; + + let bidsClone = utils.deepClone(bids); + + let response = spec.buildRequests(bidsClone, {'bids': bidsClone}); + let data = JSON.parse(response.data); + + expect(data.imp).to.deep.have.same.members(imps); + }); + }); }); diff --git a/test/spec/modules/cointrafficBidAdapter_spec.js b/test/spec/modules/cointrafficBidAdapter_spec.js new file mode 100644 index 00000000000..a2ce4cedea0 --- /dev/null +++ b/test/spec/modules/cointrafficBidAdapter_spec.js @@ -0,0 +1,261 @@ +import { expect } from 'chai'; +import { spec } from 'modules/cointrafficBidAdapter.js'; +import { config } from 'src/config.js' +import * as utils from 'src/utils.js' + +const ENDPOINT_URL = 'https://appspb.cointraffic.io/pb/tmp'; + +describe('cointrafficBidAdapter', function () { + describe('isBidRequestValid', function () { + let bid = { + bidder: 'cointraffic', + params: { + placementId: 'testPlacementId' + }, + adUnitCode: 'adunit-code', + sizes: [ + [300, 250] + ], + bidId: 'bidId12345', + bidderRequestId: 'bidderRequestId12345', + auctionId: 'auctionId12345' + }; + + it('should return true where required params found', function () { + expect(spec.isBidRequestValid(bid)).to.equal(true); + }); + }); + + describe('buildRequests', function () { + let bidRequests = [ + { + bidder: 'cointraffic', + params: { + placementId: 'testPlacementId' + }, + adUnitCode: 'adunit-code', + sizes: [ + [300, 250] + ], + bidId: 'bidId12345', + bidderRequestId: 'bidderRequestId12345', + auctionId: 'auctionId12345' + }, + { + bidder: 'cointraffic', + params: { + placementId: 'testPlacementId' + }, + adUnitCode: 'adunit-code2', + sizes: [ + [300, 250] + ], + bidId: 'bidId67890"', + bidderRequestId: 'bidderRequestId67890', + auctionId: 'auctionId12345' + } + ]; + + let bidderRequests = { + refererInfo: { + numIframes: 0, + reachedTop: true, + referer: 'https://example.com', + stack: [ + 'https://example.com' + ] + } + }; + + it('replaces currency with EUR if there is no currency provided', function () { + const request = spec.buildRequests(bidRequests, bidderRequests); + + expect(request[0].data.currency).to.equal('EUR'); + expect(request[1].data.currency).to.equal('EUR'); + }); + + it('replaces currency with EUR if there is no currency provided', function () { + const getConfigStub = sinon.stub(config, 'getConfig').callsFake( + arg => arg === 'currency.bidderCurrencyDefault.cointraffic' ? 'USD' : 'EUR' + ); + + const request = spec.buildRequests(bidRequests, bidderRequests); + + expect(request[0].data.currency).to.equal('USD'); + expect(request[1].data.currency).to.equal('USD'); + + getConfigStub.restore(); + }); + + it('throws an error if currency provided in params is not allowed', function () { + const utilsMock = sinon.mock(utils).expects('logError').twice() + const getConfigStub = sinon.stub(config, 'getConfig').callsFake( + arg => arg === 'currency.bidderCurrencyDefault.cointraffic' ? 'BTC' : 'EUR' + ); + + const request = spec.buildRequests(bidRequests, bidderRequests); + + expect(request[0]).to.undefined; + expect(request[1]).to.undefined; + + utilsMock.restore() + getConfigStub.restore(); + }); + + it('sends bid request to our endpoint via POST', function () { + const request = spec.buildRequests(bidRequests, bidderRequests); + + expect(request[0].method).to.equal('POST'); + expect(request[1].method).to.equal('POST'); + }); + + it('attaches source and version to endpoint URL as query params', function () { + const request = spec.buildRequests(bidRequests, bidderRequests); + + expect(request[0].url).to.equal(ENDPOINT_URL); + expect(request[1].url).to.equal(ENDPOINT_URL); + }); + }); + + describe('interpretResponse', function () { + it('should get the correct bid response', function () { + let bidRequest = [{ + method: 'POST', + url: ENDPOINT_URL, + data: { + placementId: 'testPlacementId', + device: 'desktop', + currency: 'EUR', + sizes: ['300x250'], + bidId: 'bidId12345', + referer: 'www.example.com' + } + }]; + + let serverResponse = { + body: { + requestId: 'bidId12345', + cpm: 3.9, + currency: 'EUR', + netRevenue: true, + width: 300, + height: 250, + creativeId: 'creativeId12345', + ttl: 90, + ad: '

I am an ad

' + } + }; + + let expectedResponse = [{ + requestId: 'bidId12345', + cpm: 3.9, + currency: 'EUR', + netRevenue: true, + width: 300, + height: 250, + creativeId: 'creativeId12345', + ttl: 90, + ad: '

I am an ad

' + }]; + + let result = spec.interpretResponse(serverResponse, bidRequest[0]); + expect(Object.keys(result)).to.deep.equal(Object.keys(expectedResponse)); + }); + + it('should get the correct bid response with different currency', function () { + let bidRequest = [{ + method: 'POST', + url: ENDPOINT_URL, + data: { + placementId: 'testPlacementId', + device: 'desktop', + currency: 'USD', + sizes: ['300x250'], + bidId: 'bidId12345', + referer: 'www.example.com' + } + }]; + + let serverResponse = { + body: { + requestId: 'bidId12345', + cpm: 3.9, + currency: 'USD', + netRevenue: true, + width: 300, + height: 250, + creativeId: 'creativeId12345', + ttl: 90, + ad: '

I am an ad

' + } + }; + + let expectedResponse = [{ + requestId: 'bidId12345', + cpm: 3.9, + currency: 'USD', + netRevenue: true, + width: 300, + height: 250, + creativeId: 'creativeId12345', + ttl: 90, + ad: '

I am an ad

' + }]; + + const getConfigStub = sinon.stub(config, 'getConfig').returns('USD'); + + let result = spec.interpretResponse(serverResponse, bidRequest[0]); + expect(Object.keys(result)).to.deep.equal(Object.keys(expectedResponse)); + + getConfigStub.restore(); + }); + + it('should get empty bid response requested currency is not available', function () { + let bidRequest = [{ + method: 'POST', + url: ENDPOINT_URL, + data: { + placementId: 'testPlacementId', + device: 'desktop', + currency: 'BTC', + sizes: ['300x250'], + bidId: 'bidId12345', + referer: 'www.example.com' + } + }]; + + let serverResponse = {}; + + let expectedResponse = []; + + const getConfigStub = sinon.stub(config, 'getConfig').returns('BTC'); + + let result = spec.interpretResponse(serverResponse, bidRequest[0]); + expect(Object.keys(result)).to.deep.equal(Object.keys(expectedResponse)); + + getConfigStub.restore(); + }); + + it('should get empty bid response if no server response', function () { + let bidRequest = [{ + method: 'POST', + url: ENDPOINT_URL, + data: { + placementId: 'testPlacementId', + device: 'desktop', + currency: 'EUR', + sizes: ['300x250'], + bidId: 'bidId12345', + referer: 'www.example.com' + } + }]; + + let serverResponse = {}; + + let expectedResponse = []; + + let result = spec.interpretResponse(serverResponse, bidRequest[0]); + expect(Object.keys(result)).to.deep.equal(Object.keys(expectedResponse)); + }); + }); +}); diff --git a/test/spec/modules/colossussspBidAdapter_spec.js b/test/spec/modules/colossussspBidAdapter_spec.js index df9bdcbd47b..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 () { @@ -108,7 +109,7 @@ describe('ColossussspAdapter', function () { bid.userId.britepoolid = 'britepoolid123'; bid.userId.idl_env = 'idl_env123'; bid.userId.tdid = 'tdid123'; - bid.userId.id5id = 'id5id123' + bid.userId.id5id = { uid: 'id5id123' }; let serverRequest = spec.buildRequests([bid], bidderRequest); it('Returns valid data if array of bids is valid', function () { let data = serverRequest.data; @@ -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/concertAnalyticsAdapter_spec.js b/test/spec/modules/concertAnalyticsAdapter_spec.js new file mode 100644 index 00000000000..b0aad2f3156 --- /dev/null +++ b/test/spec/modules/concertAnalyticsAdapter_spec.js @@ -0,0 +1,157 @@ +import concertAnalytics from 'modules/concertAnalyticsAdapter.js'; +import { expect } from 'chai'; +const sinon = require('sinon'); +let adapterManager = require('src/adapterManager').default; +let events = require('src/events'); +let constants = require('src/constants.json'); + +describe('ConcertAnalyticsAdapter', function() { + let sandbox; + let xhr; + let requests; + let clock; + let timestamp = 1896134400; + let auctionId = '9f894496-10fe-4652-863d-623462bf82b8'; + let timeout = 1000; + + before(function () { + sandbox = sinon.createSandbox(); + xhr = sandbox.useFakeXMLHttpRequest(); + requests = []; + + xhr.onCreate = function (request) { + requests.push(request); + }; + clock = sandbox.useFakeTimers(1896134400); + }); + + after(function () { + sandbox.restore(); + }); + + describe('track', function() { + beforeEach(function () { + sandbox.stub(events, 'getEvents').returns([]); + + adapterManager.enableAnalytics({ + provider: 'concert' + }); + }); + + afterEach(function () { + events.getEvents.restore(); + concertAnalytics.eventsStorage = []; + concertAnalytics.disableAnalytics(); + }); + + it('should catch all events', function() { + sandbox.spy(concertAnalytics, 'track'); + + fireBidEvents(events); + sandbox.assert.callCount(concertAnalytics.track, 5); + }); + + it('should report data for BID_RESPONSE, BID_WON events', function() { + fireBidEvents(events); + clock.tick(3000 + 1000); + + const eventsToReport = ['bidResponse', 'bidWon']; + for (var i = 0; i < concertAnalytics.eventsStorage.length; i++) { + expect(eventsToReport.indexOf(concertAnalytics.eventsStorage[i].event)).to.be.above(-1); + } + + for (var i = 0; i < eventsToReport.length; i++) { + expect(concertAnalytics.eventsStorage.some(function(event) { + return event.event === eventsToReport[i] + })).to.equal(true); + } + }); + + it('should report data in the shape expected by analytics endpoint', function() { + fireBidEvents(events); + clock.tick(3000 + 1000); + + const requiredFields = ['event', 'concert_rid', 'adId', 'auctionId', 'creativeId', 'position', 'url', 'cpm', 'width', 'height', 'timeToRespond']; + + for (var i = 0; i < requiredFields.length; i++) { + expect(concertAnalytics.eventsStorage[0]).to.have.property(requiredFields[i]); + } + }); + }); + + const adUnits = [{ + code: 'desktop_leaderboard_variable', + sizes: [[1030, 590]], + mediaTypes: { + banner: { + sizes: [[1030, 590]] + } + }, + bids: [ + { + bidder: 'concert', + params: { + partnerId: 'test_partner' + } + } + ] + }]; + + const bidResponse = { + 'bidderCode': 'concert', + 'width': 1030, + 'height': 590, + 'statusMessage': 'Bid available', + 'adId': '642f13fe18ab7dc', + 'requestId': '4062fba2e039919', + 'mediaType': 'banner', + 'source': 'client', + 'cpm': 6, + 'ad': '', + 'ttl': 360, + 'creativeId': '138308483085|62bac030-a5d3-11ea-b3be-55590c8153a5', + 'netRevenue': false, + 'currency': 'USD', + 'originalCpm': 6, + 'originalCurrency': 'USD', + 'auctionId': '9f894496-10fe-4652-863d-623462bf82b8', + 'responseTimestamp': 1591213790366, + 'requestTimestamp': 1591213790017, + 'bidder': 'concert', + 'adUnitCode': 'desktop_leaderboard_variable', + 'timeToRespond': 349, + 'status': 'rendered', + 'params': [ + { + 'partnerId': 'cst' + } + ] + } + + const bidWon = { + 'adId': '642f13fe18ab7dc', + 'mediaType': 'banner', + 'requestId': '4062fba2e039919', + 'cpm': 6, + 'creativeId': '138308483085|62bac030-a5d3-11ea-b3be-55590c8153a5', + 'currency': 'USD', + 'netRevenue': false, + 'ttl': 360, + 'auctionId': '9f894496-10fe-4652-863d-623462bf82b8', + 'statusMessage': 'Bid available', + 'responseTimestamp': 1591213790366, + 'requestTimestamp': 1591213790017, + 'bidder': 'concert', + 'adUnitCode': 'desktop_leaderboard_variable', + 'sizes': [[1030, 590]], + 'size': [1030, 590] + } + + function fireBidEvents(events) { + events.emit(constants.EVENTS.AUCTION_INIT, {timestamp, auctionId, timeout, adUnits}); + events.emit(constants.EVENTS.BID_REQUESTED, {bidder: 'concert'}); + events.emit(constants.EVENTS.BID_RESPONSE, bidResponse); + events.emit(constants.EVENTS.AUCTION_END, {}); + events.emit(constants.EVENTS.BID_WON, bidWon); + } +}); diff --git a/test/spec/modules/concertBidAdapter_spec.js b/test/spec/modules/concertBidAdapter_spec.js new file mode 100644 index 00000000000..1b869d51bde --- /dev/null +++ b/test/spec/modules/concertBidAdapter_spec.js @@ -0,0 +1,219 @@ +import { expect } from 'chai'; +import sinon from 'sinon'; +import { spec } from 'modules/concertBidAdapter.js'; +import { getStorageManager } from '../../../src/storageManager.js' + +describe('ConcertAdapter', function () { + let bidRequests; + let bidRequest; + let bidResponse; + + beforeEach(function () { + bidRequests = [ + { + bidder: 'concert', + params: { + partnerId: 'foo', + slotType: 'fizz' + }, + adUnitCode: 'desktop_leaderboard_variable', + bidId: 'foo', + transactionId: '', + sizes: [[1030, 590]] + } + ]; + + bidRequest = { + refererInfo: { + referer: 'https://www.google.com' + }, + uspConsent: '1YYY', + gdprConsent: {} + }; + + bidResponse = { + body: { + bids: [ + { + bidId: '16d2e73faea32d9', + cpm: '6', + width: '1030', + height: '590', + ad: '', + ttl: '360', + creativeId: '123349|a7d62700-a4bf-11ea-829f-ad3b0b7a9383', + netRevenue: false, + currency: 'USD' + } + ] + } + } + }); + + describe('spec.isBidRequestValid', function() { + it('should return when it recieved all the required params', function() { + const bid = bidRequests[0]; + expect(spec.isBidRequestValid(bid)).to.equal(true); + }); + + it('should return false when partner id is missing', function() { + const bid = { + bidder: 'concert', + params: {} + } + + expect(spec.isBidRequestValid(bid)).to.equal(false); + }); + }); + + describe('spec.buildRequests', function() { + it('should build a payload object with the shape expected by server', function() { + const request = spec.buildRequests(bidRequests, bidRequest); + const payload = JSON.parse(request.data); + expect(payload).to.have.property('meta'); + expect(payload).to.have.property('slots'); + + const metaRequiredFields = ['prebidVersion', 'pageUrl', 'screen', 'debug', 'uid', 'optedOut', 'adapterVersion', 'uspConsent', 'gdprConsent']; + const slotsRequiredFields = ['name', 'bidId', 'transactionId', 'sizes', 'partnerId', 'slotType']; + + metaRequiredFields.forEach(function(field) { + expect(payload.meta).to.have.property(field); + }); + slotsRequiredFields.forEach(function(field) { + expect(payload.slots[0]).to.have.property(field); + }); + }); + + it('should not generate uid if the user has opted out', function() { + const storage = getStorageManager(); + storage.setDataInLocalStorage('c_nap', 'true'); + const request = spec.buildRequests(bidRequests, bidRequest); + const payload = JSON.parse(request.data); + + expect(payload.meta.uid).to.equal(false); + }); + + it('should generate uid if the user has not opted out', function() { + const storage = getStorageManager(); + storage.removeDataFromLocalStorage('c_nap'); + const request = spec.buildRequests(bidRequests, bidRequest); + const payload = JSON.parse(request.data); + + expect(payload.meta.uid).to.not.equal(false); + }); + + it('should grab uid from local storage if it exists', function() { + const storage = getStorageManager(); + storage.setDataInLocalStorage('c_uid', 'foo'); + storage.removeDataFromLocalStorage('c_nap'); + const request = spec.buildRequests(bidRequests, bidRequest); + const payload = JSON.parse(request.data); + + expect(payload.meta.uid).to.equal('foo'); + }); + }); + + describe('spec.interpretResponse', function() { + it('should return bids in the shape expected by prebid', function() { + const bids = spec.interpretResponse(bidResponse, bidRequest); + const requiredFields = ['requestId', 'cpm', 'width', 'height', 'ad', 'ttl', 'meta', 'creativeId', 'netRevenue', 'currency']; + + requiredFields.forEach(function(field) { + expect(bids[0]).to.have.property(field); + }); + }); + + it('should return empty bids if there is no response from server', function() { + const bids = spec.interpretResponse({ body: null }, bidRequest); + expect(bids).to.have.lengthOf(0); + }); + + it('should return empty bids if there are no bids from the server', function() { + const bids = spec.interpretResponse({ body: {bids: []} }, bidRequest); + expect(bids).to.have.lengthOf(0); + }); + }); + + describe('spec.getUserSyncs', function() { + it('should not register syncs when iframe is not enabled', function() { + const opts = { + iframeEnabled: false + } + const sync = spec.getUserSyncs(opts, [], bidRequest.gdprConsent, bidRequest.uspConsent); + expect(sync).to.have.lengthOf(0); + }); + + it('should not register syncs when the user has opted out', function() { + const opts = { + iframeEnabled: true + }; + const storage = getStorageManager(); + storage.setDataInLocalStorage('c_nap', 'true'); + + const sync = spec.getUserSyncs(opts, [], bidRequest.gdprConsent, bidRequest.uspConsent); + expect(sync).to.have.lengthOf(0); + }); + + it('should set gdprApplies flag to 1 if the user is in area where GDPR applies', function() { + const opts = { + iframeEnabled: true + }; + const storage = getStorageManager(); + storage.removeDataFromLocalStorage('c_nap'); + + bidRequest.gdprConsent = { + gdprApplies: true + }; + + const sync = spec.getUserSyncs(opts, [], bidRequest.gdprConsent, bidRequest.uspConsent); + expect(sync[0].url).to.have.string('gdpr_applies=1'); + }); + + it('should set gdprApplies flag to 1 if the user is in area where GDPR applies', function() { + const opts = { + iframeEnabled: true + }; + const storage = getStorageManager(); + storage.removeDataFromLocalStorage('c_nap'); + + bidRequest.gdprConsent = { + gdprApplies: false + }; + + const sync = spec.getUserSyncs(opts, [], bidRequest.gdprConsent, bidRequest.uspConsent); + expect(sync[0].url).to.have.string('gdpr_applies=0'); + }); + + it('should set gdpr consent param with the user\'s choices on consent', function() { + const opts = { + iframeEnabled: true + }; + const storage = getStorageManager(); + storage.removeDataFromLocalStorage('c_nap'); + + bidRequest.gdprConsent = { + gdprApplies: false, + consentString: 'BOJ/P2HOJ/P2HABABMAAAAAZ+A==' + }; + + const sync = spec.getUserSyncs(opts, [], bidRequest.gdprConsent, bidRequest.uspConsent); + expect(sync[0].url).to.have.string('gdpr_consent=BOJ/P2HOJ/P2HABABMAAAAAZ+A=='); + }); + + it('should set ccpa consent param with the user\'s choices on consent', function() { + const opts = { + iframeEnabled: true + }; + const storage = getStorageManager(); + storage.removeDataFromLocalStorage('c_nap'); + + bidRequest.gdprConsent = { + gdprApplies: false, + uspConsent: '1YYY' + }; + + const sync = spec.getUserSyncs(opts, [], bidRequest.gdprConsent, bidRequest.uspConsent); + expect(sync[0].url).to.have.string('usp_consent=1YY'); + }); + }); +}); diff --git a/test/spec/modules/connectadBidAdapter_spec.js b/test/spec/modules/connectadBidAdapter_spec.js index aef4fb562a7..dbac3c0dc7c 100644 --- a/test/spec/modules/connectadBidAdapter_spec.js +++ b/test/spec/modules/connectadBidAdapter_spec.js @@ -84,7 +84,6 @@ describe('ConnectAd Adapter', function () { } }; const isValid = spec.isBidRequestValid(validBid); - expect(isValid).to.equal(true); }); @@ -162,13 +161,19 @@ describe('ConnectAd Adapter', function () { bidRequests[0].getFloor = () => floorInfo; const request = spec.buildRequests(bidRequests, bidderRequest); const requestparse = JSON.parse(request.data); - expect(requestparse.placements[0].floor).to.equal(5.20); + expect(requestparse.placements[0].bidfloor).to.equal(5.20); }); - it('should be 0 if no floormodule is available', function() { + it('should be bidfloor if no floormodule is available', function() { const request = spec.buildRequests(bidRequests, bidderRequest); const requestparse = JSON.parse(request.data); - expect(requestparse.placements[0].floor).to.equal(0); + expect(requestparse.placements[0].bidfloor).to.equal(0.50); + }); + + it('should have 0 bidfloor value', function() { + const request = spec.buildRequests(bidRequestsUserIds, bidderRequest); + const requestparse = JSON.parse(request.data); + expect(requestparse.placements[0].bidfloor).to.equal(0); }); it('should contain gdpr info', function () { diff --git a/test/spec/modules/consentManagementUsp_spec.js b/test/spec/modules/consentManagementUsp_spec.js index ee4140afa10..7d3cd48a8e4 100644 --- a/test/spec/modules/consentManagementUsp_spec.js +++ b/test/spec/modules/consentManagementUsp_spec.js @@ -64,6 +64,13 @@ describe('consentManagement', function () { sinon.assert.calledOnce(utils.logWarn); sinon.assert.notCalled(utils.logInfo); }); + + it('should exit consentManagementUsp module if config is "undefined"', function() { + setConsentConfig(undefined); + expect(consentAPI).to.be.undefined; + sinon.assert.calledOnce(utils.logWarn); + sinon.assert.notCalled(utils.logInfo); + }); }); describe('valid setConsentConfig value', function () { diff --git a/test/spec/modules/consentManagement_spec.js b/test/spec/modules/consentManagement_spec.js index deaacbc5a28..5e9b0f07f46 100644 --- a/test/spec/modules/consentManagement_spec.js +++ b/test/spec/modules/consentManagement_spec.js @@ -39,6 +39,12 @@ describe('consentManagement', function () { expect(userCMP).to.be.undefined; sinon.assert.calledOnce(utils.logWarn); }); + + it('should exit consentManagement module if config is "undefined"', function() { + setConsentConfig(undefined); + expect(userCMP).to.be.undefined; + sinon.assert.calledOnce(utils.logWarn); + }); }); describe('valid setConsentConfig value', function () { @@ -531,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 () { @@ -556,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/conversantBidAdapter_spec.js b/test/spec/modules/conversantBidAdapter_spec.js index d802cd288ef..1c24fb7694a 100644 --- a/test/spec/modules/conversantBidAdapter_spec.js +++ b/test/spec/modules/conversantBidAdapter_spec.js @@ -339,6 +339,7 @@ describe('Conversant adapter tests', function() { expect(bid).to.have.property('creativeId', '1000'); expect(bid).to.have.property('width', 300); expect(bid).to.have.property('height', 250); + expect(bid.meta.advertiserDomains).to.deep.equal(['https://example.com']); expect(bid).to.have.property('ad', 'markup000'); expect(bid).to.have.property('ttl', 300); expect(bid).to.have.property('netRevenue', true); @@ -483,7 +484,7 @@ describe('Conversant adapter tests', function() { const payload = spec.buildRequests(requests).data; expect(payload).to.have.deep.nested.property('user.ext.eids', [ {source: 'adserver.org', uids: [{id: '223344', atype: 1, ext: {rtiPartner: 'TDID'}}]}, - {source: 'liveramp.com', uids: [{id: '334455', atype: 1}]} + {source: 'liveramp.com', uids: [{id: '334455', atype: 3}]} ]); }); }); diff --git a/test/spec/modules/cpmstarBidAdapter_spec.js b/test/spec/modules/cpmstarBidAdapter_spec.js old mode 100755 new mode 100644 index 1993f28e5d5..285fca9690a --- a/test/spec/modules/cpmstarBidAdapter_spec.js +++ b/test/spec/modules/cpmstarBidAdapter_spec.js @@ -3,6 +3,41 @@ import { spec } from 'modules/cpmstarBidAdapter.js'; import { deepClone } from 'src/utils.js'; import { config } from 'src/config.js'; +const valid_bid_requests = [{ + 'bidder': 'cpmstar', + 'params': { + 'placementId': '57' + }, + 'sizes': [[300, 250]], + 'bidId': 'bidId' +}]; + +const bidderRequest = { + refererInfo: { + referer: 'referer', + reachedTop: false, + } +}; + +const serverResponse = { + body: [{ + creatives: [{ + cpm: 1, + width: 0, + height: 0, + currency: 'USD', + netRevenue: true, + ttl: 1, + creativeid: '1234', + requestid: '11123', + code: 'no idea', + media: 'banner', + } + ], + syncs: [{ type: 'image', url: 'https://server.cpmstar.com/pixel.aspx' }] + }] +}; + describe('Cpmstar Bid Adapter', function () { describe('isBidRequestValid', function () { it('should return true since the bid is valid', @@ -42,23 +77,6 @@ describe('Cpmstar Bid Adapter', function () { }); describe('buildRequests', function () { - const valid_bid_requests = [{ - 'bidder': 'cpmstar', - 'params': { - 'placementId': '57' - }, - 'sizes': [[300, 250]], - 'bidId': 'bidId' - }]; - - const bidderRequest = { - refererInfo: { - referer: 'referer', - reachedTop: false, - } - - }; - it('should produce a valid production request', function () { var requests = spec.buildRequests(valid_bid_requests, bidderRequest); expect(requests[0]).to.have.property('method'); @@ -109,7 +127,30 @@ describe('Cpmstar Bid Adapter', function () { expect(requests[0]).to.have.property('url'); expect(requests[0].url).to.include('tfcd=1'); }); - }) + }); + + it('should produce a request with support for OpenRTB SupplyChain', function () { + var reqs = deepClone(valid_bid_requests); + reqs[0].schain = { + 'ver': '1.0', + 'complete': 1, + 'nodes': [ + { + 'asi': 'exchange1.com', + 'sid': '1234', + 'hp': 1 + }, + { + 'asi': 'exchange2.com', + 'sid': 'abcd', + 'hp': 1 + } + ] + }; + var requests = spec.buildRequests(reqs, bidderRequest); + expect(requests[0]).to.have.property('url'); + expect(requests[0].url).to.include('&schain=1.0,1!exchange1.com,1234,1,,,!exchange2.com,abcd,1,,,'); + }); describe('interpretResponse', function () { const request = { @@ -117,23 +158,6 @@ describe('Cpmstar Bid Adapter', function () { mediaType: 'BANNER' } }; - const serverResponse = { - body: [{ - creatives: [{ - cpm: 1, - width: 0, - height: 0, - currency: 'USD', - netRevenue: true, - ttl: 1, - creativeid: '1234', - requestid: '11123', - code: 'no idea', - media: 'banner', - } - ], - }] - }; it('should return a valid bidresponse array', function () { var r = spec.interpretResponse(serverResponse, request) @@ -185,4 +209,23 @@ describe('Cpmstar Bid Adapter', function () { expect(spec.interpretResponse(dealServer, request)[0].dealId).to.equal('deal'); }); }); + + describe('getUserSyncs', function () { + var sres = [deepClone(serverResponse)]; + + it('should return a valid pixel sync', function () { + var syncs = spec.getUserSyncs({ pixelEnabled: true }, sres); + expect(syncs.length).equal(1); + expect(syncs[0].type).equal('image'); + expect(syncs[0].url).equal('https://server.cpmstar.com/pixel.aspx'); + }); + + it('should return a valid iframe sync', function () { + sres[0].body[0].syncs[0].type = 'iframe'; + var syncs = spec.getUserSyncs({ iframeEnabled: true }, sres); + expect(syncs.length).equal(1); + expect(syncs[0].type).equal('iframe'); + expect(syncs[0].url).equal('https://server.cpmstar.com/pixel.aspx'); + }); + }); }); 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..cad1e3f8114 100755 --- a/test/spec/modules/criteoBidAdapter_spec.js +++ b/test/spec/modules/criteoBidAdapter_spec.js @@ -820,14 +820,18 @@ describe('The Criteo bidding adapter', function () { it('should properly build a request with first party data', function () { const contextData = { keywords: ['power tools'], - data: { - pageType: 'article' + ext: { + data: { + pageType: 'article' + } } }; const userData = { gender: 'M', - data: { - registered: true + ext: { + data: { + registered: true + } } }; const bidRequests = [ @@ -842,8 +846,8 @@ describe('The Criteo bidding adapter', function () { bidfloor: 0.75 } }, - fpd: { - context: { + ortb2Imp: { + ext: { data: { someContextAttribute: 'abc' } @@ -854,8 +858,8 @@ describe('The Criteo bidding adapter', function () { sandbox.stub(config, 'getConfig').callsFake(key => { const config = { - fpd: { - context: contextData, + ortb2: { + site: contextData, user: userData } }; @@ -863,8 +867,8 @@ describe('The Criteo bidding adapter', function () { }); const request = spec.buildRequests(bidRequests, bidderRequest); - expect(request.data.publisher.ext).to.deep.equal(contextData); - expect(request.data.user.ext).to.deep.equal(userData); + expect(request.data.publisher.ext).to.deep.equal({keywords: ['power tools'], data: {pageType: 'article'}}); + expect(request.data.user.ext).to.deep.equal({gender: 'M', data: {registered: true}}); expect(request.data.slots[0].ext).to.deep.equal({ bidfloor: 0.75, data: { @@ -992,6 +996,7 @@ describe('The Criteo bidding adapter', function () { zoneid: 123, native: { 'products': [{ + 'sendTargetingKeys': false, 'title': 'Product title', 'description': 'Product desc', 'price': '100', @@ -1027,7 +1032,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 +1040,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/criteoIdSystem_spec.js b/test/spec/modules/criteoIdSystem_spec.js index aa5807da0da..65e5aaf741d 100644 --- a/test/spec/modules/criteoIdSystem_spec.js +++ b/test/spec/modules/criteoIdSystem_spec.js @@ -71,7 +71,6 @@ describe('CriteoId module', function () { }); it('should call user sync url with the right params', function () { - getCookieStub.withArgs('cto_test_cookie').returns('1'); getCookieStub.withArgs('cto_bundle').returns('bundle'); window.criteo_pubtag = {} @@ -80,7 +79,7 @@ describe('CriteoId module', function () { ajaxBuilderStub.callsFake(mockResponse(undefined, ajaxStub)) criteoIdSubmodule.getId(); - const expectedUrl = `https://gum.criteo.com/sid/json?origin=prebid&topUrl=https%3A%2F%2Ftestdev.com%2F&domain=testdev.com&bundle=bundle&cw=1&pbt=1`; + const expectedUrl = `https://gum.criteo.com/sid/json?origin=prebid&topUrl=https%3A%2F%2Ftestdev.com%2F&domain=testdev.com&bundle=bundle&cw=1&pbt=1&lsw=1`; expect(ajaxStub.calledWith(expectedUrl)).to.be.true; diff --git a/test/spec/modules/decenteradsBidAdapter_spec.js b/test/spec/modules/decenteradsBidAdapter_spec.js new file mode 100644 index 00000000000..257094cae3a --- /dev/null +++ b/test/spec/modules/decenteradsBidAdapter_spec.js @@ -0,0 +1,207 @@ +import { expect } from 'chai' +import { spec } from '../../../modules/decenteradsBidAdapter.js' +import { deepStrictEqual, notEqual, ok, strictEqual } from 'assert' + +describe('DecenteradsAdapter', () => { + const bid = { + bidId: '9ec5b177515ee2e5', + bidder: 'decenterads', + params: { + placementId: 0, + traffic: 'banner' + } + } + + describe('isBidRequestValid', () => { + it('Should return true if there are bidId, params and placementId parameters present', () => { + strictEqual(true, spec.isBidRequestValid(bid)) + }) + + it('Should return false if at least one of parameters is not present', () => { + const b = { ...bid } + delete b.params.placementId + strictEqual(false, spec.isBidRequestValid(b)) + }) + }) + + describe('buildRequests', () => { + const serverRequest = spec.buildRequests([bid]) + + it('Creates a ServerRequest object with method, URL and data', () => { + ok(serverRequest) + ok(serverRequest.method) + ok(serverRequest.url) + ok(serverRequest.data) + }) + + it('Returns POST method', () => { + strictEqual('POST', serverRequest.method) + }) + + it('Returns valid URL', () => { + strictEqual('https://supply.decenterads.com/?c=o&m=multi', serverRequest.url) + }) + + it('Returns valid data if array of bids is valid', () => { + const { data } = serverRequest + strictEqual('object', typeof data) + deepStrictEqual(['deviceWidth', 'deviceHeight', 'language', 'secure', 'host', 'page', 'placements'], Object.keys(data)) + strictEqual('number', typeof data.deviceWidth) + strictEqual('number', typeof data.deviceHeight) + strictEqual('string', typeof data.language) + strictEqual('string', typeof data.host) + strictEqual('string', typeof data.page) + notEqual(-1, [0, 1].indexOf(data.secure)) + + const placement = data.placements[0] + deepStrictEqual(['placementId', 'bidId', 'traffic'], Object.keys(placement)) + strictEqual(0, placement.placementId) + strictEqual('9ec5b177515ee2e5', placement.bidId) + strictEqual('banner', placement.traffic) + }) + + it('Returns empty data if no valid requests are passed', () => { + const { placements } = spec.buildRequests([]).data + + expect(spec.buildRequests([]).data.placements).to.be.an('array') + strictEqual(0, placements.length) + }) + }) + + describe('interpretResponse', () => { + const validData = [ + { + body: [{ + mediaType: 'banner', + width: 300, + height: 250, + cpm: 0.4, + ad: 'Test', + requestId: '9ec5b177515ee2e5', + ttl: 120, + creativeId: '2', + netRevenue: true, + currency: 'USD', + dealId: '1' + }] + }, + { + body: [{ + vastUrl: 'decenterads.com', + mediaType: 'video', + cpm: 0.5, + requestId: '9ec5b177515ee2e5', + ttl: 120, + creativeId: '2', + netRevenue: true, + currency: 'USD', + dealId: '1' + }] + }, + { + body: [{ + mediaType: 'native', + clickUrl: 'decenterads.com', + title: 'Test', + image: 'decenterads.com', + creativeId: '2', + impressionTrackers: ['decenterads.com'], + ttl: 120, + cpm: 0.4, + requestId: '9ec5b177515ee2e5', + netRevenue: true, + currency: 'USD', + }] + } + ] + + for (const obj of validData) { + const { mediaType } = obj.body[0] + + it(`Should interpret ${mediaType} response`, () => { + const response = spec.interpretResponse(obj) + + expect(response).to.be.an('array') + strictEqual(1, response.length) + + const copy = { ...obj.body[0] } + delete copy.mediaType + deepStrictEqual(copy, response[0]) + }) + } + + const invalidData = [ + { + body: [{ + width: 300, + cpm: 0.4, + ad: 'Test', + requestId: '9ec5b177515ee2e5', + ttl: 120, + creativeId: '2', + netRevenue: true, + currency: 'USD', + dealId: '1' + }] + }, + { + body: [{ + mediaType: 'video', + cpm: 0.5, + requestId: '9ec5b177515ee2e5', + ttl: 120, + creativeId: '2', + netRevenue: true, + currency: 'USD', + dealId: '1' + }] + }, + { + body: [{ + mediaType: 'native', + clickUrl: 'decenterads.com', + title: 'Test', + impressionTrackers: ['decenterads.com'], + ttl: 120, + requestId: '9ec5b177515ee2e5', + creativeId: '2', + netRevenue: true, + currency: 'USD', + }] + } + ] + + for (const obj of invalidData) { + const { mediaType } = obj.body[0] + + it(`Should return an empty array if invalid ${mediaType} response is passed `, () => { + const response = spec.interpretResponse(obj) + + expect(response).to.be.an('array') + strictEqual(0, response.length) + }) + } + + it('Should return an empty array if invalid response is passed', () => { + const response = spec.interpretResponse({ + body: [{ + ttl: 120, + creativeId: '2', + netRevenue: true, + currency: 'USD', + dealId: '1' + }] + }) + + expect(response).to.be.an('array') + strictEqual(0, response.length) + }) + }) + + describe('getUserSyncs', () => { + it('Returns valid URL and type', () => { + const expectedResult = [{ type: 'image', url: 'https://supply.decenterads.com/?c=o&m=cookie' }] + deepStrictEqual(expectedResult, spec.getUserSyncs()) + }) + }) +}) diff --git a/test/spec/modules/deepintentBidAdapter_spec.js b/test/spec/modules/deepintentBidAdapter_spec.js index 6d9b883e2bb..fcf7056fb3f 100644 --- a/test/spec/modules/deepintentBidAdapter_spec.js +++ b/test/spec/modules/deepintentBidAdapter_spec.js @@ -202,6 +202,7 @@ describe('Deepintent adapter', function () { expect(bResponse[0].height).to.equal(bannerResponse.body.seatbid[0].bid[0].h); expect(bResponse[0].currency).to.equal('USD'); expect(bResponse[0].netRevenue).to.equal(false); + expect(bResponse[0].meta.advertiserDomains).to.deep.equal(['deepintent.com']); expect(bResponse[0].ttl).to.equal(300); expect(bResponse[0].creativeId).to.equal(bannerResponse.body.seatbid[0].bid[0].crid); expect(bResponse[0].dealId).to.equal(bannerResponse.body.seatbid[0].bid[0].dealId); diff --git a/test/spec/modules/deepintentDpesIdsystem_spec.js b/test/spec/modules/deepintentDpesIdsystem_spec.js new file mode 100644 index 00000000000..7ea5553393c --- /dev/null +++ b/test/spec/modules/deepintentDpesIdsystem_spec.js @@ -0,0 +1,76 @@ +import { expect } from 'chai'; +import find from 'core-js-pure/features/array/find.js'; +import { storage, deepintentDpesSubmodule } from 'modules/deepintentDpesIdSystem.js'; +import { init, requestBidsHook, setSubmoduleRegistry } from 'modules/userId/index.js'; +import { config } from 'src/config.js'; + +const DI_COOKIE_NAME = '_dpes_id'; +const DI_COOKIE_STORED = '{"id":"2cf40748c4f7f60d343336e08f80dc99"}'; +const DI_COOKIE_OBJECT = {id: '2cf40748c4f7f60d343336e08f80dc99'}; + +const cookieConfig = { + name: 'deepintentId', + storage: { + type: 'cookie', + name: '_dpes_id', + expires: 28 + } +}; + +const html5Config = { + name: 'deepintentId', + storage: { + type: 'html5', + name: '_dpes_id', + expires: 28 + } +} + +describe('Deepintent DPES System', () => { + let getDataFromLocalStorageStub, localStorageIsEnabledStub; + let getCookieStub, cookiesAreEnabledStub; + + beforeEach(() => { + getDataFromLocalStorageStub = sinon.stub(storage, 'getDataFromLocalStorage'); + localStorageIsEnabledStub = sinon.stub(storage, 'localStorageIsEnabled'); + getCookieStub = sinon.stub(storage, 'getCookie'); + cookiesAreEnabledStub = sinon.stub(storage, 'cookiesAreEnabled'); + }); + + afterEach(() => { + getDataFromLocalStorageStub.restore(); + localStorageIsEnabledStub.restore(); + getCookieStub.restore(); + cookiesAreEnabledStub.restore(); + }); + + describe('Deepintent Dpes Sytsem: test "getId" method', () => { + it('Wrong config should fail the tests', () => { + // no config + expect(deepintentDpesSubmodule.getId()).to.be.eq(undefined); + expect(deepintentDpesSubmodule.getId({ })).to.be.eq(undefined); + + expect(deepintentDpesSubmodule.getId({params: {}, storage: {}})).to.be.eq(undefined); + expect(deepintentDpesSubmodule.getId({params: {}, storage: {type: 'cookie'}})).to.be.eq(undefined); + expect(deepintentDpesSubmodule.getId({params: {}, storage: {name: '_dpes_id'}})).to.be.eq(undefined); + }); + + it('Get value stored in cookie for getId', () => { + getCookieStub.withArgs(DI_COOKIE_NAME).returns(DI_COOKIE_STORED); + let diId = deepintentDpesSubmodule.getId(cookieConfig, undefined, DI_COOKIE_OBJECT); + expect(diId).to.deep.equal(DI_COOKIE_OBJECT); + }); + + it('provides the stored deepintentId if cookie is absent but present in local storage', () => { + getDataFromLocalStorageStub.withArgs(DI_COOKIE_NAME).returns(DI_COOKIE_STORED); + let idx = deepintentDpesSubmodule.getId(html5Config, undefined, DI_COOKIE_OBJECT); + expect(idx).to.deep.equal(DI_COOKIE_OBJECT); + }); + }); + + describe('Deepintent Dpes System : test "decode" method', () => { + it('Get the correct decoded value for dpes id', () => { + expect(deepintentDpesSubmodule.decode(DI_COOKIE_OBJECT, cookieConfig)).to.deep.equal({'deepintentId': {'id': '2cf40748c4f7f60d343336e08f80dc99'}}); + }); + }); +}); diff --git a/test/spec/modules/dfpAdServerVideo_spec.js b/test/spec/modules/dfpAdServerVideo_spec.js index cd412530d2b..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'; @@ -38,7 +39,7 @@ describe('The DFP video support module', function () { expect(queryParams).to.have.property('env', 'vp'); expect(queryParams).to.have.property('gdfp_req', '1'); expect(queryParams).to.have.property('iu', 'my/adUnit'); - expect(queryParams).to.have.property('output', 'xml_vast3'); + expect(queryParams).to.have.property('output', 'vast'); expect(queryParams).to.have.property('sz', '640x480'); expect(queryParams).to.have.property('unviewed_position_start', '1'); expect(queryParams).to.have.property('url'); @@ -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', @@ -375,15 +494,21 @@ describe('The DFP video support module', function () { expect(queryParams).to.have.property('env', 'vp'); expect(queryParams).to.have.property('gdfp_req', '1'); expect(queryParams).to.have.property('iu', 'my/adUnit'); - expect(queryParams).to.have.property('output', 'xml_vast3'); + expect(queryParams).to.have.property('output', 'vast'); expect(queryParams).to.have.property('sz', '640x480'); 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 d8f0beb9a36..90c18c6a84f 100644 --- a/test/spec/modules/districtmDmxBidAdapter_spec.js +++ b/test/spec/modules/districtmDmxBidAdapter_spec.js @@ -1,6 +1,60 @@ -import {expect} from 'chai'; +import { expect } from 'chai'; import * as _ from 'lodash'; -import {spec, matchRequest, checkDeepArray, defaultSize, upto5, cleanSizes, shuffle} from '../../../modules/districtmDMXBidAdapter.js'; +import { spec, matchRequest, checkDeepArray, defaultSize, upto5, cleanSizes, shuffle, getApi, bindUserId, getPlaybackmethod, getProtocols, cleanVast } from '../../../modules/districtmDMXBidAdapter.js'; + +const sample_vast = ` + + + + + + + + + 00:00:15 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +` const supportedSize = [ { @@ -42,6 +96,30 @@ const bidRequest = [{ 'dmxid': 100001, 'memberid': 100003, }, + 'userId': { + idl_env: {}, + digitrustid: { + data: { + id: {} + } + }, + id5id: { + uid: '' + }, + pubcid: {}, + tdid: {}, + criteoId: {}, + britepoolid: {}, + intentiqid: {}, + lotamePanoramaId: {}, + parrableId: {}, + netId: {}, + sharedid: {}, + lipb: { + lipbid: {} + }, + + }, 'adUnitCode': 'div-gpt-ad-12345678-1', 'transactionId': 'f6d13fa6-ebc1-41ac-9afa-d8171d22d2c2', 'sizes': [ @@ -53,6 +131,35 @@ const bidRequest = [{ 'auctionId': '3d62f2d3-56a2-4991-888e-f7754619ddcf' }]; +const bidRequestVideo = [{ + 'bidder': 'districtmDMX', + 'params': { + 'dmxid': 100001, + 'memberid': 100003, + 'video': { + id: 123, + skipppable: true, + playback_method: ['auto_play_sound_off', 'viewport_sound_off'], + mimes: ['application/javascript', + 'video/mp4'], + } + }, + 'mediaTypes': { + video: { + context: 'instream', // or 'outstream' + playerSize: [[640, 480]] + } + }, + 'adUnitCode': 'div-gpt-ad-12345678-1', + 'transactionId': 'f6d13fa6-ebc1-41ac-9afa-d8171d22d2c2', + 'sizes': [ + [300, 250], + [300, 600] + ], + 'bidId': '29a28a1bbc8a8d', + 'bidderRequestId': '124b579a136515', + 'auctionId': '3d62f2d3-56a2-4991-888e-f7754619ddcf' +}]; const bidRequestNoCoppa = [{ 'bidder': 'districtmDMX', 'params': { @@ -499,12 +606,57 @@ const emptyResponseSeatBid = { body: { seatbid: [] } }; describe('DistrictM Adaptor', function () { const districtm = spec; - describe('verification of upto5', function() { - it('upto5 function should always break 12 imps into 3 request same for 15', function() { + describe('verification of upto5', function () { + it('upto5 function should always break 12 imps into 3 request same for 15', function () { expect(upto5([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12], bidRequest, bidderRequest, 'https://google').length).to.be.equal(3) expect(upto5([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15], bidRequest, bidderRequest, 'https://google').length).to.be.equal(3) }) }) + + describe('test vast tag', function () { + it('img tag should not be present', function () { + expect(cleanVast(sample_vast).indexOf('img') !== -1).to.be.equal(false) + }) + }) + describe('Test getApi function', function () { + const data = { + api: [1] + } + it('Will return 1 for vpaid version 1', function () { + expect(getApi(data)[0]).to.be.equal(1) + }) + it('Will return 2 for vpaid default', function () { + expect(getApi({})[0]).to.be.equal(2) + }) + }) + + describe('Test cleanSizes function', function () { + it('sequence will be respected', function () { + expect(cleanSizes(bidderRequest.bids[0].sizes).toString()).to.be.equal('300,250,300,600') + }) + it('sequence will be respected', function () { + expect(cleanSizes([[728, 90], [970, 90], [300, 600], [320, 50]]).toString()).to.be.equal('728,90,320,50,300,600,970,90') + }) + }) + + describe('Test getPlaybackmethod function', function () { + it('getPlaybackmethod will return 2', function () { + expect(getPlaybackmethod([])[0]).to.be.equal(2) + }) + it('getPlaybackmethod will return 6', function () { + expect(getPlaybackmethod(['viewport_sound_off'])[0]).to.be.equal(6) + }) + }) + + describe('Test getProtocols function', function () { + it('getProtocols will return 3', function () { + expect(getProtocols({ protocols: [3] })[0]).to.be.equal(3) + }) + it('getProtocols will return 6', function () { + expect(_.isEqual(getProtocols({}), [2, 3, 5, 6, 7, 8])).to.be.equal(true) + }) + }) + describe('All needed functions are available', function () { it(`isBidRequestValid is present and type function`, function () { expect(districtm.isBidRequestValid).to.exist.and.to.be.a('function') @@ -535,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); + 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'); }); }); @@ -568,19 +719,19 @@ describe('DistrictM Adaptor', function () { it(`the function should return an array`, function () { expect(buildRequestResults).to.be.an('object'); }); - it(`contain gdpr consent & ccpa`, function() { + it(`contain gdpr consent & ccpa`, function () { const bidr = JSON.parse(buildRequestResults.data) expect(bidr.regs.ext.gdpr).to.be.equal(1); expect(bidr.regs.ext.us_privacy).to.be.equal('1NY'); expect(bidr.user.ext.consent).to.be.an('string'); }); - it(`test contain COPPA`, function() { + it(`test contain COPPA`, function () { const bidr = JSON.parse(buildRequestResults.data) bidr.regs = bidr.regs || {}; bidr.regs.coppa = 1; expect(bidr.regs.coppa).to.be.equal(1) }) - it(`test should not contain COPPA`, function() { + it(`test should not contain COPPA`, function () { const bidr = JSON.parse(buildRequestResultsNoCoppa.data) expect(bidr.regs.coppa).to.be.equal(0) }) @@ -589,11 +740,17 @@ describe('DistrictM Adaptor', function () { }); }); + describe('bidRequest Video testing', function () { + const request = districtm.buildRequests(bidRequestVideo, bidRequestVideo); + const data = JSON.parse(request.data) + expect(data instanceof Object).to.be.equal(true) + }) + describe(`interpretResponse test usage`, function () { - const responseResults = districtm.interpretResponse(responses, {bidderRequest}); - const emptyResponseResults = districtm.interpretResponse(emptyResponse, {bidderRequest}); - const emptyResponseResultsNegation = districtm.interpretResponse(responsesNegative, {bidderRequest}); - const emptyResponseResultsEmptySeat = districtm.interpretResponse(emptyResponseSeatBid, {bidderRequest}); + const responseResults = districtm.interpretResponse(responses, { bidderRequest }); + const emptyResponseResults = districtm.interpretResponse(emptyResponse, { bidderRequest }); + const emptyResponseResultsNegation = districtm.interpretResponse(responsesNegative, { bidderRequest }); + const emptyResponseResultsEmptySeat = districtm.interpretResponse(emptyResponseSeatBid, { bidderRequest }); it(`the function should return an array`, function () { expect(responseResults).to.be.an('array'); }); @@ -613,10 +770,10 @@ describe('DistrictM Adaptor', function () { }); describe(`check validation for id sync gdpr ccpa`, () => { - let allin = spec.getUserSyncs({iframeEnabled: true}, {}, bidderRequest.gdprConsent, bidderRequest.uspConsent)[0] - let noCCPA = spec.getUserSyncs({iframeEnabled: true}, {}, bidderRequest.gdprConsent, null)[0] - let noGDPR = spec.getUserSyncs({iframeEnabled: true}, {}, null, bidderRequest.uspConsent)[0] - let nothing = spec.getUserSyncs({iframeEnabled: true}, {}, null, null)[0] + let allin = spec.getUserSyncs({ iframeEnabled: true }, {}, bidderRequest.gdprConsent, bidderRequest.uspConsent)[0] + let noCCPA = spec.getUserSyncs({ iframeEnabled: true }, {}, bidderRequest.gdprConsent, null)[0] + let noGDPR = spec.getUserSyncs({ iframeEnabled: true }, {}, null, bidderRequest.uspConsent)[0] + let nothing = spec.getUserSyncs({ iframeEnabled: true }, {}, null, null)[0] /* @@ -639,10 +796,10 @@ describe('DistrictM Adaptor', function () { }) describe(`Helper function testing`, function () { - const bid = matchRequest('29a28a1bbc8a8d', {bidderRequest}); - const {width, height} = defaultSize(bid); + const bid = matchRequest('29a28a1bbc8a8d', { bidderRequest }); + const { width, height } = defaultSize(bid); it(`test matchRequest`, function () { - expect(matchRequest('29a28a1bbc8a8d', {bidderRequest})).to.be.an('object'); + expect(matchRequest('29a28a1bbc8a8d', { bidderRequest })).to.be.an('object'); }); it(`test checkDeepArray`, function () { expect(_.isEqual(checkDeepArray([728, 90]), [728, 90])).to.be.equal(true); diff --git a/test/spec/modules/dmdIdSystem_spec.js b/test/spec/modules/dmdIdSystem_spec.js new file mode 100644 index 00000000000..9c603eebbd5 --- /dev/null +++ b/test/spec/modules/dmdIdSystem_spec.js @@ -0,0 +1,48 @@ +import * as utils from '../../../src/utils.js'; + +import {dmdIdSubmodule} from 'modules/dmdIdSystem.js'; + +describe('Dmd ID System', function() { + let logErrorStub; + + beforeEach(function () { + logErrorStub = sinon.stub(utils, 'logError'); + }); + + afterEach(function () { + logErrorStub.restore(); + }); + + it('should log an error if no configParams were passed into getId', function () { + dmdIdSubmodule.getId(); + expect(logErrorStub.calledOnce).to.be.true; + }); + + it('should log an error if configParams doesnot have api_key passed to getId', function () { + dmdIdSubmodule.getId({params: {}}); + expect(logErrorStub.calledOnce).to.be.true; + }); + + it('should log an error if configParams has invalid api_key passed into getId', function () { + dmdIdSubmodule.getId({params: {api_key: 123}}); + expect(logErrorStub.calledOnce).to.be.true; + }); + + it('should not log an error if configParams has valid api_key passed into getId', function () { + dmdIdSubmodule.getId({params: {api_key: '3fdbe297-3690-4f5c-9e11-ee9186a6d77c'}}); + expect(logErrorStub.calledOnce).to.be.false; + }); + + it('should return undefined if empty value passed into decode', function () { + expect(dmdIdSubmodule.decode()).to.be.undefined; + }); + + it('should return undefined if invalid dmd-dgid passed into decode', function () { + expect(dmdIdSubmodule.decode(123)).to.be.undefined; + }); + + it('should return dmdId if valid dmd-dgid passed into decode', function () { + let data = { 'dmdId': 'U12345' }; + expect(dmdIdSubmodule.decode('U12345')).to.deep.equal(data); + }); +}); diff --git a/test/spec/modules/docereeBidAdapter_spec.js b/test/spec/modules/docereeBidAdapter_spec.js new file mode 100644 index 00000000000..efff2efa319 --- /dev/null +++ b/test/spec/modules/docereeBidAdapter_spec.js @@ -0,0 +1,97 @@ +import {expect} from 'chai'; +import {spec} from '../../../modules/docereeBidAdapter.js'; +import { config } from '../../../src/config.js'; + +describe('BidlabBidAdapter', function () { + config.setConfig({ + doceree: { + context: { + data: { + token: 'testing-token', // required + } + }, + user: { + data: { + gender: '', + email: '', + hashedEmail: '', + firstName: '', + lastName: '', + npi: '', + hashedNPI: '', + city: '', + zipCode: '', + specialization: '', + } + } + } + }); + let bid = { + bidId: 'testing', + bidder: 'doceree', + params: { + placementId: 'DOC_7jm9j5eqkl0xvc5w', + } + }; + + describe('isBidRequestValid', function () { + it('Should return true if placementId is present', function () { + expect(spec.isBidRequestValid(bid)).to.be.true; + }); + it('Should return false if placementId is not present', function () { + delete bid.params.placementId; + expect(spec.isBidRequestValid(bid)).to.be.false; + }); + }); + + describe('buildRequests', function () { + let serverRequest = spec.buildRequests([bid]); + serverRequest = serverRequest[0] + it('Creates a ServerRequest object with method, URL and data', function () { + expect(serverRequest).to.exist; + expect(serverRequest.method).to.exist; + expect(serverRequest.url).to.exist; + }); + it('Returns GET method', function () { + expect(serverRequest.method).to.equal('GET'); + }); + it('Returns valid URL', function () { + expect(serverRequest.url).to.equal('https://bidder.doceree.com/v1/adrequest?id=DOC_7jm9j5eqkl0xvc5w&pubRequestedURL=undefined&loggedInUser=JTdCJTIyZ2VuZGVyJTIyJTNBJTIyJTIyJTJDJTIyZW1haWwlMjIlM0ElMjIlMjIlMkMlMjJoYXNoZWRFbWFpbCUyMiUzQSUyMiUyMiUyQyUyMmZpcnN0TmFtZSUyMiUzQSUyMiUyMiUyQyUyMmxhc3ROYW1lJTIyJTNBJTIyJTIyJTJDJTIybnBpJTIyJTNBJTIyJTIyJTJDJTIyaGFzaGVkTlBJJTIyJTNBJTIyJTIyJTJDJTIyY2l0eSUyMiUzQSUyMiUyMiUyQyUyMnppcENvZGUlMjIlM0ElMjIlMjIlMkMlMjJzcGVjaWFsaXphdGlvbiUyMiUzQSUyMiUyMiU3RA%3D%3D&prebidjs=true&requestId=testing&'); + }); + }); + describe('interpretResponse', function () { + it('Should interpret banner response', function () { + const banner = { + body: { + DIVID: 'DOC_7jm9j5eqkl0xvc5w', + creativeType: 'banner', + guid: 'G125fzC5NKl3FHeOT8yvL98ILfQS9TVUgk6Q', + currency: 'USD', + cpmBid: 2, + height: '250', + width: '300', + ctaLink: 'https://doceree.com/', + sourceURL: '', + sourceHTML: '
test
', + advertiserDomain: 'doceree.com', + } + }; + let bannerResponses = spec.interpretResponse(banner); + expect(bannerResponses).to.be.an('array').that.is.not.empty; + let dataItem = bannerResponses[0]; + expect(dataItem).to.have.all.keys('requestId', 'cpm', 'width', 'height', 'ad', 'ttl', + 'netRevenue', 'currency', 'mediaType', 'creativeId', 'meta'); + expect(dataItem.requestId).to.equal('G125fzC5NKl3FHeOT8yvL98ILfQS9TVUgk6Q'); + expect(dataItem.cpm).to.equal(2); + expect(dataItem.width).to.equal(300); + expect(dataItem.height).to.equal(250); + expect(dataItem.ad).to.equal('
test
'); + expect(dataItem.ttl).to.equal(30); + expect(dataItem.netRevenue).to.be.true; + expect(dataItem.currency).to.equal('USD'); + expect(dataItem.creativeId).to.equal('DOC_7jm9j5eqkl0xvc5w'); + expect(dataItem.meta.advertiserDomains).to.be.an('array').that.is.not.empty; + expect(dataItem.meta.advertiserDomains[0]).to.equal('doceree.com') + }); + }) +}); diff --git a/test/spec/modules/dspxBidAdapter_spec.js b/test/spec/modules/dspxBidAdapter_spec.js index cf36c3f62c4..87752f7747a 100644 --- a/test/spec/modules/dspxBidAdapter_spec.js +++ b/test/spec/modules/dspxBidAdapter_spec.js @@ -283,4 +283,60 @@ describe('dspxAdapter', function () { expect(result.length).to.equal(0); }); }); + + describe(`getUserSyncs test usage`, function () { + let serverResponses; + + beforeEach(function () { + serverResponses = [{ + body: { + requestId: '23beaa6af6cdde', + cpm: 0.5, + width: 0, + height: 0, + creativeId: 100500, + dealId: '', + currency: 'EUR', + netRevenue: true, + ttl: 300, + type: 'sspHTML', + ad: '', + userSync: { + iframeUrl: ['anyIframeUrl?a=1'], + imageUrl: ['anyImageUrl', 'anyImageUrl2'] + } + } + }]; + }); + + it(`return value should be an array`, function () { + expect(spec.getUserSyncs({ iframeEnabled: true })).to.be.an('array'); + }); + it(`array should have only one object and it should have a property type = 'iframe'`, function () { + expect(spec.getUserSyncs({ iframeEnabled: true }, serverResponses).length).to.be.equal(1); + let [userSync] = spec.getUserSyncs({ iframeEnabled: true }, serverResponses); + expect(userSync).to.have.property('type'); + expect(userSync.type).to.be.equal('iframe'); + }); + it(`we have valid sync url for iframe`, function () { + let [userSync] = spec.getUserSyncs({ iframeEnabled: true }, serverResponses, {consentString: 'anyString'}); + expect(userSync.url).to.be.equal('anyIframeUrl?a=1&gdpr_consent=anyString') + expect(userSync.type).to.be.equal('iframe'); + }); + it(`we have valid sync url for image`, function () { + let [userSync] = spec.getUserSyncs({ pixelEnabled: true }, serverResponses, {gdprApplies: true, consentString: 'anyString'}); + expect(userSync.url).to.be.equal('anyImageUrl?gdpr=1&gdpr_consent=anyString') + expect(userSync.type).to.be.equal('image'); + }); + it(`we have valid sync url for image and iframe`, function () { + let userSync = spec.getUserSyncs({ iframeEnabled: true, pixelEnabled: true }, serverResponses, {gdprApplies: true, consentString: 'anyString'}); + expect(userSync.length).to.be.equal(3); + expect(userSync[0].url).to.be.equal('anyIframeUrl?a=1&gdpr=1&gdpr_consent=anyString') + expect(userSync[0].type).to.be.equal('iframe'); + expect(userSync[1].url).to.be.equal('anyImageUrl?gdpr=1&gdpr_consent=anyString') + expect(userSync[1].type).to.be.equal('image'); + expect(userSync[2].url).to.be.equal('anyImageUrl2?gdpr=1&gdpr_consent=anyString') + expect(userSync[2].type).to.be.equal('image'); + }); + }); }); diff --git a/test/spec/modules/eids_spec.js b/test/spec/modules/eids_spec.js index 434e1841a0f..1ccaab2b302 100644 --- a/test/spec/modules/eids_spec.js +++ b/test/spec/modules/eids_spec.js @@ -29,29 +29,73 @@ describe('eids array generation for known sub-modules', function() { }); }); - it('id5Id', function() { + describe('id5Id', function() { + it('does not include an ext if not provided', function() { + const userId = { + id5id: { + uid: 'some-random-id-value' + } + }; + const newEids = createEidsArray(userId); + expect(newEids.length).to.equal(1); + expect(newEids[0]).to.deep.equal({ + source: 'id5-sync.com', + uids: [{ id: 'some-random-id-value', atype: 1 }] + }); + }); + + it('includes ext if provided', function() { + const userId = { + id5id: { + uid: 'some-random-id-value', + ext: { + linkType: 0 + } + } + }; + const newEids = createEidsArray(userId); + 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 + } + }] + }); + }); + }); + + it('parrableId', function() { const userId = { - id5id: 'some-random-id-value' + parrableId: { + eid: 'some-random-id-value' + } }; const newEids = createEidsArray(userId); expect(newEids.length).to.equal(1); expect(newEids[0]).to.deep.equal({ - source: 'id5-sync.com', + source: 'parrable.com', uids: [{id: 'some-random-id-value', atype: 1}] }); }); - it('parrableId', function() { + it('merkleId', function() { const userId = { - parrableId: { - eid: 'some-random-id-value' + merkleId: { + id: 'some-random-id-value', keyID: 1 } }; const newEids = createEidsArray(userId); expect(newEids.length).to.equal(1); expect(newEids[0]).to.deep.equal({ - source: 'parrable.com', - uids: [{id: 'some-random-id-value', atype: 1}] + source: 'merkleinc.com', + uids: [{id: 'some-random-id-value', + atype: 3, + ext: { keyID: 1 + }}] }); }); @@ -63,7 +107,7 @@ describe('eids array generation for known sub-modules', function() { expect(newEids.length).to.equal(1); expect(newEids[0]).to.deep.equal({ source: 'liveramp.com', - uids: [{id: 'some-random-id-value', atype: 1}] + uids: [{id: 'some-random-id-value', atype: 3}] }); }); @@ -78,7 +122,7 @@ describe('eids array generation for known sub-modules', function() { expect(newEids.length).to.equal(1); expect(newEids[0]).to.deep.equal({ source: 'liveintent.com', - uids: [{id: 'some-random-id-value', atype: 1}], + uids: [{id: 'some-random-id-value', atype: 3}], ext: {segments: ['s1', 's2']} }); }); @@ -93,7 +137,7 @@ describe('eids array generation for known sub-modules', function() { expect(newEids.length).to.equal(1); expect(newEids[0]).to.deep.equal({ source: 'liveintent.com', - uids: [{id: 'some-random-id-value', atype: 1}] + uids: [{id: 'some-random-id-value', atype: 3}] }); }); @@ -105,7 +149,7 @@ describe('eids array generation for known sub-modules', function() { expect(newEids.length).to.equal(1); expect(newEids[0]).to.deep.equal({ source: 'britepool.com', - uids: [{id: 'some-random-id-value', atype: 1}] + uids: [{id: 'some-random-id-value', atype: 3}] }); }); @@ -133,6 +177,30 @@ 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('deepintentId', function() { + const userId = { + deepintentId: 'some-random-id-value' + }; + const newEids = createEidsArray(userId); + expect(newEids.length).to.equal(1); + expect(newEids[0]).to.deep.equal({ + source: 'deepintent.com', + uids: [{id: 'some-random-id-value', atype: 3}] + }); + }); + it('NetId', function() { const userId = { netId: 'some-random-id-value' @@ -144,6 +212,18 @@ describe('eids array generation for known sub-modules', function() { uids: [{id: 'some-random-id-value', atype: 1}] }); }); + + it('NextRollId', function() { + const userId = { + nextrollId: 'some-random-id-value' + }; + const newEids = createEidsArray(userId); + expect(newEids.length).to.equal(1); + expect(newEids[0]).to.deep.equal({ + source: 'nextroll.com', + uids: [{id: 'some-random-id-value', atype: 1}] + }); + }); it('Sharedid', function() { const userId = { sharedid: { @@ -180,8 +260,101 @@ describe('eids array generation for known sub-modules', function() { }] }); }); -}); + it('zeotapIdPlus', function() { + const userId = { + IDP: 'some-random-id-value' + }; + const newEids = createEidsArray(userId); + expect(newEids.length).to.equal(1); + expect(newEids[0]).to.deep.equal({ + source: 'zeotap.com', + uids: [{ + id: 'some-random-id-value', + atype: 1 + }] + }); + }); + + it('haloId', function() { + const userId = { + haloId: 'some-random-id-value' + }; + const newEids = createEidsArray(userId); + expect(newEids.length).to.equal(1); + expect(newEids[0]).to.deep.equal({ + source: 'audigent.com', + uids: [{ + id: 'some-random-id-value', + atype: 1 + }] + }); + }); + + it('quantcastId', function() { + const userId = { + quantcastId: 'some-random-id-value' + }; + const newEids = createEidsArray(userId); + expect(newEids.length).to.equal(1); + expect(newEids[0]).to.deep.equal({ + source: 'quantcast.com', + uids: [{ + id: 'some-random-id-value', + atype: 1 + }] + }); + }); + it('uid2', function() { + const userId = { + uid2: {'id': 'Sample_AD_Token'} + }; + const newEids = createEidsArray(userId); + expect(newEids.length).to.equal(1); + expect(newEids[0]).to.deep.equal({ + source: 'uidapi.com', + uids: [{ + id: 'Sample_AD_Token', + atype: 3 + }] + }); + }); + it('pubProvidedId', function() { + const userId = { + pubProvidedId: [{ + source: 'example.com', + uids: [{ + id: 'value read from cookie or local storage', + ext: { + stype: 'ppuid' + } + }] + }, { + source: 'id-partner.com', + uids: [{ + id: 'value read from cookie or local storage' + }] + }] + }; + const newEids = createEidsArray(userId); + expect(newEids.length).to.equal(2); + expect(newEids[0]).to.deep.equal({ + source: 'example.com', + uids: [{ + id: 'value read from cookie or local storage', + ext: { + stype: 'ppuid' + } + }] + }); + expect(newEids[1]).to.deep.equal({ + source: 'id-partner.com', + uids: [{ + id: 'value read from cookie or local storage' + }] + }); + }); +}); describe('Negative case', function() { it('eids array generation for UN-known sub-module', function() { // UnknownCommonId diff --git a/test/spec/modules/emx_digitalBidAdapter_spec.js b/test/spec/modules/emx_digitalBidAdapter_spec.js index 138786b9c74..855ffa0a0b7 100644 --- a/test/spec/modules/emx_digitalBidAdapter_spec.js +++ b/test/spec/modules/emx_digitalBidAdapter_spec.js @@ -367,6 +367,27 @@ describe('emx_digital Adapter', function () { expect(request.us_privacy).to.exist; expect(request.us_privacy).to.exist.and.to.equal(consentString); }); + + it('should add schain object to request', function() { + const schainBidderRequest = utils.deepClone(bidderRequest); + schainBidderRequest.bids[0].schain = { + 'complete': 1, + 'ver': '1.0', + 'nodes': [ + { + 'asi': 'testing.com', + 'sid': 'abc', + 'hp': 1 + } + ] + }; + let request = spec.buildRequests(schainBidderRequest.bids, schainBidderRequest); + request = JSON.parse(request.data); + expect(request.source.ext.schain).to.exist; + expect(request.source.ext.schain).to.have.property('complete', 1); + expect(request.source.ext.schain).to.have.property('ver', '1.0'); + expect(request.source.ext.schain.nodes[0].asi).to.equal(schainBidderRequest.bids[0].schain.nodes[0].asi); + }); }); describe('interpretResponse', function () { 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/enrichmentFpdModule_spec.js b/test/spec/modules/enrichmentFpdModule_spec.js new file mode 100644 index 00000000000..e5271143f2c --- /dev/null +++ b/test/spec/modules/enrichmentFpdModule_spec.js @@ -0,0 +1,97 @@ +import { expect } from 'chai'; +import * as utils from 'src/utils.js'; +import { getRefererInfo } from 'src/refererDetection.js'; +import { initSubmodule } from 'modules/enrichmentFpdModule.js'; + +describe('the first party data enrichment module', function() { + let width; + let widthStub; + let height; + let heightStub; + let querySelectorStub; + let canonical; + let keywords; + + before(function() { + canonical = document.createElement('link'); + canonical.rel = 'canonical'; + keywords = document.createElement('meta'); + keywords.name = 'keywords'; + }); + + beforeEach(function() { + querySelectorStub = sinon.stub(window.top.document, 'querySelector'); + querySelectorStub.withArgs("link[rel='canonical']").returns(canonical); + querySelectorStub.withArgs("meta[name='keywords']").returns(keywords); + widthStub = sinon.stub(window.top, 'innerWidth').get(function() { + return width; + }); + heightStub = sinon.stub(window.top, 'innerHeight').get(function() { + return height; + }); + }); + + afterEach(function() { + widthStub.restore(); + heightStub.restore(); + querySelectorStub.restore(); + canonical = document.createElement('link'); + canonical.rel = 'canonical'; + keywords = document.createElement('meta'); + keywords.name = 'keywords'; + }); + + it('adds ref and device values', function() { + width = 800; + height = 500; + + let validated = initSubmodule({}, {}); + + expect(validated.site.ref).to.equal(getRefererInfo().referer); + expect(validated.site.page).to.be.undefined; + expect(validated.site.domain).to.be.undefined; + expect(validated.device).to.deep.equal({ w: 800, h: 500 }); + expect(validated.site.keywords).to.be.undefined; + }); + + it('adds page and domain values if canonical url exists', function() { + width = 800; + height = 500; + canonical.href = 'https://www.domain.com/path?query=12345'; + + let validated = initSubmodule({}, {}); + + expect(validated.site.ref).to.equal(getRefererInfo().referer); + expect(validated.site.page).to.equal('https://www.domain.com/path?query=12345'); + expect(validated.site.domain).to.equal('domain.com'); + expect(validated.device).to.deep.equal({ w: 800, h: 500 }); + expect(validated.site.keywords).to.be.undefined; + }); + + it('adds keyword value if keyword meta content exists', function() { + width = 800; + height = 500; + keywords.content = 'value1,value2,value3'; + + let validated = initSubmodule({}, {}); + + expect(validated.site.ref).to.equal(getRefererInfo().referer); + expect(validated.site.page).to.be.undefined; + expect(validated.site.domain).to.be.undefined; + expect(validated.device).to.deep.equal({ w: 800, h: 500 }); + expect(validated.site.keywords).to.equal('value1,value2,value3'); + }); + + it('does not overwrite existing data from getConfig ortb2', function() { + width = 800; + height = 500; + + let validated = initSubmodule({}, {device: {w: 1200, h: 700}, site: {ref: 'https://someUrl.com', page: 'test.com'}}); + + expect(validated.site.ref).to.equal('https://someUrl.com'); + expect(validated.site.page).to.equal('test.com'); + expect(validated.site.domain).to.be.undefined; + expect(validated.device).to.deep.equal({ w: 1200, h: 700 }); + expect(validated.site.keywords).to.be.undefined; + }); +}); diff --git a/test/spec/modules/eplanningBidAdapter_spec.js b/test/spec/modules/eplanningBidAdapter_spec.js index f267680b46b..1d3a8344170 100644 --- a/test/spec/modules/eplanningBidAdapter_spec.js +++ b/test/spec/modules/eplanningBidAdapter_spec.js @@ -1,6 +1,9 @@ import { expect } from 'chai'; import { spec, storage } from 'modules/eplanningBidAdapter.js'; import { newBidder } from 'src/adapters/bidderFactory.js'; +import { config } from 'src/config.js'; +import { init } from 'modules/userId/index.js'; +import * as utils from 'src/utils.js'; describe('E-Planning Adapter', function () { const adapter = newBidder('spec'); @@ -32,6 +35,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 +55,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 +108,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,9 +293,30 @@ 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'); + expect(url).to.equal('https://ads.us.e-planning.net/pbjs/1/' + CI + '/1/localhost/ROS'); }); it('should return GET method', function () { @@ -257,11 +324,6 @@ describe('E-Planning Adapter', function () { expect(method).to.equal('GET'); }); - it('should return r parameter with value pbjs', function () { - const r = spec.buildRequests(bidRequests, bidderRequest).data.r; - expect(r).to.equal('pbjs'); - }); - it('should return pbv parameter with value prebid version', function () { const pbv = spec.buildRequests(bidRequests, bidderRequest).data.pbv; expect(pbv).to.equal('$prebid.version$'); @@ -278,6 +340,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 +382,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 +473,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 () { @@ -583,7 +687,7 @@ describe('E-Planning Adapter', function () { hasLocalStorageStub.returns(false); const response = spec.buildRequests(bidRequests, bidderRequest); - expect(response.url).to.equal('https://ads.us.e-planning.net/hb/1/' + CI + '/1/localhost/ROS'); + expect(response.url).to.equal('https://ads.us.e-planning.net/pbjs/1/' + CI + '/1/localhost/ROS'); expect(response.data.vs).to.equal('F'); sinon.assert.notCalled(getLocalStorageSpy); @@ -593,7 +697,7 @@ describe('E-Planning Adapter', function () { it('should create the url correctly with LocalStorage', function() { createElementVisible(); const response = spec.buildRequests(bidRequests, bidderRequest); - expect(response.url).to.equal('https://ads.us.e-planning.net/hb/1/' + CI + '/1/localhost/ROS'); + expect(response.url).to.equal('https://ads.us.e-planning.net/pbjs/1/' + CI + '/1/localhost/ROS'); expect(response.data.vs).to.equal('F'); @@ -796,4 +900,26 @@ describe('E-Planning Adapter', function () { }); }); }); + describe('Send eids', function() { + it('should add eids to the request', function() { + init(config); + config.setConfig({ + userSync: { + userIds: [ + { name: 'id5Id', value: { 'id5id': { uid: 'ID5-ZHMOL_IfFSt7_lVYX8rBZc6GH3XMWyPQOBUfr4bm0g!', ext: { linkType: 1 } } } }, + { name: 'pubCommonId', value: {'pubcid': 'c29cb2ae-769d-42f6-891a-f53cadee823d'} }, + { name: 'unifiedId', value: {'tdid': 'D6885E90-2A7A-4E0F-87CB-7734ED1B99A3'} } + ] + } + }); + let bidRequests = [validBidView]; + const expected_id5id = encodeURIComponent(JSON.stringify({ uid: 'ID5-ZHMOL_IfFSt7_lVYX8rBZc6GH3XMWyPQOBUfr4bm0g!', ext: { linkType: 1 } })); + const request = spec.buildRequests(bidRequests, bidderRequest); + const dataRequest = request.data; + + expect('D6885E90-2A7A-4E0F-87CB-7734ED1B99A3').to.equal(dataRequest.e_tdid); + expect('c29cb2ae-769d-42f6-891a-f53cadee823d').to.equal(dataRequest.e_pubcid); + expect(expected_id5id).to.equal(dataRequest.e_id5id); + }); + }); }); diff --git a/test/spec/modules/etargetBidAdapter_spec.js b/test/spec/modules/etargetBidAdapter_spec.js index 4f5e0c224ec..2dbf6cd68c5 100644 --- a/test/spec/modules/etargetBidAdapter_spec.js +++ b/test/spec/modules/etargetBidAdapter_spec.js @@ -154,6 +154,7 @@ describe('etarget adapter', function () { assert.equal(result.height, 250); assert.equal(result.currency, 'EUR'); assert.equal(result.netRevenue, true); + assert.isNotNull(result.reason); assert.equal(result.ttl, 360); assert.equal(result.ad, ''); assert.equal(result.transactionId, '5f33781f-9552-4ca1'); diff --git a/test/spec/modules/fabrickIdSystem_spec.js b/test/spec/modules/fabrickIdSystem_spec.js new file mode 100644 index 00000000000..c250c8e5e8b --- /dev/null +++ b/test/spec/modules/fabrickIdSystem_spec.js @@ -0,0 +1,134 @@ +import * as utils from '../../../src/utils.js'; +import {server} from '../../mocks/xhr.js'; + +import {fabrickIdSubmodule, appendUrl} from 'modules/fabrickIdSystem.js'; + +const defaultConfigParams = { + apiKey: '123', + e: 'abc', + p: ['def', 'hij'], + url: 'http://localhost:9999/test/mocks/fabrickId.json?' +}; +const responseHeader = {'Content-Type': 'application/json'} + +describe('Fabrick ID System', function() { + let logErrorStub; + + beforeEach(function () { + }); + + afterEach(function () { + }); + + 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 + }).callback; + let callBackSpy = sinon.spy(); + submoduleCallback(callBackSpy); + let request = server.requests[0]; + request.respond( + 200, + responseHeader, + '] this is not json {' + ); + 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 < 1500; i++) { + r += 'r'; + } + let configParams = Object.assign({}, defaultConfigParams, { + refererInfo: { + referer: r, + stack: ['s-0'], + canonicalUrl: 'cu-0' + } + }); + let submoduleCallback = fabrickIdSubmodule.getId({ + name: 'fabrickId', + params: configParams + }).callback; + let callBackSpy = sinon.spy(); + submoduleCallback(callBackSpy); + let request = server.requests[0]; + r = ''; + for (let i = 0; i < 1000 - 3; i++) { + r += 'r'; + } + expect(request.url).to.match(new RegExp(`r=${r}&r=`)); + request.respond( + 200, + responseHeader, + JSON.stringify({}) + ); + expect(callBackSpy.calledOnce).to.be.true; + }); + + it('should complete successfully', function() { + let configParams = Object.assign({}, defaultConfigParams, { + refererInfo: { + referer: 'r-0', + stack: ['s-0'], + canonicalUrl: 'cu-0' + } + }); + let submoduleCallback = fabrickIdSubmodule.getId({ + name: 'fabrickId', + params: configParams + }).callback; + let callBackSpy = sinon.spy(); + submoduleCallback(callBackSpy); + let request = server.requests[0]; + expect(request.url).to.match(/r=r-0&r=s-0&r=cu-0&r=http/); + request.respond( + 200, + responseHeader, + JSON.stringify({}) + ); + expect(callBackSpy.calledOnce).to.be.true; + }); + + 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/feedadBidAdapter_spec.js b/test/spec/modules/feedadBidAdapter_spec.js index 066ab6b21f6..6b75af0d55d 100644 --- a/test/spec/modules/feedadBidAdapter_spec.js +++ b/test/spec/modules/feedadBidAdapter_spec.js @@ -13,7 +13,7 @@ describe('FeedAdAdapter', function () { it('should only support video and banner ads', function () { expect(spec.supportedMediaTypes).to.be.a('array'); expect(spec.supportedMediaTypes).to.include(BANNER); - expect(spec.supportedMediaTypes).to.include(VIDEO); + expect(spec.supportedMediaTypes).not.to.include(VIDEO); expect(spec.supportedMediaTypes).not.to.include(NATIVE); }); it('should export the BidderSpec functions', function () { @@ -23,6 +23,9 @@ describe('FeedAdAdapter', function () { expect(spec.onTimeout).to.be.a('function'); expect(spec.onBidWon).to.be.a('function'); }); + it('should export the TCF vendor ID', function () { + expect(spec.gvlid).to.equal(781); + }) }); describe('isBidRequestValid', function () { @@ -248,6 +251,40 @@ describe('FeedAdAdapter', function () { let result = spec.buildRequests([bid, bid, bid]); expect(result).to.be.empty; }); + it('should not include GDPR data if the bidder request has none available', function () { + let bid = { + code: 'feedad', + mediaTypes: { + banner: { + sizes: [[320, 250]] + } + }, + params: {clientToken: 'clientToken', placementId: 'placement-id'} + }; + let result = spec.buildRequests([bid], bidderRequest); + expect(result.data.gdprApplies).to.be.undefined; + expect(result.data.consentIabTcf).to.be.undefined; + }); + it('should include GDPR data if the bidder requests contains it', function () { + let bid = { + code: 'feedad', + mediaTypes: { + banner: { + sizes: [[320, 250]] + } + }, + params: {clientToken: 'clientToken', placementId: 'placement-id'} + }; + let request = Object.assign({}, bidderRequest, { + gdprConsent: { + consentString: 'the consent string', + gdprApplies: true + } + }); + let result = spec.buildRequests([bid], request); + expect(result.data.gdprApplies).to.equal(request.gdprConsent.gdprApplies); + expect(result.data.consentIabTcf).to.equal(request.gdprConsent.consentString); + }); }); describe('interpretResponse', function () { @@ -404,7 +441,7 @@ describe('FeedAdAdapter', function () { prebid_bid_id: bidId, prebid_transaction_id: transactionId, referer, - sdk_version: '1.0.0' + sdk_version: '1.0.2' }; subject(data); expect(server.requests.length).to.equal(1); diff --git a/test/spec/modules/fintezaAnalyticsAdapter_spec.js b/test/spec/modules/fintezaAnalyticsAdapter_spec.js index 29136c85241..4c76f79f518 100644 --- a/test/spec/modules/fintezaAnalyticsAdapter_spec.js +++ b/test/spec/modules/fintezaAnalyticsAdapter_spec.js @@ -12,7 +12,7 @@ function setCookie(name, value, expires) { document.cookie = name + '=' + value + '; path=/' + (expires ? ('; expires=' + expires.toUTCString()) : '') + - '; SameSite=None'; + '; SameSite=Lax'; } describe('finteza analytics adapter', function () { diff --git a/test/spec/modules/fpdModule_spec.js b/test/spec/modules/fpdModule_spec.js new file mode 100644 index 00000000000..c2a6c41835e --- /dev/null +++ b/test/spec/modules/fpdModule_spec.js @@ -0,0 +1,464 @@ +import {expect} from 'chai'; +import * as utils from 'src/utils.js'; +import {config} from 'src/config.js'; +import {getRefererInfo} from 'src/refererDetection.js'; +import {init, registerSubmodules} from 'modules/fpdModule/index.js'; +import * as enrichmentModule from 'modules/enrichmentFpdModule.js'; +import * as validationModule from 'modules/validationFpdModule/index.js'; + +let enrichments = { + name: 'enrichments', + queue: 2, + init: enrichmentModule.initSubmodule +}; +let validations = { + name: 'validations', + queue: 1, + init: validationModule.initSubmodule +}; + +describe('the first party data module', function () { + let ortb2 = { + device: { + h: 911, + w: 1733 + }, + user: { + data: [{ + segment: [{ + id: 'foo' + }], + name: 'bar', + ext: 'string' + }] + }, + site: { + content: { + data: [{ + segment: [{ + id: 'test' + }], + name: 'content', + ext: { + foo: 'bar' + } + }] + } + } + }; + + let conf = { + device: { + h: 500, + w: 750 + }, + user: { + keywords: 'test1, test2', + gender: 'f', + data: [{ + segment: [{ + id: 'test' + }], + name: 'alt' + }] + }, + site: { + ref: 'domain.com', + page: 'www.domain.com/test', + ext: { + data: { + inventory: ['first'] + } + } + } + }; + + afterEach(function () { + config.resetConfig(); + }); + + describe('first party data intitializing', function () { + let width; + let widthStub; + let height; + let heightStub; + let querySelectorStub; + let canonical; + let keywords; + + before(function() { + canonical = document.createElement('link'); + canonical.rel = 'canonical'; + keywords = document.createElement('meta'); + keywords.name = 'keywords'; + }); + + beforeEach(function() { + querySelectorStub = sinon.stub(window.top.document, 'querySelector'); + querySelectorStub.withArgs("link[rel='canonical']").returns(canonical); + querySelectorStub.withArgs("meta[name='keywords']").returns(keywords); + widthStub = sinon.stub(window.top, 'innerWidth').get(function () { + return width; + }); + heightStub = sinon.stub(window.top, 'innerHeight').get(function () { + return height; + }); + }); + + afterEach(function() { + widthStub.restore(); + heightStub.restore(); + querySelectorStub.restore(); + canonical = document.createElement('link'); + canonical.rel = 'canonical'; + keywords = document.createElement('meta'); + keywords.name = 'keywords'; + }); + + it('sets default referer and dimension values to ortb2 data', function () { + registerSubmodules(enrichments); + registerSubmodules(validations); + + let validated; + + width = 1120; + height = 750; + + init(); + + validated = config.getConfig('ortb2'); + expect(validated.site.ref).to.equal(getRefererInfo().referer); + expect(validated.site.page).to.be.undefined; + expect(validated.site.domain).to.be.undefined; + expect(validated.device).to.deep.equal({w: 1120, h: 750}); + expect(validated.site.keywords).to.be.undefined; + }); + + it('sets page and domain values to ortb2 data if canonical link exists', function () { + let validated; + + canonical.href = 'https://www.domain.com/path?query=12345'; + + init(); + + validated = config.getConfig('ortb2'); + expect(validated.site.ref).to.equal(getRefererInfo().referer); + expect(validated.site.page).to.equal('https://www.domain.com/path?query=12345'); + expect(validated.site.domain).to.equal('domain.com'); + expect(validated.device).to.deep.to.equal({w: 1120, h: 750}); + expect(validated.site.keywords).to.be.undefined; + }); + + it('sets keyword values to ortb2 data if keywords meta exists', function () { + let validated; + + keywords.content = 'value1,value2,value3'; + + init(); + + validated = config.getConfig('ortb2'); + expect(validated.site.ref).to.equal(getRefererInfo().referer); + expect(validated.site.page).to.be.undefined; + expect(validated.site.domain).to.be.undefined; + expect(validated.device).to.deep.to.equal({w: 1120, h: 750}); + expect(validated.site.keywords).to.equal('value1,value2,value3'); + }); + + it('only sets values that do not exist in ortb2 config', function () { + let validated; + + config.setConfig({ortb2: {site: {ref: 'https://testpage.com', domain: 'newDomain.com'}}}); + + init(); + + validated = config.getConfig('ortb2'); + expect(validated.site.ref).to.equal('https://testpage.com'); + expect(validated.site.page).to.be.undefined; + expect(validated.site.domain).to.equal('newDomain.com'); + expect(validated.device).to.deep.to.equal({w: 1120, h: 750}); + expect(validated.site.keywords).to.be.undefined; + }); + + it('filters ortb2 data that is set', function () { + let validated; + let conf = { + ortb2: { + user: { + data: {}, + gender: 'f', + age: 45 + }, + site: { + content: { + data: [{ + segment: { + test: 1 + }, + name: 'foo' + }, { + segment: [{ + id: 'test' + }, { + id: 3 + }], + name: 'bar' + }] + } + }, + device: { + w: 1, + h: 1 + } + } + }; + + config.setConfig(conf); + canonical.href = 'https://www.domain.com/path?query=12345'; + width = 1120; + height = 750; + + init(); + + validated = config.getConfig('ortb2'); + expect(validated.site.ref).to.equal(getRefererInfo().referer); + expect(validated.site.page).to.equal('https://www.domain.com/path?query=12345'); + expect(validated.site.domain).to.equal('domain.com'); + expect(validated.site.content.data).to.deep.equal([{segment: [{id: 'test'}], name: 'bar'}]); + expect(validated.user.data).to.be.undefined; + expect(validated.device).to.deep.to.equal({w: 1, h: 1}); + expect(validated.site.keywords).to.be.undefined; + }); + + it('should not overwrite existing data with default settings', function () { + let validated; + let conf = { + ortb2: { + site: { + ref: 'https://referer.com' + } + } + }; + + config.setConfig(conf); + + init(); + + validated = config.getConfig('ortb2'); + expect(validated.site.ref).to.equal('https://referer.com'); + }); + + it('should allow overwrite default data with setConfig', function () { + let validated; + let conf = { + ortb2: { + site: { + ref: 'https://referer.com' + } + } + }; + + config.setConfig(conf); + + init(); + + validated = config.getConfig('ortb2'); + expect(validated.site.ref).to.equal('https://referer.com'); + }); + + it('should filter all data', function () { + let validated; + let conf = { + imp: [], + site: { + name: 123, + domain: 456, + page: 789, + ref: 987, + keywords: ['keywords'], + search: 654, + cat: 'cat', + sectioncat: 'sectioncat', + pagecat: 'pagecat', + content: { + data: [{ + name: 1, + segment: [] + }] + } + }, + user: { + yob: 'twenty', + gender: 0, + keywords: ['foobar'], + data: ['test'] + }, + device: [800, 450], + cur: { + adServerCurrency: 'USD' + } + }; + + config.setConfig({'firstPartyData': {skipEnrichments: true}}); + + config.setConfig({ortb2: conf}); + + init(); + + validated = config.getConfig('ortb2'); + expect(validated).to.deep.equal({}); + }); + + it('should add enrichments but not alter any arbitrary ortb2 data', function () { + let validated; + let conf = { + site: { + ext: { + data: { + inventory: ['value1'] + } + } + }, + user: { + ext: { + data: { + visitor: ['value2'] + } + } + }, + cur: ['USD'] + }; + + config.setConfig({ortb2: conf}); + + init(); + + validated = config.getConfig('ortb2'); + expect(validated.site.ref).to.equal(getRefererInfo().referer); + expect(validated.site.ext.data).to.deep.equal({inventory: ['value1']}); + expect(validated.user.ext.data).to.deep.equal({visitor: ['value2']}); + expect(validated.cur).to.deep.equal(['USD']); + }); + + it('should filter bidderConfig data', function () { + let validated; + let conf = { + bidders: ['bidderA', 'bidderB'], + config: { + ortb2: { + site: { + keywords: 'other', + ref: 'https://domain.com' + }, + user: { + keywords: 'test', + data: [{ + segment: [{id: 4}], + name: 't' + }] + } + } + } + }; + + config.setBidderConfig(conf); + + init(); + + validated = config.getBidderConfig(); + expect(validated.bidderA.ortb2).to.not.be.undefined; + expect(validated.bidderA.ortb2.user.data).to.be.undefined; + expect(validated.bidderA.ortb2.user.keywords).to.equal('test'); + expect(validated.bidderA.ortb2.site.keywords).to.equal('other'); + expect(validated.bidderA.ortb2.site.ref).to.equal('https://domain.com'); + }); + + it('should not filter bidderConfig data as it is valid', function () { + let validated; + let conf = { + bidders: ['bidderA', 'bidderB'], + config: { + ortb2: { + site: { + keywords: 'other', + ref: 'https://domain.com' + }, + user: { + keywords: 'test', + data: [{ + segment: [{id: 'data1_id'}], + name: 'data1' + }] + } + } + } + }; + + config.setBidderConfig(conf); + + init(); + + validated = config.getBidderConfig(); + expect(validated.bidderA.ortb2).to.not.be.undefined; + expect(validated.bidderA.ortb2.user.data).to.deep.equal([{segment: [{id: 'data1_id'}], name: 'data1'}]); + expect(validated.bidderA.ortb2.user.keywords).to.equal('test'); + expect(validated.bidderA.ortb2.site.keywords).to.equal('other'); + expect(validated.bidderA.ortb2.site.ref).to.equal('https://domain.com'); + }); + + it('should not set default values if skipEnrichments is turned on', function () { + let validated; + config.setConfig({'firstPartyData': {skipEnrichments: true}}); + + let conf = { + site: { + keywords: 'other' + }, + user: { + keywords: 'test', + data: [{ + segment: [{id: 'data1_id'}], + name: 'data1' + }] + } + } + ; + + config.setConfig({ortb2: conf}); + + init(); + + validated = config.getConfig(); + expect(validated.ortb2).to.not.be.undefined; + expect(validated.ortb2.device).to.be.undefined; + expect(validated.ortb2.site.ref).to.be.undefined; + expect(validated.ortb2.site.page).to.be.undefined; + expect(validated.ortb2.site.domain).to.be.undefined; + }); + + it('should not validate ortb2 data if skipValidations is turned on', function () { + let validated; + config.setConfig({'firstPartyData': {skipValidations: true}}); + + let conf = { + site: { + keywords: 'other' + }, + user: { + keywords: 'test', + data: [{ + segment: [{id: 'nonfiltered'}] + }] + } + } + ; + + config.setConfig({ortb2: conf}); + + init(); + + validated = config.getConfig(); + expect(validated.ortb2).to.not.be.undefined; + expect(validated.ortb2.user.data).to.deep.equal([{segment: [{id: 'nonfiltered'}]}]); + }); + }); +}); diff --git a/test/spec/modules/freewheel-sspBidAdapter_spec.js b/test/spec/modules/freewheel-sspBidAdapter_spec.js index 3047b635d13..e416bd1c26b 100644 --- a/test/spec/modules/freewheel-sspBidAdapter_spec.js +++ b/test/spec/modules/freewheel-sspBidAdapter_spec.js @@ -152,6 +152,19 @@ describe('freewheelSSP BidAdapter Test', () => { expect(payload.playerSize).to.equal('300x600'); expect(payload._fw_gdpr_consent).to.exist.and.to.be.a('string'); expect(payload._fw_gdpr_consent).to.equal(gdprConsentString); + + let gdprConsent = { + 'gdprApplies': true, + 'consentString': gdprConsentString + } + let syncOptions = { + 'pixelEnabled': true + } + const userSyncs = spec.getUserSyncs(syncOptions, null, gdprConsent, null); + expect(userSyncs).to.deep.equal([{ + type: 'image', + url: 'https://ads.stickyadstv.com/auto-user-sync?gdpr=1&gdpr_consent=1FW-SSP-gdprConsent-' + }]); }); }) @@ -226,6 +239,19 @@ describe('freewheelSSP BidAdapter Test', () => { expect(payload.playerSize).to.equal('300x600'); expect(payload._fw_gdpr_consent).to.exist.and.to.be.a('string'); expect(payload._fw_gdpr_consent).to.equal(gdprConsentString); + + let gdprConsent = { + 'gdprApplies': true, + 'consentString': gdprConsentString + } + let syncOptions = { + 'pixelEnabled': true + } + const userSyncs = spec.getUserSyncs(syncOptions, null, gdprConsent, null); + expect(userSyncs).to.deep.equal([{ + type: 'image', + url: 'https://ads.stickyadstv.com/auto-user-sync?gdpr=1&gdpr_consent=1FW-SSP-gdprConsent-' + }]); }); }) @@ -297,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' + + ' ' + + ' ' + + ' ' + ' ' + ' ' + ' ' + @@ -332,6 +361,8 @@ describe('freewheelSSP BidAdapter Test', () => { netRevenue: true, ttl: 360, dealId: 'NRJ-PRO-00008', + campaignId: 'SMF-WOW-55555', + bannerId: '12345', ad: ad } ]; @@ -339,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', () => { @@ -355,6 +388,8 @@ describe('freewheelSSP BidAdapter Test', () => { netRevenue: true, ttl: 360, dealId: 'NRJ-PRO-00008', + campaignId: 'SMF-WOW-55555', + bannerId: '12345', ad: formattedAd } ]; @@ -362,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', () => { @@ -372,6 +409,7 @@ describe('freewheelSSP BidAdapter Test', () => { expect(result.length).to.equal(0); }); }); + describe('interpretResponseForVideo', () => { let bidRequests = [ { @@ -435,6 +473,12 @@ describe('freewheelSSP BidAdapter Test', () => { ' Adswizz' + ' ' + ' https://ads.stickyadstv.com/auto-user-sync?dealId=NRJ-PRO-00008' + + ' ' + + ' ' + + ' ' + + ' ' + + ' ' + + ' ' + ' ' + ' ' + ' ' + @@ -469,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 @@ -478,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', () => { @@ -494,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 @@ -503,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/gamoshiBidAdapter_spec.js b/test/spec/modules/gamoshiBidAdapter_spec.js index 79f58470cb3..2996b853046 100644 --- a/test/spec/modules/gamoshiBidAdapter_spec.js +++ b/test/spec/modules/gamoshiBidAdapter_spec.js @@ -423,7 +423,7 @@ describe('GamoshiAdapter', () => { it('build request with ID5 Id', () => { const bidRequestClone = utils.deepClone(bidRequest); bidRequestClone.userId = {}; - bidRequestClone.userId.id5id = 'id5-user-id'; + bidRequestClone.userId.id5id = { uid: 'id5-user-id' }; let request = spec.buildRequests([bidRequestClone], bidRequestClone)[0]; expect(request.data.user.ext.eids).to.deep.equal([{ 'source': 'id5-sync.com', diff --git a/test/spec/modules/gdprEnforcement_spec.js b/test/spec/modules/gdprEnforcement_spec.js index d5c8d7bfb88..82cb70f42be 100644 --- a/test/spec/modules/gdprEnforcement_spec.js +++ b/test/spec/modules/gdprEnforcement_spec.js @@ -8,7 +8,9 @@ import { enforcementRules, purpose1Rule, purpose2Rule, - enableAnalyticsHook + enableAnalyticsHook, + getGvlid, + internal } from 'modules/gdprEnforcement.js'; import { config } from 'src/config.js'; import adapterManager, { gdprDataHandler } from 'src/adapterManager.js'; @@ -1067,4 +1069,57 @@ describe('gdpr enforcement', function () { sinon.assert.calledWith(events.emit.getCall(1), 'tcf2Enforcement', sinon.match.object); }) }); + + describe('getGvlid', function() { + let sandbox; + let getGvlidForBidAdapterStub; + let getGvlidForUserIdModuleStub; + let getGvlidForAnalyticsAdapterStub; + beforeEach(function() { + sandbox = sinon.createSandbox(); + getGvlidForBidAdapterStub = sandbox.stub(internal, 'getGvlidForBidAdapter'); + getGvlidForUserIdModuleStub = sandbox.stub(internal, 'getGvlidForUserIdModule'); + getGvlidForAnalyticsAdapterStub = sandbox.stub(internal, 'getGvlidForAnalyticsAdapter'); + }); + afterEach(function() { + sandbox.restore(); + config.resetConfig(); + }); + + it('should return "null" if called without passing any argument', function() { + const gvlid = getGvlid(); + expect(gvlid).to.equal(null); + }); + + it('should return "null" if GVL ID is not defined for any of these modules: Bid adapter, UserId submodule and Analytics adapter', function() { + getGvlidForBidAdapterStub.withArgs('moduleA').returns(null); + getGvlidForUserIdModuleStub.withArgs('moduleA').returns(null); + getGvlidForAnalyticsAdapterStub.withArgs('moduleA').returns(null); + + const gvlid = getGvlid('moduleA'); + expect(gvlid).to.equal(null); + }); + + it('should return the GVL ID from gvlMapping if it is defined in setConfig', function() { + config.setConfig({ + gvlMapping: { + moduleA: 1 + } + }); + + // Actual GVL ID for moduleA is 2, as defined on its the bidAdapter.js file. + getGvlidForBidAdapterStub.withArgs('moduleA').returns(2); + + const gvlid = getGvlid('moduleA'); + expect(gvlid).to.equal(1); + }); + + it('should return the GVL ID by calling getGvlidForBidAdapter -> getGvlidForUserIdModule -> getGvlidForAnalyticsAdapter in sequence', function() { + getGvlidForBidAdapterStub.withArgs('moduleA').returns(null); + getGvlidForUserIdModuleStub.withArgs('moduleA').returns(null); + getGvlidForAnalyticsAdapterStub.withArgs('moduleA').returns(7); + + expect(getGvlid('moduleA')).to.equal(7); + }); + }); }); diff --git a/test/spec/modules/geoedgeRtdProvider_spec.js b/test/spec/modules/geoedgeRtdProvider_spec.js new file mode 100644 index 00000000000..cf4e0b53fde --- /dev/null +++ b/test/spec/modules/geoedgeRtdProvider_spec.js @@ -0,0 +1,111 @@ +import * as utils from '../../../src/utils.js'; +import * as hook from '../../../src/hook.js' +import { beforeInit, geoedgeSubmodule, setWrapper, wrapper, htmlPlaceholder, WRAPPER_URL, getClientUrl } from '../../../modules/geoedgeRtdProvider.js'; +import { server } from '../../../test/mocks/xhr.js'; + +let key = '123123123'; +function makeConfig() { + return { + name: 'geoedge', + params: { + wap: false, + key: key, + bidders: { + bidderA: true, + bidderB: false + } + } + }; +} + +function mockBid(bidderCode) { + return { + 'ad': '', + 'cpm': '1.00', + 'width': 300, + 'height': 250, + 'bidderCode': bidderCode, + 'requestId': utils.getUniqueIdentifierStr(), + 'creativeId': 'id', + 'currency': 'USD', + 'netRevenue': true, + 'ttl': 360 + }; +} + +let mockWrapper = `${htmlPlaceholder}`; + +describe('Geoedge RTD module', function () { + describe('beforeInit', function () { + let submoduleStub; + + before(function () { + submoduleStub = sinon.stub(hook, 'submodule'); + }); + after(function () { + submoduleStub.restore(); + }); + it('should fetch the wrapper', function () { + beforeInit(); + let request = server.requests[0]; + let isWrapperRequest = request && request.url && request.url && request.url === WRAPPER_URL; + expect(isWrapperRequest).to.equal(true); + }); + it('should register RTD submodule provider', function () { + expect(submoduleStub.calledWith('realTimeData', geoedgeSubmodule)).to.equal(true); + }); + }); + describe('setWrapper', function () { + it('should set the wrapper', function () { + setWrapper(mockWrapper); + expect(wrapper).to.equal(mockWrapper); + }); + }); + describe('submodule', function () { + describe('name', function () { + it('should be geoedge', function () { + expect(geoedgeSubmodule.name).to.equal('geoedge'); + }); + }); + describe('init', function () { + let insertElementStub; + + before(function () { + insertElementStub = sinon.stub(utils, 'insertElement'); + }); + after(function () { + utils.insertElement.restore(); + }); + it('should return false when missing params or key', function () { + let missingParams = geoedgeSubmodule.init({}); + let missingKey = geoedgeSubmodule.init({ params: {} }); + expect(missingParams || missingKey).to.equal(false); + }); + it('should return true when params are ok', function () { + expect(geoedgeSubmodule.init(makeConfig())).to.equal(true); + }); + it('should preload the client', function () { + let isLinkPreloadAsScript = arg => arg.tagName === 'LINK' && arg.rel === 'preload' && arg.as === 'script' && arg.href === getClientUrl(key); + expect(insertElementStub.calledWith(sinon.match(isLinkPreloadAsScript))).to.equal(true); + }); + }); + describe('onBidResponseEvent', function () { + let bidFromA = mockBid('bidderA'); + it('should wrap bid html when bidder is configured', function () { + geoedgeSubmodule.onBidResponseEvent(bidFromA, makeConfig()); + expect(bidFromA.ad.indexOf('')).to.equal(0); + }); + it('should not wrap bid html when bidder is not configured', function () { + let bidFromB = mockBid('bidderB'); + geoedgeSubmodule.onBidResponseEvent(bidFromB, makeConfig()); + expect(bidFromB.ad.indexOf('')).to.equal(-1); + }); + it('should only muatate the bid ad porperty', function () { + let copy = Object.assign({}, bidFromA); + delete copy.ad; + let equalsOriginal = Object.keys(copy).every(key => copy[key] === bidFromA[key]); + expect(equalsOriginal).to.equal(true); + }); + }); + }); +}); diff --git a/test/spec/modules/gjirafaBidAdapter_spec.js b/test/spec/modules/gjirafaBidAdapter_spec.js new file mode 100644 index 00000000000..f0fb01f4398 --- /dev/null +++ b/test/spec/modules/gjirafaBidAdapter_spec.js @@ -0,0 +1,168 @@ +import { expect } from 'chai'; +import { spec } from 'modules/gjirafaBidAdapter'; + +describe('gjirafaAdapterTest', () => { + describe('bidRequestValidity', () => { + it('bidRequest with propertyId and placementId', () => { + expect(spec.isBidRequestValid({ + bidder: 'gjirafa', + params: { + propertyId: '{propertyId}', + placementId: '{placementId}' + } + })).to.equal(true); + }); + + it('bidRequest without propertyId', () => { + expect(spec.isBidRequestValid({ + bidder: 'gjirafa', + params: { + placementId: '{placementId}' + } + })).to.equal(false); + }); + + it('bidRequest without placementId', () => { + expect(spec.isBidRequestValid({ + bidder: 'gjirafa', + params: { + propertyId: '{propertyId}', + } + })).to.equal(false); + }); + + it('bidRequest without propertyId or placementId', () => { + expect(spec.isBidRequestValid({ + bidder: 'gjirafa', + params: {} + })).to.equal(false); + }); + }); + + describe('bidRequest', () => { + const bidRequests = [{ + 'bidder': 'gjirafa', + 'params': { + 'propertyId': '{propertyId}', + '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', + 'sizes': [[728, 90]], + 'bidId': '10bdc36fe0b48c8', + 'bidderRequestId': '70deaff71c281d', + 'auctionId': 'f9012acc-b6b7-4748-9098-97252914f9dc' + }]; + + it('bidRequest HTTP method', () => { + const requests = spec.buildRequests(bidRequests); + requests.forEach(function (requestItem) { + expect(requestItem.method).to.equal('POST'); + }); + }); + + it('bidRequest url', () => { + const endpointUrl = 'https://central.gjirafa.com/bid'; + const requests = spec.buildRequests(bidRequests); + requests.forEach(function (requestItem) { + expect(requestItem.url).to.match(new RegExp(`${endpointUrl}`)); + }); + }); + + it('bidRequest data', () => { + const requests = spec.buildRequests(bidRequests); + requests.forEach(function (requestItem) { + expect(requestItem.data).to.exist; + }); + }); + + it('bidRequest sizes', () => { + const requests = spec.buildRequests(bidRequests); + requests.forEach(function (requestItem) { + expect(requestItem.data.placements).to.exist; + expect(requestItem.data.placements.length).to.equal(1); + 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', () => { + const bidRequest = { + 'method': 'POST', + 'url': 'https://central.gjirafa.com/bid', + 'data': { + 'sizes': '728x90', + 'adUnitId': 'hb-leaderboard', + 'placementId': '{placementId}', + 'propertyId': '{propertyId}', + 'pageViewGuid': '{pageViewGuid}', + 'url': 'http://localhost:9999/integrationExamples/gpt/hello_world.html?pbjs_debug=true', + 'requestid': '26ee8fe87940da7', + 'bidid': '2962dbedc4768bf' + } + }; + + const bidResponse = { + body: [{ + 'CPM': 1, + 'Width': 728, + 'Height': 90, + 'Referrer': 'http://localhost:9999/integrationExamples/gpt/hello_world.html?pbjs_debug=true', + 'Ad': '
Test ad
', + 'CreativeId': '123abc', + 'NetRevenue': false, + 'Currency': 'EUR', + 'TTL': 360 + }], + headers: {} + }; + + it('all keys present', () => { + const result = spec.interpretResponse(bidResponse, bidRequest); + + let keys = [ + 'requestId', + 'cpm', + 'width', + 'height', + 'creativeId', + 'currency', + 'netRevenue', + 'ttl', + 'referrer', + 'ad', + 'vastUrl', + 'mediaType' + ]; + + let resultKeys = Object.keys(result[0]); + resultKeys.forEach(function (key) { + expect(keys.indexOf(key) !== -1).to.equal(true); + }); + }) + }); +}); 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/gnetBidAdapter_spec.js b/test/spec/modules/gnetBidAdapter_spec.js new file mode 100644 index 00000000000..40f8ad50d78 --- /dev/null +++ b/test/spec/modules/gnetBidAdapter_spec.js @@ -0,0 +1,145 @@ +import { + expect +} from 'chai'; +import { + spec +} from 'modules/gnetBidAdapter.js'; +import { + newBidder +} from 'src/adapters/bidderFactory.js'; + +const ENDPOINT = 'https://adserver.gnetproject.com/prebid.php'; + +describe('gnetAdapter', function () { + const adapter = newBidder(spec); + + describe('inherited functions', function () { + it('exists and is a function', function () { + expect(adapter.callBids).to.exist.and.to.be.a('function'); + }); + }); + + describe('isBidRequestValid', function () { + let bid = { + bidder: 'gnet', + params: { + websiteId: '4', + externalId: '4d52cccf30309282256012cf30309282' + } + }; + + it('should return true when required params found', function () { + expect(spec.isBidRequestValid(bid)).to.equal(true); + }); + + it('should return false when required params are not passed', function () { + let bid = Object.assign({}, bid); + delete bid.params; + bid.params = {}; + expect(spec.isBidRequestValid(bid)).to.equal(false); + }); + }); + + describe('buildRequests', function () { + const bidRequests = [{ + bidder: 'gnet', + params: { + websiteId: '4', + externalId: '4d52cccf30309282256012cf30309282' + }, + adUnitCode: '/150790500/4_ZONA_IAB_300x250_5', + sizes: [ + [300, 250], + ], + bidId: '2a19afd5173318', + bidderRequestId: '1f4001782ac16c', + auctionId: 'aba03555-4802-4c45-9f15-05ffa8594cff', + transactionId: '894bdff6-61ec-4bec-a5a9-f36a5bfccef5' + }]; + + const bidderRequest = { + refererInfo: { + referer: 'https://gnetproject.com/' + } + }; + + it('sends bid request to ENDPOINT via POST', function () { + const requests = spec.buildRequests(bidRequests, bidderRequest); + expect(requests[0].url).to.equal(ENDPOINT); + expect(requests[0].method).to.equal('POST'); + expect(requests[0].data).to.equal(JSON.stringify({ + 'referer': 'https://gnetproject.com/', + 'adUnitCode': '/150790500/4_ZONA_IAB_300x250_5', + 'bidId': '2a19afd5173318', + 'transactionId': '894bdff6-61ec-4bec-a5a9-f36a5bfccef5', + 'sizes': ['300x250'], + 'params': { + 'websiteId': '4', + 'externalId': '4d52cccf30309282256012cf30309282' + } + })); + }); + }); + + describe('interpretResponse', function () { + const bidderRequests = [{ + bidder: 'gnet', + params: { + clientId: '123456' + }, + adUnitCode: '/150790500/4_ZONA_IAB_300x250_5', + sizes: [ + [300, 250], + ], + bidId: '2a19afd5173318', + bidderRequestId: '1f4001782ac16c', + auctionId: 'aba03555-4802-4c45-9f15-05ffa8594cff', + transactionId: '894bdff6-61ec-4bec-a5a9-f36a5bfccef5' + }]; + + it('should get correct banner bid response', function () { + const response = { + bids: [ + { + bidId: '2a19afd5173318', + cpm: 0.1, + currency: 'BRL', + width: 300, + height: 250, + ad: '

I am an ad

', + creativeId: '173560700', + } + ] + }; + + const expectedResponse = [ + { + requestId: '2a19afd5173318', + cpm: 0.1, + currency: 'BRL', + width: 300, + height: 250, + ad: '

I am an ad

', + ttl: 300, + creativeId: '173560700', + netRevenue: true + } + ]; + + const result = spec.interpretResponse({ + body: response + }, bidderRequests); + expect(result).to.have.lengthOf(1); + expect(result).to.deep.have.same.members(expectedResponse); + }); + + it('handles nobid responses', function () { + const response = ''; + + const result = spec.interpretResponse({ + body: response + }, bidderRequests); + expect(result.length).to.equal(0); + }); + }); +}); diff --git a/test/spec/modules/gothamadsBidAdapter_spec.js b/test/spec/modules/gothamadsBidAdapter_spec.js new file mode 100644 index 00000000000..f0a3ea253f3 --- /dev/null +++ b/test/spec/modules/gothamadsBidAdapter_spec.js @@ -0,0 +1,416 @@ +import { expect } from 'chai'; +import { spec } from 'modules/gothamadsBidAdapter.js'; +import { config } from 'src/config.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, + gdprConsent: { + consentString: 'BOEFEAyOEFEAyAHABDENAI4AAAB9vABAASA', + gdprApplies: 1, + }, + uspConsent: 'uspConsent' +} + +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('with COPPA', function () { + beforeEach(function () { + sinon.stub(config, 'getConfig') + .withArgs('coppa') + .returns(true); + }); + afterEach(function () { + config.getConfig.restore(); + }); + + it('should send the Coppa "required" flag set to "1" in the request', function () { + let serverRequest = spec.buildRequests([BANNER_BID_REQUEST]); + expect(serverRequest.data[0].regs.coppa).to.equal(1); + }); + }); + 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('check consent and ccpa string is set properly', function () { + expect(request.data[0].regs.ext.gdpr).to.equal(1); + expect(request.data[0].user.ext.consent).to.equal(BANNER_BID_REQUEST.gdprConsent.consentString); + expect(request.data[0].regs.ext.us_privacy).to.equal(BANNER_BID_REQUEST.uspConsent); + }); + + it('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', + meta: BANNER_BID_RESPONSE.seatbid[0].bid[0].adomain, + 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', 'meta'); + 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.meta).to.have.property('advertiserDomains', expectedBidResponse.meta); + 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', + meta: VIDEO_BID_RESPONSE.seatbid[0].bid[0].adomain, + 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', 'meta'); + expect(dataItem.requestId).to.equal(expectedBidResponse.requestId); + expect(dataItem.cpm).to.equal(expectedBidResponse.cpm); + expect(dataItem.vastXml).to.equal(expectedBidResponse.vastXml); + expect(dataItem.ttl).to.equal(expectedBidResponse.ttl); + expect(dataItem.creativeId).to.equal(expectedBidResponse.creativeId); + expect(dataItem.meta).to.have.property('advertiserDomains', expectedBidResponse.meta); + 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, + meta: NATIVE_BID_RESPONSE.seatbid[0].bid[0].adomain, + 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', 'meta'); + 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.meta).to.have.property('advertiserDomains', expectedBidResponse.meta); + 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/gptPreAuction_spec.js b/test/spec/modules/gptPreAuction_spec.js index 16b84467af2..c4a81c21d5c 100644 --- a/test/spec/modules/gptPreAuction_spec.js +++ b/test/spec/modules/gptPreAuction_spec.js @@ -30,34 +30,34 @@ describe('GPT pre-auction module', () => { '
test2
'; it('should be unchanged if already defined on adUnit', () => { - const adUnit = { fpd: { context: { pbAdSlot: '12345' } } }; + const adUnit = { ortb2Imp: { ext: { data: { pbadslot: '12345' } } } }; appendPbAdSlot(adUnit); - expect(adUnit.fpd.context.pbAdSlot).to.equal('12345'); + expect(adUnit.ortb2Imp.ext.data.pbadslot).to.equal('12345'); }); it('should use adUnit.code if matching id exists', () => { - const adUnit = { code: 'foo1', fpd: { context: {} } }; + const adUnit = { code: 'foo1', ortb2Imp: { ext: { data: {} } } }; appendPbAdSlot(adUnit); - expect(adUnit.fpd.context.pbAdSlot).to.equal('bar1'); + expect(adUnit.ortb2Imp.ext.data.pbadslot).to.equal('bar1'); }); it('should use the gptSlot.adUnitPath if the adUnit.code matches a div id but does not have a data-adslotid', () => { - const adUnit = { code: 'foo3', mediaTypes: { banner: { sizes: [[250, 250]] } }, fpd: { context: { adServer: { name: 'gam', adSlot: '/baz' } } } }; + const adUnit = { code: 'foo3', mediaTypes: { banner: { sizes: [[250, 250]] } }, ortb2Imp: { ext: { data: { adserver: { name: 'gam', adslot: '/baz' } } } } }; appendPbAdSlot(adUnit); - expect(adUnit.fpd.context.pbAdSlot).to.equal('/baz'); + expect(adUnit.ortb2Imp.ext.data.pbadslot).to.equal('/baz'); }); it('should use the video adUnit.code (which *should* match the configured "adSlotName", but is not being tested) if there is no matching div with "data-adslotid" defined', () => { - const adUnit = { code: 'foo4', mediaTypes: { video: { sizes: [[250, 250]] } }, fpd: { context: {} } }; + const adUnit = { code: 'foo4', mediaTypes: { video: { sizes: [[250, 250]] } }, ortb2Imp: { ext: { data: {} } } }; adUnit.code = 'foo5'; appendPbAdSlot(adUnit, undefined); - expect(adUnit.fpd.context.pbAdSlot).to.equal('foo5'); + expect(adUnit.ortb2Imp.ext.data.pbadslot).to.equal('foo5'); }); it('should use the adUnit.code if all other sources failed', () => { - const adUnit = { code: 'foo4', fpd: { context: {} } }; + const adUnit = { code: 'foo4', ortb2Imp: { ext: { data: {} } } }; appendPbAdSlot(adUnit, undefined); - expect(adUnit.fpd.context.pbAdSlot).to.equal('foo4'); + expect(adUnit.ortb2Imp.ext.data.pbadslot).to.equal('foo4'); }); it('should use the customPbAdSlot function if one is given', () => { @@ -67,32 +67,32 @@ describe('GPT pre-auction module', () => { } }); - const adUnit = { code: 'foo1', fpd: { context: {} } }; + const adUnit = { code: 'foo1', ortb2Imp: { ext: { data: {} } } }; appendPbAdSlot(adUnit); - expect(adUnit.fpd.context.pbAdSlot).to.equal('customPbAdSlotName'); + expect(adUnit.ortb2Imp.ext.data.pbadslot).to.equal('customPbAdSlotName'); }); }); describe('appendGptSlots', () => { it('should not add adServer object to context if no slots defined', () => { - const adUnit = { code: 'adUnitCode', fpd: { context: {} } }; + const adUnit = { code: 'adUnitCode', ortb2Imp: { ext: { data: {} } } }; appendGptSlots([adUnit]); - expect(adUnit.fpd.context.adServer).to.be.undefined; + expect(adUnit.ortb2Imp.ext.data.adserver).to.be.undefined; }); it('should not add adServer object to context if no slot matches', () => { window.googletag.pubads().setSlots(testSlots); - const adUnit = { code: 'adUnitCode', fpd: { context: {} } }; + const adUnit = { code: 'adUnitCode', ortb2Imp: { ext: { data: {} } } }; appendGptSlots([adUnit]); - expect(adUnit.fpd.context.adServer).to.be.undefined; + expect(adUnit.ortb2Imp.ext.data.adserver).to.be.undefined; }); it('should add adServer object to context if matching slot is found', () => { window.googletag.pubads().setSlots(testSlots); - const adUnit = { code: 'slotCode2', fpd: { context: {} } }; + const adUnit = { code: 'slotCode2', ortb2Imp: { ext: { data: {} } } }; appendGptSlots([adUnit]); - expect(adUnit.fpd.context.adServer).to.be.an('object'); - expect(adUnit.fpd.context.adServer).to.deep.equal({ name: 'gam', adSlot: 'slotCode2' }); + expect(adUnit.ortb2Imp.ext.data.adserver).to.be.an('object'); + expect(adUnit.ortb2Imp.ext.data.adserver).to.deep.equal({ name: 'gam', adslot: 'slotCode2' }); }); it('should use the customGptSlotMatching function if one is given', () => { @@ -104,10 +104,10 @@ describe('GPT pre-auction module', () => { }); window.googletag.pubads().setSlots(testSlots); - const adUnit = { code: 'SlOtCoDe1', fpd: { context: {} } }; + const adUnit = { code: 'SlOtCoDe1', ortb2Imp: { ext: { data: {} } } }; appendGptSlots([adUnit]); - expect(adUnit.fpd.context.adServer).to.be.an('object'); - expect(adUnit.fpd.context.adServer).to.deep.equal({ name: 'gam', adSlot: 'slotCode1' }); + expect(adUnit.ortb2Imp.ext.data.adserver).to.be.an('object'); + expect(adUnit.ortb2Imp.ext.data.adserver).to.deep.equal({ name: 'gam', adslot: 'slotCode1' }); }); }); @@ -164,23 +164,23 @@ describe('GPT pre-auction module', () => { it('should append PB Ad Slot and GPT Slot info to first-party data in each ad unit', () => { const testAdUnits = [{ code: 'adUnit1', - fpd: { context: { pbAdSlot: '12345' } } + ortb2Imp: { ext: { data: { pbadslot: '12345' } } } }, { code: 'slotCode1', - fpd: { context: { pbAdSlot: '67890' } } + ortb2Imp: { ext: { data: { pbadslot: '67890' } } } }, { code: 'slotCode3', }]; const expectedAdUnits = [{ code: 'adUnit1', - fpd: { context: { pbAdSlot: '12345' } } + ortb2Imp: { ext: { data: { pbadslot: '12345' } } } }, { code: 'slotCode1', - fpd: { context: { pbAdSlot: '67890', adServer: { name: 'gam', adSlot: 'slotCode1' } } } + ortb2Imp: { ext: { data: { pbadslot: '67890', adserver: { name: 'gam', adslot: 'slotCode1' } } } } }, { code: 'slotCode3', - fpd: { context: { pbAdSlot: 'slotCode3', adServer: { name: 'gam', adSlot: 'slotCode3' } } } + ortb2Imp: { ext: { data: { pbadslot: 'slotCode3', adserver: { name: 'gam', adslot: 'slotCode3' } } } } }]; window.googletag.pubads().setSlots(testSlots); diff --git a/test/spec/modules/gridBidAdapter_spec.js b/test/spec/modules/gridBidAdapter_spec.js index 344f1764c05..e161d82fdd6 100644 --- a/test/spec/modules/gridBidAdapter_spec.js +++ b/test/spec/modules/gridBidAdapter_spec.js @@ -40,229 +40,6 @@ describe('TheMediaGrid Adapter', function () { }); describe('buildRequests', function () { - function parseRequest(url) { - const res = {}; - url.split('&').forEach((it) => { - const couple = it.split('='); - res[couple[0]] = decodeURIComponent(couple[1]); - }); - return res; - } - const bidderRequest = {refererInfo: {referer: 'https://example.com'}}; - const referrer = bidderRequest.refererInfo.referer; - let bidRequests = [ - { - 'bidder': 'grid', - 'params': { - 'uid': '1' - }, - 'adUnitCode': 'adunit-code-1', - 'sizes': [[300, 250], [300, 600]], - 'bidId': '30b31c1838de1e', - 'bidderRequestId': '22edbae2733bf6', - 'auctionId': '1d1a030790a475', - }, - { - 'bidder': 'grid', - 'params': { - 'uid': '1' - }, - 'adUnitCode': 'adunit-code-2', - 'sizes': [[728, 90]], - 'mediaTypes': { - 'video': { - 'playerSize': [400, 600] - }, - 'banner': { - 'sizes': [[728, 90]] - } - }, - 'bidId': '3150ccb55da321', - 'bidderRequestId': '22edbae2733bf6', - 'auctionId': '1d1a030790a475', - }, - { - 'bidder': 'grid', - 'params': { - 'uid': '2' - }, - 'adUnitCode': 'adunit-code-1', - 'sizes': [[300, 250], [300, 600]], - 'mediaTypes': { - 'video': { - 'playerSize': [400, 600] - }, - 'banner': { - 'sizes': [[300, 250], [300, 600]] - } - }, - 'bidId': '42dbe3a7168a6a', - 'bidderRequestId': '22edbae2733bf6', - 'auctionId': '1d1a030790a475', - } - ]; - - it('should attach valid params to the tag', function () { - const [request] = spec.buildRequests([bidRequests[0]], bidderRequest); - expect(request.data).to.be.an('string'); - const payload = parseRequest(request.data); - expect(payload).to.have.property('u', referrer); - expect(payload).to.have.property('auids', '1'); - expect(payload).to.have.property('sizes', '300x250,300x600'); - expect(payload).to.have.property('r', '22edbae2733bf6'); - expect(payload).to.have.property('wrapperType', 'Prebid_js'); - expect(payload).to.have.property('wrapperVersion', '$prebid.version$'); - }); - - it('sizes must be added from mediaTypes', function () { - const [request] = spec.buildRequests([bidRequests[0], bidRequests[1]], bidderRequest); - expect(request.data).to.be.an('string'); - const payload = parseRequest(request.data); - expect(payload).to.have.property('u', referrer); - expect(payload).to.have.property('auids', '1,1'); - expect(payload).to.have.property('sizes', '300x250,300x600,728x90,400x600'); - expect(payload).to.have.property('r', '22edbae2733bf6'); - }); - - it('sizes must not be duplicated', function () { - const [request] = spec.buildRequests(bidRequests, bidderRequest); - expect(request.data).to.be.an('string'); - const payload = parseRequest(request.data); - expect(payload).to.have.property('u', referrer); - expect(payload).to.have.property('auids', '1,1,2'); - expect(payload).to.have.property('sizes', '300x250,300x600,728x90,400x600'); - expect(payload).to.have.property('r', '22edbae2733bf6'); - }); - - it('if gdprConsent is present payload must have gdpr params', function () { - const [request] = spec.buildRequests(bidRequests, {gdprConsent: {consentString: 'AAA', gdprApplies: true}, refererInfo: bidderRequest.refererInfo}); - expect(request.data).to.be.an('string'); - const payload = parseRequest(request.data); - expect(payload).to.have.property('u', referrer); - expect(payload).to.have.property('gdpr_consent', 'AAA'); - expect(payload).to.have.property('gdpr_applies', '1'); - }); - - it('if gdprApplies is false gdpr_applies must be 0', function () { - const [request] = spec.buildRequests(bidRequests, {gdprConsent: {consentString: 'AAA', gdprApplies: false}}); - expect(request.data).to.be.an('string'); - const payload = parseRequest(request.data); - expect(payload).to.have.property('gdpr_consent', 'AAA'); - expect(payload).to.have.property('gdpr_applies', '0'); - }); - - it('if gdprApplies is undefined gdpr_applies must be 1', function () { - const [request] = spec.buildRequests(bidRequests, {gdprConsent: {consentString: 'AAA'}}); - expect(request.data).to.be.an('string'); - const payload = parseRequest(request.data); - expect(payload).to.have.property('gdpr_consent', 'AAA'); - expect(payload).to.have.property('gdpr_applies', '1'); - }); - - it('if usPrivacy is present payload must have us_privacy param', function () { - const bidderRequestWithUSP = Object.assign({uspConsent: '1YNN'}, bidderRequest); - const [request] = spec.buildRequests(bidRequests, bidderRequestWithUSP); - expect(request.data).to.be.an('string'); - const payload = parseRequest(request.data); - expect(payload).to.have.property('us_privacy', '1YNN'); - }); - - it('should convert keyword params to proper form and attaches to request', function () { - const bidRequestWithKeywords = [].concat(bidRequests); - bidRequestWithKeywords[1] = Object.assign({}, - bidRequests[1], - { - params: { - uid: '1', - keywords: { - single: 'val', - singleArr: ['val'], - singleArrNum: [3], - multiValMixed: ['value1', 2, 'value3'], - singleValNum: 123, - emptyStr: '', - emptyArr: [''], - badValue: {'foo': 'bar'} // should be dropped - } - } - } - ); - - const [request] = spec.buildRequests(bidRequestWithKeywords, bidderRequest); - expect(request.data).to.be.an('string'); - const payload = parseRequest(request.data); - expect(payload.keywords).to.be.an('string'); - payload.keywords = JSON.parse(payload.keywords); - - expect(payload.keywords).to.deep.equal([{ - 'key': 'single', - 'value': ['val'] - }, { - 'key': 'singleArr', - 'value': ['val'] - }, { - 'key': 'singleArrNum', - 'value': ['3'] - }, { - 'key': 'multiValMixed', - 'value': ['value1', '2', 'value3'] - }, { - 'key': 'singleValNum', - 'value': ['123'] - }, { - 'key': 'emptyStr' - }, { - 'key': 'emptyArr' - }]); - }); - - it('should mix keyword param with keywords from config', function () { - const getConfigStub = sinon.stub(config, 'getConfig').callsFake( - arg => arg === 'fpd.user' ? {'keywords': ['a', 'b']} : arg === 'fpd.context' ? {'keywords': ['any words']} : null); - - const bidRequestWithKeywords = [].concat(bidRequests); - bidRequestWithKeywords[1] = Object.assign({}, - bidRequests[1], - { - params: { - uid: '1', - keywords: { - single: 'val', - singleArr: ['val'], - multiValMixed: ['value1', 2, 'value3'] - } - } - } - ); - - const [request] = spec.buildRequests(bidRequestWithKeywords, bidderRequest); - expect(request.data).to.be.an('string'); - const payload = parseRequest(request.data); - expect(payload.keywords).to.be.an('string'); - payload.keywords = JSON.parse(payload.keywords); - - expect(payload.keywords).to.deep.equal([{ - 'key': 'single', - 'value': ['val'] - }, { - 'key': 'singleArr', - 'value': ['val'] - }, { - 'key': 'multiValMixed', - 'value': ['value1', '2', 'value3'] - }, { - 'key': 'user', - 'value': ['a', 'b'] - }, { - 'key': 'context', - 'value': ['any words'] - }]); - - getConfigStub.restore(); - }); - }); - - describe('buildRequests in new format', function () { function parseRequest(data) { return JSON.parse(data); } @@ -278,7 +55,7 @@ describe('TheMediaGrid Adapter', function () { 'bidder': 'grid', 'params': { 'uid': '1', - 'useNewFormat': true + 'bidFloor': 1.25 }, 'adUnitCode': 'adunit-code-1', 'sizes': [[300, 250], [300, 600]], @@ -294,8 +71,7 @@ describe('TheMediaGrid Adapter', function () { { 'bidder': 'grid', 'params': { - 'uid': '2', - 'useNewFormat': true + 'uid': '2' }, 'adUnitCode': 'adunit-code-1', 'sizes': [[300, 250], [300, 600]], @@ -306,8 +82,7 @@ describe('TheMediaGrid Adapter', function () { { 'bidder': 'grid', 'params': { - 'uid': '11', - 'useNewFormat': true + 'uid': '11' }, 'adUnitCode': 'adunit-code-2', 'sizes': [[728, 90]], @@ -324,14 +99,14 @@ describe('TheMediaGrid Adapter', function () { { 'bidder': 'grid', 'params': { - 'uid': '3', - 'useNewFormat': true + 'uid': '3' }, 'adUnitCode': 'adunit-code-2', 'sizes': [[728, 90]], 'mediaTypes': { 'video': { - 'playerSize': [[400, 600]] + 'playerSize': [[400, 600]], + 'protocols': [1, 2, 3] }, 'banner': { 'sizes': [[728, 90]] @@ -344,7 +119,7 @@ describe('TheMediaGrid Adapter', function () { ]; it('should attach valid params to the tag', function () { - const [request] = spec.buildRequests([bidRequests[0]], bidderRequest); + const request = spec.buildRequests([bidRequests[0]], bidderRequest); expect(request.data).to.be.an('string'); const payload = parseRequest(request.data); expect(payload).to.deep.equal({ @@ -361,6 +136,7 @@ describe('TheMediaGrid Adapter', function () { 'id': bidRequests[0].bidId, 'tagid': bidRequests[0].params.uid, 'ext': {'divid': bidRequests[0].adUnitCode}, + 'bidfloor': bidRequests[0].params.bidFloor, 'banner': { 'w': 300, 'h': 250, @@ -371,7 +147,7 @@ describe('TheMediaGrid Adapter', function () { }); it('make possible to process request without mediaTypes', function () { - const [request] = spec.buildRequests([bidRequests[0], bidRequests[1]], bidderRequest); + const request = spec.buildRequests([bidRequests[0], bidRequests[1]], bidderRequest); expect(request.data).to.be.an('string'); const payload = parseRequest(request.data); expect(payload).to.deep.equal({ @@ -388,6 +164,7 @@ describe('TheMediaGrid Adapter', function () { 'id': bidRequests[0].bidId, 'tagid': bidRequests[0].params.uid, 'ext': {'divid': bidRequests[0].adUnitCode}, + 'bidfloor': bidRequests[0].params.bidFloor, 'banner': { 'w': 300, 'h': 250, @@ -407,7 +184,7 @@ describe('TheMediaGrid Adapter', function () { }); it('should attach valid params to the video tag', function () { - const [request] = spec.buildRequests(bidRequests.slice(0, 3), bidderRequest); + const request = spec.buildRequests(bidRequests.slice(0, 3), bidderRequest); expect(request.data).to.be.an('string'); const payload = parseRequest(request.data); expect(payload).to.deep.equal({ @@ -424,6 +201,7 @@ describe('TheMediaGrid Adapter', function () { 'id': bidRequests[0].bidId, 'tagid': bidRequests[0].params.uid, 'ext': {'divid': bidRequests[0].adUnitCode}, + 'bidfloor': bidRequests[0].params.bidFloor, 'banner': { 'w': 300, 'h': 250, @@ -452,7 +230,7 @@ describe('TheMediaGrid Adapter', function () { }); it('should support mixed mediaTypes', function () { - const [request] = spec.buildRequests(bidRequests, bidderRequest); + const request = spec.buildRequests(bidRequests, bidderRequest); expect(request.data).to.be.an('string'); const payload = parseRequest(request.data); expect(payload).to.deep.equal({ @@ -469,6 +247,7 @@ describe('TheMediaGrid Adapter', function () { 'id': bidRequests[0].bidId, 'tagid': bidRequests[0].params.uid, 'ext': {'divid': bidRequests[0].adUnitCode}, + 'bidfloor': bidRequests[0].params.bidFloor, 'banner': { 'w': 300, 'h': 250, @@ -503,7 +282,8 @@ describe('TheMediaGrid Adapter', function () { }, 'video': { 'w': 400, - 'h': 600 + 'h': 600, + 'protocols': [1, 2, 3] } }] }); @@ -511,7 +291,7 @@ describe('TheMediaGrid Adapter', function () { it('if gdprConsent is present payload must have gdpr params', function () { const gdprBidderRequest = Object.assign({gdprConsent: {consentString: 'AAA', gdprApplies: true}}, bidderRequest); - const [request] = spec.buildRequests(bidRequests, gdprBidderRequest); + const request = spec.buildRequests(bidRequests, gdprBidderRequest); expect(request.data).to.be.an('string'); const payload = parseRequest(request.data); expect(payload).to.have.property('user'); @@ -524,7 +304,7 @@ describe('TheMediaGrid Adapter', function () { it('if usPrivacy is present payload must have us_privacy param', function () { const bidderRequestWithUSP = Object.assign({uspConsent: '1YNN'}, bidderRequest); - const [request] = spec.buildRequests(bidRequests, bidderRequestWithUSP); + const request = spec.buildRequests(bidRequests, bidderRequestWithUSP); expect(request.data).to.be.an('string'); const payload = parseRequest(request.data); expect(payload).to.have.property('regs'); @@ -533,25 +313,36 @@ describe('TheMediaGrid Adapter', function () { }); it('if userId is present payload must have user.ext param with right keys', function () { + const eids = [ + { + source: 'pubcid.org', + uids: [{ + id: 'some-random-id-value', + atype: 1 + }] + }, + { + source: 'adserver.org', + uids: [{ + id: 'some-random-id-value', + atype: 1, + ext: { + rtiPartner: 'TDID' + } + }] + } + ]; const bidRequestsWithUserIds = bidRequests.map((bid) => { return Object.assign({ - userId: { - id5id: 'id5id_1', - tdid: 'tdid_1', - digitrustid: {data: {id: 'DTID', keyv: 4, privacy: {optout: false}, producer: 'ABC', version: 2}}, - lipb: {lipbid: 'lipb_1'} - } + userIdAsEids: eids }, bid); }); - const [request] = spec.buildRequests(bidRequestsWithUserIds, bidderRequest); + 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).to.have.property('unifiedid', 'tdid_1'); - expect(payload.user.ext).to.have.property('id5id', 'id5id_1'); - expect(payload.user.ext).to.have.property('digitrustid', 'DTID'); - expect(payload.user.ext).to.have.property('liveintentid', 'lipb_1'); + expect(payload.user.ext.eids).to.deep.equal(eids); }); it('if schain is present payload must have source.ext.schain param', function () { @@ -570,7 +361,7 @@ describe('TheMediaGrid Adapter', function () { schain: schain }, bid); }); - const [request] = spec.buildRequests(bidRequestsWithSChain, bidderRequest); + const request = spec.buildRequests(bidRequestsWithSChain, bidderRequest); expect(request.data).to.be.an('string'); const payload = parseRequest(request.data); expect(payload).to.have.property('source'); @@ -578,14 +369,160 @@ describe('TheMediaGrid Adapter', function () { expect(payload.source.ext).to.have.property('schain'); expect(payload.source.ext.schain).to.deep.equal(schain); }); + + 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 bidRequestsWithJwTargeting = bidRequests.map((bid) => { + return Object.assign({ + rtd: { + jwplayer: { + targeting: { + segments: jsSegments, + content: jsContent + } + } + } + }, bid); + }); + const request = spec.buildRequests(bidRequestsWithJwTargeting, bidderRequest); + expect(request.data).to.be.an('string'); + const payload = parseRequest(request.data); + expect(payload).to.have.property('user'); + expect(payload.user.data).to.deep.equal([{ + name: 'iow_labs_pub_data', + segment: [ + {name: 'jwpseg', value: jsSegments[0]}, + {name: 'jwpseg', value: jsSegments[1]} + ] + }]); + expect(payload).to.have.property('site'); + expect(payload.site.content).to.deep.equal(jsContent); + }); + + it('should contain the keyword values if it present in ortb2.(site/user)', function () { + const getConfigStub = sinon.stub(config, 'getConfig').callsFake( + arg => arg === 'ortb2.user' ? {'keywords': 'foo'} : (arg === 'ortb2.site' ? {'keywords': 'bar'} : null)); + const request = spec.buildRequests([bidRequests[0]], bidderRequest); + expect(request.data).to.be.an('string'); + const payload = parseRequest(request.data); + expect(payload.ext.keywords).to.deep.equal([{'key': 'user', 'value': ['foo']}, {'key': 'context', 'value': ['bar']}]); + getConfigStub.restore(); + }); + + it('shold be right tmax when timeout in config is less then timeout in bidderRequest', function() { + const getConfigStub = sinon.stub(config, 'getConfig').callsFake( + arg => arg === 'bidderTimeout' ? 2000 : null); + const request = spec.buildRequests([bidRequests[0]], bidderRequest); + expect(request.data).to.be.an('string'); + const payload = parseRequest(request.data); + expect(payload.tmax).to.equal(2000); + getConfigStub.restore(); + }); + it('shold be right tmax when timeout in bidderRequest is less then timeout in config', function() { + const getConfigStub = sinon.stub(config, 'getConfig').callsFake( + arg => arg === 'bidderTimeout' ? 5000 : null); + const request = spec.buildRequests([bidRequests[0]], bidderRequest); + expect(request.data).to.be.an('string'); + const payload = parseRequest(request.data); + expect(payload.tmax).to.equal(3000); + getConfigStub.restore(); + }); + it('should contain regs.coppa if coppa is true in config', function () { + const getConfigStub = sinon.stub(config, 'getConfig').callsFake( + arg => arg === 'coppa' ? true : null); + const request = spec.buildRequests([bidRequests[0]], bidderRequest); + expect(request.data).to.be.an('string'); + const payload = parseRequest(request.data); + expect(payload).to.have.property('regs'); + expect(payload.regs).to.have.property('coppa', 1); + getConfigStub.restore(); + }); + it('should contain imp[].ext.data.adserver if available', function() { + const ortb2Imp = [{ + ext: { + data: { + adserver: { + name: 'ad_server_name', + adslot: '/111111/slot' + }, + pbadslot: '/111111/slot' + } + } + }, { + ext: { + data: { + adserver: { + name: 'ad_server_name', + adslot: '/222222/slot' + }, + pbadslot: '/222222/slot' + } + } + }]; + const bidRequestsWithOrtb2Imp = bidRequests.slice(0, 3).map((bid, ind) => { + return Object.assign(ortb2Imp[ind] ? { ortb2Imp: ortb2Imp[ind] } : {}, bid); + }); + const request = spec.buildRequests(bidRequestsWithOrtb2Imp, bidderRequest); + expect(request.data).to.be.an('string'); + const payload = parseRequest(request.data); + expect(payload.imp[0].ext).to.deep.equal({ + divid: bidRequests[0].adUnitCode, + data: ortb2Imp[0].ext.data, + gpid: ortb2Imp[0].ext.data.adserver.adslot + }); + expect(payload.imp[1].ext).to.deep.equal({ + divid: bidRequests[1].adUnitCode, + data: ortb2Imp[1].ext.data, + gpid: ortb2Imp[1].ext.data.adserver.adslot + }); + expect(payload.imp[2].ext).to.deep.equal({ + divid: bidRequests[2].adUnitCode + }); + }); + describe('floorModule', function () { + const floorTestData = { + 'currency': 'USD', + 'floor': 1.50 + }; + const bidRequest = Object.assign({ + getFloor: (_) => { + return floorTestData; + } + }, bidRequests[1]); + it('should return the value from getFloor if present', function () { + const request = spec.buildRequests([bidRequest], bidderRequest); + expect(request.data).to.be.an('string'); + const payload = parseRequest(request.data); + expect(payload.imp[0].bidfloor).to.equal(floorTestData.floor); + }); + it('should return the getFloor.floor value if it is greater than bidfloor', function () { + const bidfloor = 0.80; + const bidRequestsWithFloor = { ...bidRequest }; + bidRequestsWithFloor.params = Object.assign({bidFloor: bidfloor}, bidRequestsWithFloor.params); + const request = spec.buildRequests([bidRequestsWithFloor], bidderRequest); + expect(request.data).to.be.an('string'); + const payload = parseRequest(request.data); + expect(payload.imp[0].bidfloor).to.equal(floorTestData.floor); + }); + it('should return the bidfloor value if it is greater than getFloor.floor', function () { + const bidfloor = 1.80; + const bidRequestsWithFloor = { ...bidRequest }; + bidRequestsWithFloor.params = Object.assign({bidFloor: bidfloor}, bidRequestsWithFloor.params); + const request = spec.buildRequests([bidRequestsWithFloor], bidderRequest); + expect(request.data).to.be.an('string'); + const payload = parseRequest(request.data); + expect(payload.imp[0].bidfloor).to.equal(bidfloor); + }); + }); }); describe('interpretResponse', function () { const responses = [ - {'bid': [{'price': 1.15, 'adm': '
test content 1
', 'auid': 1, 'h': 250, 'w': 300, dealid: 11}], 'seat': '1'}, - {'bid': [{'price': 0.5, 'adm': '
test content 2
', 'auid': 2, 'h': 600, 'w': 300}], 'seat': '1'}, - {'bid': [{'price': 0.15, 'adm': '
test content 3
', 'auid': 1, 'h': 90, 'w': 728}], 'seat': '1'}, - {'bid': [{'price': 0, 'auid': 3, 'h': 250, 'w': 300}], 'seat': '1'}, + {'bid': [{'impid': '659423fff799cb', 'price': 1.15, 'adm': '
test content 1
', 'auid': 1, 'h': 250, 'w': 300, 'dealid': 11}], 'seat': '1'}, + {'bid': [{'impid': '4dff80cc4ee346', 'price': 0.5, 'adm': '
test content 2
', 'auid': 2, 'h': 600, 'w': 300}], 'seat': '1'}, + {'bid': [{'impid': '5703af74d0472a', 'price': 0.15, 'adm': '
test content 3
', 'auid': 1, 'h': 90, 'w': 728}], 'seat': '1'}, + {'bid': [{'impid': '2344da98f78b42', 'price': 0, 'auid': 3, 'h': 250, 'w': 300}], 'seat': '1'}, {'bid': [{'price': 0, 'adm': '
test content 5
', 'h': 250, 'w': 300}], 'seat': '1'}, undefined, {'bid': [], 'seat': '1'}, @@ -606,7 +543,7 @@ describe('TheMediaGrid Adapter', function () { 'auctionId': '1cbd2feafe5e8b', } ]; - const [request] = spec.buildRequests(bidRequests); + const request = spec.buildRequests(bidRequests); const expectedResponse = [ { 'requestId': '659423fff799cb', @@ -616,11 +553,13 @@ describe('TheMediaGrid Adapter', function () { 'width': 300, 'height': 250, 'ad': '
test content 1
', - 'bidderCode': 'grid', 'currency': 'USD', 'mediaType': 'banner', 'netRevenue': false, 'ttl': 360, + 'meta': { + advertiserDomains: [] + }, } ]; @@ -637,7 +576,7 @@ describe('TheMediaGrid Adapter', function () { }, 'adUnitCode': 'adunit-code-1', 'sizes': [[300, 250], [300, 600]], - 'bidId': '300bfeb0d71a5b', + 'bidId': '659423fff799cb', 'bidderRequestId': '2c2bb1972df9a', 'auctionId': '1fa09aee5c8c99', }, @@ -664,21 +603,23 @@ describe('TheMediaGrid Adapter', function () { 'auctionId': '1fa09aee5c8c99', } ]; - const [request] = spec.buildRequests(bidRequests); + const request = spec.buildRequests(bidRequests); const expectedResponse = [ { - 'requestId': '300bfeb0d71a5b', + 'requestId': '659423fff799cb', 'cpm': 1.15, 'creativeId': 1, 'dealId': 11, 'width': 300, 'height': 250, 'ad': '
test content 1
', - 'bidderCode': 'grid', 'currency': 'USD', 'mediaType': 'banner', 'netRevenue': false, 'ttl': 360, + 'meta': { + advertiserDomains: [] + }, }, { 'requestId': '4dff80cc4ee346', @@ -688,11 +629,13 @@ describe('TheMediaGrid Adapter', function () { 'width': 300, 'height': 600, 'ad': '
test content 2
', - 'bidderCode': 'grid', 'currency': 'USD', 'mediaType': 'banner', 'netRevenue': false, 'ttl': 360, + 'meta': { + advertiserDomains: [] + }, }, { 'requestId': '5703af74d0472a', @@ -702,11 +645,13 @@ describe('TheMediaGrid Adapter', function () { 'width': 728, 'height': 90, 'ad': '
test content 3
', - 'bidderCode': 'grid', 'currency': 'USD', 'mediaType': 'banner', 'netRevenue': false, 'ttl': 360, + 'meta': { + advertiserDomains: [] + }, } ]; @@ -750,10 +695,10 @@ describe('TheMediaGrid Adapter', function () { } ]; const response = [ - {'bid': [{'price': 1.15, 'adm': '\n<\/Ad>\n<\/VAST>', 'auid': 11, content_type: 'video', w: 300, h: 600}], 'seat': '2'}, - {'bid': [{'price': 1.00, 'adm': '\n<\/Ad>\n<\/VAST>', 'auid': 12, content_type: 'video'}], 'seat': '2'} + {'bid': [{'impid': '659423fff799cb', 'price': 1.15, 'adm': '\n<\/Ad>\n<\/VAST>', 'auid': 11, content_type: 'video', w: 300, h: 600}], 'seat': '2'}, + {'bid': [{'impid': '2bc598e42b6a', 'price': 1.00, 'adm': '\n<\/Ad>\n<\/VAST>', 'auid': 12, content_type: 'video'}], 'seat': '2'} ]; - const [request] = spec.buildRequests(bidRequests); + const request = spec.buildRequests(bidRequests); const expectedResponse = [ { 'requestId': '659423fff799cb', @@ -762,15 +707,36 @@ describe('TheMediaGrid Adapter', function () { 'dealId': undefined, 'width': 300, 'height': 600, - 'bidderCode': 'grid', 'currency': 'USD', 'mediaType': 'video', 'netRevenue': false, 'ttl': 360, + 'meta': { + advertiserDomains: [] + }, 'vastXml': '\n<\/Ad>\n<\/VAST>', 'adResponse': { 'content': '\n<\/Ad>\n<\/VAST>' } + }, + { + 'requestId': '2bc598e42b6a', + 'cpm': 1.00, + 'creativeId': 12, + 'dealId': undefined, + 'width': undefined, + 'height': undefined, + 'currency': 'USD', + 'mediaType': 'video', + 'netRevenue': false, + 'ttl': 360, + 'meta': { + advertiserDomains: [] + }, + 'vastXml': '\n<\/Ad>\n<\/VAST>', + 'adResponse': { + 'content': '\n<\/Ad>\n<\/VAST>' + } } ]; @@ -814,18 +780,18 @@ describe('TheMediaGrid Adapter', function () { 'auctionId': '1fa09aee5c84d34', } ]; - const [request] = spec.buildRequests(bidRequests); + const request = spec.buildRequests(bidRequests); const result = spec.interpretResponse({'body': {'seatbid': responses.slice(2)}}, request); expect(result.length).to.equal(0); }); it('complicated case', function () { const fullResponse = [ - {'bid': [{'price': 1.15, 'adm': '
test content 1
', 'auid': 1, 'h': 250, 'w': 300, dealid: 11}], 'seat': '1'}, - {'bid': [{'price': 0.5, 'adm': '
test content 2
', 'auid': 2, 'h': 600, 'w': 300, dealid: 12}], 'seat': '1'}, - {'bid': [{'price': 0.15, 'adm': '
test content 3
', 'auid': 1, 'h': 90, 'w': 728}], 'seat': '1'}, - {'bid': [{'price': 0.15, 'adm': '
test content 4
', 'auid': 1, 'h': 600, 'w': 300}], 'seat': '1'}, - {'bid': [{'price': 0.5, 'adm': '
test content 5
', 'auid': 2, 'h': 600, 'w': 350}], 'seat': '1'}, + {'bid': [{'impid': '2164be6358b9', 'price': 1.15, 'adm': '
test content 1
', 'auid': 1, 'h': 250, 'w': 300, dealid: 11}], 'seat': '1'}, + {'bid': [{'impid': '4e111f1b66e4', 'price': 0.5, 'adm': '
test content 2
', 'auid': 2, 'h': 600, 'w': 300, dealid: 12}], 'seat': '1'}, + {'bid': [{'impid': '26d6f897b516', 'price': 0.15, 'adm': '
test content 3
', 'auid': 1, 'h': 90, 'w': 728}], 'seat': '1'}, + {'bid': [{'impid': '326bde7fbf69', 'price': 0.15, 'adm': '
test content 4
', 'auid': 1, 'h': 600, 'w': 300}], 'seat': '1'}, + {'bid': [{'impid': '2234f233b22a', 'price': 0.5, 'adm': '
test content 5
', 'auid': 2, 'h': 600, 'w': 350}], 'seat': '1'}, ]; const bidRequests = [ { @@ -884,7 +850,7 @@ describe('TheMediaGrid Adapter', function () { 'auctionId': '32a1f276cb87cb8', } ]; - const [request] = spec.buildRequests(bidRequests); + const request = spec.buildRequests(bidRequests); const expectedResponse = [ { 'requestId': '2164be6358b9', @@ -894,11 +860,13 @@ describe('TheMediaGrid Adapter', function () { 'width': 300, 'height': 250, 'ad': '
test content 1
', - 'bidderCode': 'grid', 'currency': 'USD', 'mediaType': 'banner', 'netRevenue': false, 'ttl': 360, + 'meta': { + advertiserDomains: [] + }, }, { 'requestId': '4e111f1b66e4', @@ -908,11 +876,13 @@ describe('TheMediaGrid Adapter', function () { 'width': 300, 'height': 600, 'ad': '
test content 2
', - 'bidderCode': 'grid', 'currency': 'USD', 'mediaType': 'banner', 'netRevenue': false, 'ttl': 360, + 'meta': { + advertiserDomains: [] + }, }, { 'requestId': '26d6f897b516', @@ -922,11 +892,13 @@ describe('TheMediaGrid Adapter', function () { 'width': 728, 'height': 90, 'ad': '
test content 3
', - 'bidderCode': 'grid', 'currency': 'USD', 'mediaType': 'banner', 'netRevenue': false, 'ttl': 360, + 'meta': { + advertiserDomains: [] + }, }, { 'requestId': '326bde7fbf69', @@ -936,87 +908,13 @@ describe('TheMediaGrid Adapter', function () { 'width': 300, 'height': 600, 'ad': '
test content 4
', - 'bidderCode': 'grid', 'currency': 'USD', 'mediaType': 'banner', 'netRevenue': false, 'ttl': 360, - } - ]; - - const result = spec.interpretResponse({'body': {'seatbid': fullResponse}}, request); - expect(result).to.deep.equal(expectedResponse); - }); - - it('dublicate uids and sizes in one slot', function () { - const fullResponse = [ - {'bid': [{'price': 1.15, 'adm': '
test content 1
', 'auid': 1, 'h': 250, 'w': 300}], 'seat': '1'}, - {'bid': [{'price': 0.5, 'adm': '
test content 2
', 'auid': 1, 'h': 250, 'w': 300}], 'seat': '1'}, - ]; - const bidRequests = [ - { - 'bidder': 'grid', - 'params': { - 'uid': '1' - }, - 'adUnitCode': 'adunit-code-1', - 'sizes': [[300, 250], [300, 600]], - 'bidId': '5126e301f4be', - 'bidderRequestId': '171c5405a390', - 'auctionId': '35bcbc0f7e79c', - }, - { - 'bidder': 'grid', - 'params': { - 'uid': '1' - }, - 'adUnitCode': 'adunit-code-1', - 'sizes': [[300, 250], [300, 600]], - 'bidId': '57b2ebe70e16', - 'bidderRequestId': '171c5405a390', - 'auctionId': '35bcbc0f7e79c', - }, - { - 'bidder': 'grid', - 'params': { - 'uid': '1' + 'meta': { + advertiserDomains: [] }, - 'adUnitCode': 'adunit-code-1', - 'sizes': [[300, 250], [300, 600]], - 'bidId': '225fcd44b18c', - 'bidderRequestId': '171c5405a390', - 'auctionId': '35bcbc0f7e79c', - } - ]; - const [request] = spec.buildRequests(bidRequests); - const expectedResponse = [ - { - 'requestId': '5126e301f4be', - 'cpm': 1.15, - 'creativeId': 1, - 'dealId': undefined, - 'width': 300, - 'height': 250, - 'ad': '
test content 1
', - 'bidderCode': 'grid', - 'currency': 'USD', - 'mediaType': 'banner', - 'netRevenue': false, - 'ttl': 360, - }, - { - 'requestId': '57b2ebe70e16', - 'cpm': 0.5, - 'creativeId': 1, - 'dealId': undefined, - 'width': 300, - 'height': 250, - 'ad': '
test content 2
', - 'bidderCode': 'grid', - 'currency': 'USD', - 'mediaType': 'banner', - 'netRevenue': false, - 'ttl': 360, } ]; diff --git a/test/spec/modules/gridNMBidAdapter_spec.js b/test/spec/modules/gridNMBidAdapter_spec.js index 0dbaac0c526..2aec9713000 100644 --- a/test/spec/modules/gridNMBidAdapter_spec.js +++ b/test/spec/modules/gridNMBidAdapter_spec.js @@ -300,7 +300,6 @@ describe('TheMediaGridNM Adapter', function () { 'dealId': 11, 'width': 300, 'height': 250, - 'bidderCode': 'gridNM', 'currency': 'USD', 'mediaType': 'video', 'netRevenue': false, @@ -317,7 +316,6 @@ describe('TheMediaGridNM Adapter', function () { 'dealId': undefined, 'width': 300, 'height': 600, - 'bidderCode': 'gridNM', 'currency': 'USD', 'mediaType': 'video', 'netRevenue': false, diff --git a/test/spec/modules/gumgumBidAdapter_spec.js b/test/spec/modules/gumgumBidAdapter_spec.js index af929f437da..a7b18a16173 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'; @@ -33,7 +35,11 @@ describe('gumgumAdapter', function () { }; it('should return true when required params found', function () { + const zoneBid = { ...bid, params: { 'zone': '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); }); it('should return true when required params found', function () { @@ -46,6 +52,17 @@ describe('gumgumAdapter', function () { expect(spec.isBidRequestValid(bid)).to.equal(true); }); + it('should return true when inslot sends sizes and trackingid', function () { + let bid = Object.assign({}, bid); + delete bid.params; + bid.params = { + 'inSlot': '789', + 'sizes': [[0, 1], [2, 3], [4, 5], [6, 7]] + }; + + expect(spec.isBidRequestValid(bid)).to.equal(true); + }); + it('should return false when no unit type is specified', function () { let bid = Object.assign({}, bid); delete bid.params; @@ -81,6 +98,7 @@ describe('gumgumAdapter', function () { }); describe('buildRequests', function () { + let sizesArray = [[300, 250], [300, 600]]; let bidRequests = [ { 'bidder': 'gumgum', @@ -88,7 +106,7 @@ describe('gumgumAdapter', function () { 'inSlot': '9' }, 'adUnitCode': 'adunit-code', - 'sizes': [[300, 250], [300, 600]], + 'sizes': sizesArray, 'bidId': '30b31c1838de1e', 'schain': { 'ver': '1.0', @@ -126,13 +144,94 @@ describe('gumgumAdapter', function () { protocols: [1, 2] } }; + const zoneParam = { 'zone': '123a' }; + const pubIdParam = { 'pubId': 123 }; + + 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 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 bidRequest = spec.buildRequests([request])[0]; + expect(bidRequest.data.pi).to.equal(5); + }); + it('should set the correct pi param for video', function () { + const request = { ...bidRequests[0], params: zoneParam, mediaTypes: vidMediaTypes }; + const bidRequest = spec.buildRequests([request])[0]; + expect(bidRequest.data.pi).to.equal(7); + }); + it('should set the correct pi param for invideo', function () { + const invideo = { video: { ...vidMediaTypes.video, linearity: 2 } }; + const request = { ...bidRequests[0], params: zoneParam, mediaTypes: invideo }; + const bidRequest = spec.buildRequests([request])[0]; + expect(bidRequest.data.pi).to.equal(6); + }); + 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.pi).to.equal(3); + }); + 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); + }); + }); it('should return a defined sizes field for video', function () { const request = { ...bidRequests[0], mediaTypes: vidMediaTypes, params: { 'videoPubID': 123 } }; const bidRequest = spec.buildRequests([request])[0]; expect(bidRequest.sizes).to.equal(vidMediaTypes.video.playerSize); }); - + it('should handle multiple sizes for inslot', function () { + const mediaTypes = { banner: { sizes: [[300, 250], [300, 600]] } } + const request = { ...bidRequests[0], mediaTypes }; + const bidRequest = spec.buildRequests([request])[0]; + expect(bidRequest.data.bf).to.equal('300x250,300x600'); + }); describe('floorModule', function () { const floorTestData = { 'currency': 'USD', @@ -142,9 +241,8 @@ describe('gumgumAdapter', function () { return floorTestData; }; it('should return the value from getFloor if present', function () { - const request = { ...bidRequests[0] }; - const bidRequest = spec.buildRequests([request])[0]; - expect(bidRequest.data.fp).to.equal(floorTestData.floor); + const request = spec.buildRequests(bidRequests)[0]; + expect(request.data.fp).to.equal(floorTestData.floor); }); it('should return the getFloor.floor value if it is greater than bidfloor', function () { const bidfloor = 0.80; @@ -160,6 +258,10 @@ describe('gumgumAdapter', function () { const bidRequest = spec.buildRequests([request])[0]; expect(bidRequest.data.fp).to.equal(bidfloor); }); + it('should return a floor currency', function () { + const request = spec.buildRequests(bidRequests)[0]; + expect(request.data.fpc).to.equal(floorTestData.currency); + }) }); it('sends bid request to ENDPOINT via GET', function () { @@ -180,6 +282,25 @@ describe('gumgumAdapter', function () { expect(bidRequest.data).to.include.any.keys('t'); expect(bidRequest.data).to.include.any.keys('fp'); }); + it('should set iriscat parameter if iriscat param is found and is of type string', function () { + const iriscat = 'segment'; + const request = { ...bidRequests[0] }; + request.params = { ...request.params, iriscat }; + const bidRequest = spec.buildRequests([request])[0]; + expect(bidRequest.data.iriscat).to.equal(iriscat); + }); + it('should not send iriscat parameter if iriscat param is not found', function () { + const request = { ...bidRequests[0] }; + const bidRequest = spec.buildRequests([request])[0]; + expect(bidRequest.data.iriscat).to.be.undefined; + }); + it('should not send iriscat parameter if iriscat param is not of type string', function () { + const iriscat = 123; + const request = { ...bidRequests[0] }; + request.params = { ...request.params, iriscat }; + const bidRequest = spec.buildRequests([request])[0]; + expect(bidRequest.data.iriscat).to.be.undefined; + }); it('should send pubId if inScreenPubID param is specified', function () { const request = Object.assign({}, bidRequests[0]); delete request.params; @@ -340,60 +461,85 @@ describe('gumgumAdapter', function () { }) describe('interpretResponse', function () { - let serverResponse = { - 'ad': { - 'id': 29593, - 'width': 300, - 'height': 250, - 'ipd': 2000, - 'markup': '

I am an ad

', - 'ii': true, - 'du': null, - 'price': 0, - 'zi': 0, - 'impurl': 'http://g2.gumgum.com/ad/view', - 'clsurl': 'http://g2.gumgum.com/ad/close' + const metaData = { adomain: ['advertiser.com'], mediaType: BANNER } + const serverResponse = { + ad: { + id: 29593, + width: 300, + height: 250, + ipd: 2000, + markup: '

I am an ad

', + ii: true, + du: null, + price: 0, + zi: 0, + impurl: 'http://g2.gumgum.com/ad/view', + clsurl: 'http://g2.gumgum.com/ad/close' }, - 'pag': { - 't': 'ggumtest', - 'pvid': 'aa8bbb65-427f-4689-8cee-e3eed0b89eec', - 'css': 'html { overflow-y: auto }', - 'js': 'console.log("environment", env);' + pag: { + t: 'ggumtest', + pvid: 'aa8bbb65-427f-4689-8cee-e3eed0b89eec', + css: 'html { overflow-y: auto }', + js: 'console.log("environment", env);' }, - 'jcsi': { t: 0, rq: 8 }, - 'thms': 10000 + jcsi: { t: 0, rq: 8 }, + thms: 10000, + meta: metaData } - let bidRequest = { + const bidRequest = { id: 12345, sizes: [[300, 250], [1, 1]], url: ENDPOINT, method: 'GET', 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, - ttl: 60 + const expectedMetaData = { advertiserDomains: ['advertiser.com'], mediaType: BANNER }; + const expectedResponse = { + ad: '

I am an ad

', + cpm: 0, + creativeId: 29593, + currency: 'USD', + height: '250', + netRevenue: true, + requestId: 12345, + width: '300', + mediaType: BANNER, + ttl: 60, + meta: expectedMetaData }; it('should get correct bid response', function () { expect(spec.interpretResponse({ body: serverResponse }, bidRequest)).to.deep.equal([expectedResponse]); }); + it('should set a default value for advertiserDomains if adomain is not found', function () { + const meta = { ...metaData }; + delete meta.adomain; + + const response = { ...serverResponse, meta }; + const expectedMeta = { ...expectedMetaData, advertiserDomains: [] }; + const expected = { ...expectedResponse, meta: expectedMeta }; + + expect(spec.interpretResponse({ body: response }, bidRequest)).to.deep.equal([expected]); + }); + + it('should set a default value for meta.mediaType if mediaType is not found in the response', function () { + const meta = { ...metaData }; + delete meta.mediaType; + const response = { ...serverResponse, meta }; + const expected = { ...expectedResponse }; + + expect(spec.interpretResponse({ body: response }, bidRequest)).to.deep.equal([expected]); + }); + it('should pass correct currency if found in bid response', function () { const cur = 'EURO'; - let response = Object.assign({}, serverResponse); - let expected = Object.assign({}, expectedResponse); + const response = { ...serverResponse }; response.ad.cur = cur; + + const expected = { ...expectedResponse }; expected.currency = cur; + expect(spec.interpretResponse({ body: response }, bidRequest)).to.deep.equal([expected]); }); @@ -418,6 +564,25 @@ describe('gumgumAdapter', function () { expect(result.length).to.equal(0); }); + it('uses response width and height', function () { + const result = spec.interpretResponse({ body: serverResponse }, bidRequest)[0]; + expect(result.width).to.equal(serverResponse.ad.width.toString()); + expect(result.height).to.equal(serverResponse.ad.height.toString()); + }); + + it('defaults to use bidRequest sizes when width and height are not found', function () { + const { ad, jcsi, pag, thms, meta } = serverResponse + const noAdSizes = { ...ad } + delete noAdSizes.width + delete noAdSizes.height + const responseWithoutSizes = { jcsi, pag, thms, meta, ad: noAdSizes } + const request = { ...bidRequest, sizes: [[100, 200]] } + const result = spec.interpretResponse({ body: responseWithoutSizes }, request)[0]; + + expect(result.width).to.equal(request.sizes[0][0].toString()) + expect(result.height).to.equal(request.sizes[0][1].toString()) + }); + it('returns 1x1 when eligible product and size available', function () { let inscreenBidRequest = { id: 12346, @@ -461,6 +626,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/h12mediaBidAdapter_spec.js b/test/spec/modules/h12mediaBidAdapter_spec.js index 08a83ce981f..9861069f260 100644 --- a/test/spec/modules/h12mediaBidAdapter_spec.js +++ b/test/spec/modules/h12mediaBidAdapter_spec.js @@ -1,6 +1,7 @@ import {expect} from 'chai'; import {spec} from 'modules/h12mediaBidAdapter'; import {newBidder} from 'src/adapters/bidderFactory'; +import * as utils from 'src/utils'; describe('H12 Media Adapter', function () { const DEFAULT_CURRENCY = 'USD'; @@ -21,6 +22,7 @@ describe('H12 Media Adapter', function () { auctionId: '9adc85ed-43ee-4a78-816b-52b7e578f313', params: { pubid: 123321, + pubsubid: 'pubsubtestid', }, }; @@ -72,34 +74,34 @@ describe('H12 Media Adapter', function () { currency: 'EUR', netRevenue: true, ttl: 500, - bids: [{ + bid: { bidId: validBid.bidId, cpm: 0.33, width: 300, height: 600, creativeId: '335566', ad: '
my ad
', - usersync: [ - {url: 'https://cookiesync.3rdpartypartner.com/?3rdparty_partner_user_id={user_id}&partner_id=h12media&gdpr_applies={gdpr}&gdpr_consent_string={gdpr_cs}', type: 'image'}, - {url: 'https://cookiesync.3rdpartypartner.com/?3rdparty_partner_user_id={user_id}&partner_id=h12media&gdpr_applies={gdpr}&gdpr_consent_string={gdpr_cs}', type: 'iframe'} - ], meta: { advertiserId: '54321', advertiserName: 'My advertiser', advertiserDomains: ['test.com'] } - }] + }, + usersync: [ + {url: 'https://cookiesync.3rdpartypartner.com/?3rdparty_partner_user_id={user_id}&partner_id=h12media&gdpr_applies={gdpr}&gdpr_consent_string={gdpr_cs}', type: 'image'}, + {url: 'https://cookiesync.3rdpartypartner.com/?3rdparty_partner_user_id={user_id}&partner_id=h12media&gdpr_applies={gdpr}&gdpr_consent_string={gdpr_cs}', type: 'iframe'} + ], }; const serverResponse2 = { - bids: [{ + bid: { bidId: validBid2.bidId, cpm: 0.33, width: 300, height: 600, creativeId: '335566', ad: '
my ad 2
', - }] + } }; function removeElement(id) { @@ -152,6 +154,10 @@ describe('H12 Media Adapter', function () { beforeEach(function () { sandbox = sinon.sandbox.create(); + sandbox.stub(frameElement, 'getBoundingClientRect').returns({ + left: 10, + top: 10, + }); }); afterEach(function () { @@ -186,36 +192,62 @@ describe('H12 Media Adapter', function () { createElementVisible(validBid.adUnitCode); createElementVisible(validBid2.adUnitCode); const requests = spec.buildRequests([validBid, validBid2], bidderRequest); - const requestsData = requests.data; + const requestsData = requests[0].data.bidrequest; - expect(requestsData.bidrequests[0]).to.include({adunitSize: validBid.mediaTypes.banner.sizes}); + expect(requestsData).to.include({adunitSize: validBid.mediaTypes.banner.sizes}); }); it('should return empty bid size', function () { createElementVisible(validBid.adUnitCode); createElementVisible(validBid2.adUnitCode); const requests = spec.buildRequests([validBid, validBid2], bidderRequest); - const requestsData = requests.data; + const requestsData2 = requests[1].data.bidrequest; + + expect(requestsData2).to.deep.include({adunitSize: []}); + }); + + it('should return pubsubid from params', function () { + createElementVisible(validBid.adUnitCode); + createElementVisible(validBid2.adUnitCode); + const requests = spec.buildRequests([validBid, validBid2], bidderRequest); + const requestsData = requests[0].data.bidrequest; + const requestsData2 = requests[1].data.bidrequest; + + expect(requestsData).to.include({pubsubid: 'pubsubtestid'}); + expect(requestsData2).to.include({pubsubid: ''}); + }); - expect(requestsData.bidrequests[1]).to.deep.include({adunitSize: []}); + it('should return empty for incorrect pubsubid from params', function () { + createElementVisible(validBid.adUnitCode); + createElementVisible(validBid2.adUnitCode); + const bidWithPub = {...validBid}; + bidWithPub.params.pubsubid = 'iiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiii'; // More than 32 chars + const requests = spec.buildRequests([bidWithPub], bidderRequest); + const requestsData = requests[0].data.bidrequest; + + expect(requestsData).to.include({pubsubid: ''}); }); it('should return bid size from params', function () { createElementVisible(validBid.adUnitCode); createElementVisible(validBid2.adUnitCode); const requests = spec.buildRequests([validBid, validBid2], bidderRequest); - const requestsData = requests.data; + const requestsData = requests[0].data.bidrequest; + const requestsData2 = requests[1].data.bidrequest; - expect(requestsData.bidrequests[1]).to.include({size: validBid2.params.size}); + expect(requestsData).to.include({size: ''}); + expect(requestsData2).to.include({size: validBid2.params.size}); }); it('should return GDPR info', function () { createElementVisible(validBid.adUnitCode); createElementVisible(validBid2.adUnitCode); const requests = spec.buildRequests([validBid, validBid2], bidderRequest); - const requestsData = requests.data; + const requestsData = requests[0].data; + const requestsData2 = requests[1].data; expect(requestsData).to.include({gdpr: true, gdpr_cs: bidderRequest.gdprConsent.consentString}); + expect(requestsData2).to.include({gdpr: true, gdpr_cs: bidderRequest.gdprConsent.consentString}); }); it('should not have error on empty GDPR', function () { @@ -223,9 +255,23 @@ describe('H12 Media Adapter', function () { createElementVisible(validBid2.adUnitCode); const bidderRequestWithoutGDRP = {...bidderRequest, gdprConsent: null}; const requests = spec.buildRequests([validBid, validBid2], bidderRequestWithoutGDRP); - const requestsData = requests.data; + const requestsData = requests[0].data; + const requestsData2 = requests[1].data; expect(requestsData).to.include({gdpr: false}); + expect(requestsData2).to.include({gdpr: false}); + }); + + it('should not have error on empty USP', function () { + createElementVisible(validBid.adUnitCode); + createElementVisible(validBid2.adUnitCode); + const bidderRequestWithoutUSP = {...bidderRequest, uspConsent: null}; + const requests = spec.buildRequests([validBid, validBid2], bidderRequestWithoutUSP); + const requestsData = requests[0].data; + const requestsData2 = requests[1].data; + + expect(requestsData).to.include({usp: false}); + expect(requestsData2).to.include({usp: false}); }); it('should create single POST', function () { @@ -233,7 +279,8 @@ describe('H12 Media Adapter', function () { createElementVisible(validBid2.adUnitCode); const requests = spec.buildRequests([validBid, validBid2], bidderRequest); - expect(requests.method).to.equal('POST'); + expect(requests[0].method).to.equal('POST'); + expect(requests[1].method).to.equal('POST'); }); }); @@ -241,42 +288,44 @@ describe('H12 Media Adapter', function () { it('should return coords', function () { createElementVisible(validBid.adUnitCode); const requests = spec.buildRequests([validBid], bidderRequest); - const requestsData = requests.data; + const requestsData = requests[0].data.bidrequest; - expect(requestsData.bidrequests[0]).to.deep.include({coords: {x: 10, y: 10}}); + expect(requestsData).to.deep.include({coords: {x: 10, y: 10}}); }); - it('should define not iframe', function () { + it('should define iframe', function () { createElementVisible(validBid.adUnitCode); createElementVisible(validBid2.adUnitCode); const requests = spec.buildRequests([validBid, validBid2], bidderRequest); - const requestsData = requests.data; + const requestsData = requests[0].data; + const requestsData2 = requests[1].data; - expect(requestsData).to.include({isiframe: false}); + expect(requestsData).to.include({isiframe: true}); + expect(requestsData2).to.include({isiframe: true}); }); it('should define visible element', function () { createElementVisible(validBid.adUnitCode); const requests = spec.buildRequests([validBid], bidderRequest); - const requestsData = requests.data; + const requestsData = requests[0].data.bidrequest; - expect(requestsData.bidrequests[0]).to.include({ishidden: false}); + expect(requestsData).to.include({ishidden: false}); }); it('should define invisible element', function () { createElementInvisible(validBid.adUnitCode); const requests = spec.buildRequests([validBid], bidderRequest); - const requestsData = requests.data; + const requestsData = requests[0].data.bidrequest; - expect(requestsData.bidrequests[0]).to.include({ishidden: true}); + expect(requestsData).to.include({ishidden: true}); }); it('should define hidden element', function () { createElementHidden(validBid.adUnitCode); const requests = spec.buildRequests([validBid], bidderRequest); - const requestsData = requests.data; + const requestsData = requests[0].data.bidrequest; - expect(requestsData.bidrequests[0]).to.include({ishidden: true}); + expect(requestsData).to.include({ishidden: true}); }); }); @@ -290,27 +339,27 @@ describe('H12 Media Adapter', function () { it('should return no bids if the response is empty', function () { const bidResponse = spec.interpretResponse({ body: [] }, { validBid }); - expect(bidResponse.length).to.equal(0); + expect(bidResponse).to.be.empty; }); it('should return valid bid responses', function () { createElementVisible(validBid.adUnitCode); createElementVisible(validBid2.adUnitCode); const request = spec.buildRequests([validBid, validBid2], bidderRequest); - const bidResponse = spec.interpretResponse({body: serverResponse}, request); + const bidResponse = spec.interpretResponse({body: serverResponse}, request[0]); expect(bidResponse[0]).to.deep.include({ requestId: validBid.bidId, - ad: serverResponse.bids[0].ad, + ad: serverResponse.bid.ad, mediaType: 'banner', - creativeId: serverResponse.bids[0].creativeId, - cpm: serverResponse.bids[0].cpm, - width: serverResponse.bids[0].width, - height: serverResponse.bids[0].height, + creativeId: serverResponse.bid.creativeId, + cpm: serverResponse.bid.cpm, + width: serverResponse.bid.width, + height: serverResponse.bid.height, currency: 'EUR', netRevenue: true, ttl: 500, - meta: serverResponse.bids[0].meta, + meta: serverResponse.bid.meta, pubid: validBid.params.pubid }); }); @@ -319,17 +368,17 @@ describe('H12 Media Adapter', function () { createElementVisible(validBid.adUnitCode); createElementVisible(validBid2.adUnitCode); const request = spec.buildRequests([validBid, validBid2], bidderRequest); - const bidResponse = spec.interpretResponse({body: serverResponse2}, request); + const bidResponse = spec.interpretResponse({body: serverResponse2}, request[0]); expect(bidResponse[0]).to.deep.include({ requestId: validBid2.bidId, - ad: serverResponse2.bids[0].ad, + ad: serverResponse2.bid.ad, mediaType: 'banner', - creativeId: serverResponse2.bids[0].creativeId, - cpm: serverResponse2.bids[0].cpm, - width: serverResponse2.bids[0].width, - height: serverResponse2.bids[0].height, - meta: serverResponse2.bids[0].meta, + creativeId: serverResponse2.bid.creativeId, + cpm: serverResponse2.bid.cpm, + width: serverResponse2.bid.width, + height: serverResponse2.bid.height, + meta: serverResponse2.bid.meta, pubid: validBid2.params.pubid, currency: DEFAULT_CURRENCY, netRevenue: DEFAULT_NET_REVENUE, diff --git a/test/spec/modules/haloIdSystem_spec.js b/test/spec/modules/haloIdSystem_spec.js new file mode 100644 index 00000000000..8b6a67adee1 --- /dev/null +++ b/test/spec/modules/haloIdSystem_spec.js @@ -0,0 +1,42 @@ +import { haloIdSubmodule, storage } from 'modules/haloIdSystem.js'; +import { server } from 'test/mocks/xhr.js'; +import * as utils from 'src/utils.js'; + +describe('HaloIdSystem', function () { + describe('getId', function() { + let getDataFromLocalStorageStub; + + beforeEach(function() { + getDataFromLocalStorageStub = sinon.stub(storage, 'getDataFromLocalStorage'); + }); + + afterEach(function () { + getDataFromLocalStorageStub.restore(); + }); + + it('gets a haloId', function() { + const config = { + params: {} + }; + const callbackSpy = sinon.spy(); + const callback = haloIdSubmodule.getId(config).callback; + callback(callbackSpy); + const request = server.requests[0]; + expect(request.url).to.eq(`https://id.halo.ad.gt/api/v1/pbhid`); + request.respond(200, { 'Content-Type': 'application/json' }, JSON.stringify({ haloId: 'testHaloId1' })); + expect(callbackSpy.lastCall.lastArg).to.deep.equal({haloId: 'testHaloId1'}); + }); + + it('gets a cached haloid', function() { + const config = { + params: {} + }; + getDataFromLocalStorageStub.withArgs('auHaloId').returns('tstCachedHaloId1'); + + const callbackSpy = sinon.spy(); + const callback = haloIdSubmodule.getId(config).callback; + callback(callbackSpy); + expect(callbackSpy.lastCall.lastArg).to.deep.equal({haloId: 'tstCachedHaloId1'}); + }); + }); +}); diff --git a/test/spec/modules/haloRtdProvider_spec.js b/test/spec/modules/haloRtdProvider_spec.js new file mode 100644 index 00000000000..3052441a00d --- /dev/null +++ b/test/spec/modules/haloRtdProvider_spec.js @@ -0,0 +1,374 @@ +import {config} from 'src/config.js'; +import {HALOID_LOCAL_NAME, RTD_LOCAL_NAME, addRealTimeData, getRealTimeData, haloSubmodule, storage} from 'modules/haloRtdProvider.js'; +import {server} from 'test/mocks/xhr.js'; + +const responseHeader = {'Content-Type': 'application/json'}; + +describe('haloRtdProvider', function() { + let getDataFromLocalStorageStub; + + beforeEach(function() { + config.resetConfig(); + getDataFromLocalStorageStub = sinon.stub(storage, 'getDataFromLocalStorage'); + }); + + afterEach(function () { + getDataFromLocalStorageStub.restore(); + }); + + describe('haloSubmodule', function() { + it('successfully instantiates', function () { + expect(haloSubmodule.init()).to.equal(true); + }); + }); + + describe('Add Real-Time Data', function() { + it('merges ortb2 data', function() { + let rtdConfig = {}; + let bidConfig = {}; + + const setConfigUserObj1 = { + name: 'www.dataprovider1.com', + ext: { taxonomyname: 'iab_audience_taxonomy' }, + segment: [{ + id: '1776' + }] + }; + + const setConfigUserObj2 = { + name: 'www.dataprovider2.com', + ext: { taxonomyname: 'iab_audience_taxonomy' }, + segment: [{ + id: '1914' + }] + }; + + const setConfigSiteObj1 = { + name: 'www.dataprovider3.com', + ext: { + taxonomyname: 'iab_audience_taxonomy' + }, + segment: [ + { + id: '1812' + }, + { + id: '1955' + } + ] + } + + config.setConfig({ + ortb2: { + user: { + data: [setConfigUserObj1, setConfigUserObj2] + }, + site: { + content: { + data: [setConfigSiteObj1] + } + } + } + }); + + const rtdUserObj1 = { + name: 'www.dataprovider4.com', + ext: { + taxonomyname: 'iab_audience_taxonomy' + }, + segment: [ + { + id: '1918' + }, + { + id: '1939' + } + ] + }; + + const rtdSiteObj1 = { + name: 'www.dataprovider5.com', + ext: { + taxonomyname: 'iab_audience_taxonomy' + }, + segment: [ + { + id: '1945' + }, + { + id: '2003' + } + ] + }; + + const rtd = { + ortb2: { + user: { + data: [rtdUserObj1] + }, + site: { + content: { + data: [rtdSiteObj1] + } + } + } + }; + + let pbConfig = config.getConfig(); + addRealTimeData(bidConfig, rtd, rtdConfig); + + let ortb2Config = config.getConfig().ortb2; + + expect(ortb2Config.user.data).to.deep.include.members([setConfigUserObj1, setConfigUserObj2, rtdUserObj1]); + expect(ortb2Config.site.content.data).to.deep.include.members([setConfigSiteObj1, rtdSiteObj1]); + }); + + it('allows publisher defined rtd ortb2 logic', function() { + const rtdConfig = { + params: { + handleRtd: function(bidConfig, rtd, rtdConfig, pbConfig) { + if (rtd.ortb2.user.data[0].segment[0].id == '1776') { + pbConfig.setConfig({ortb2: rtd.ortb2}); + } else { + pbConfig.setConfig({ortb2: {}}); + } + } + } + }; + + let bidConfig = {}; + + const rtdUserObj1 = { + name: 'www.dataprovider.com', + ext: { taxonomyname: 'iab_audience_taxonomy' }, + segment: [{ + id: '1776' + }] + }; + + let rtd = { + ortb2: { + user: { + data: [rtdUserObj1] + } + } + }; + + config.resetConfig(); + + let pbConfig = config.getConfig(); + addRealTimeData(bidConfig, rtd, rtdConfig); + expect(config.getConfig().ortb2.user.data).to.deep.include.members([rtdUserObj1]); + + const rtdUserObj2 = { + name: 'www.audigent.com', + ext: { + segtax: '1', + taxprovider: '1' + }, + segment: [{ + id: 'pubseg1' + }] + }; + + rtd = { + ortb2: { + user: { + data: [rtdUserObj2] + } + } + }; + + config.resetConfig(); + + pbConfig = config.getConfig(); + addRealTimeData(bidConfig, rtd, rtdConfig); + expect(config.getConfig().ortb2).to.deep.equal({}); + }); + + it('allows publisher defined adunit logic', function() { + const rtdConfig = { + params: { + handleRtd: function(bidConfig, rtd, rtdConfig, pbConfig) { + var adUnits = bidConfig.adUnits; + for (var i = 0; i < adUnits.length; i++) { + var adUnit = adUnits[i]; + for (var j = 0; j < adUnit.bids.length; j++) { + var bid = adUnit.bids[j]; + if (bid.bidder == 'adBuzz') { + for (var k = 0; k < rtd.adBuzz.length; k++) { + bid.adBuzzData.segments.adBuzz.push(rtd.adBuzz[k]); + } + } else if (bid.bidder == 'trueBid') { + for (var k = 0; k < rtd.trueBid.length; k++) { + bid.trueBidSegments.push(rtd.trueBid[k]); + } + } + } + } + } + } + }; + + let bidConfig = { + adUnits: [ + { + bids: [ + { + bidder: 'adBuzz', + adBuzzData: { + segments: { + adBuzz: [ + { + id: 'adBuzzSeg1' + } + ] + } + } + }, + { + bidder: 'trueBid', + trueBidSegments: [] + } + ] + } + ] + }; + + const rtd = { + adBuzz: [{id: 'adBuzzSeg2'}, {id: 'adBuzzSeg3'}], + trueBid: [{id: 'truebidSeg1'}, {id: 'truebidSeg2'}, {id: 'truebidSeg3'}] + }; + + addRealTimeData(bidConfig, rtd, rtdConfig); + + expect(bidConfig.adUnits[0].bids[0].adBuzzData.segments.adBuzz[0].id).to.equal('adBuzzSeg1'); + expect(bidConfig.adUnits[0].bids[0].adBuzzData.segments.adBuzz[1].id).to.equal('adBuzzSeg2'); + expect(bidConfig.adUnits[0].bids[0].adBuzzData.segments.adBuzz[2].id).to.equal('adBuzzSeg3'); + expect(bidConfig.adUnits[0].bids[1].trueBidSegments[0].id).to.equal('truebidSeg1'); + expect(bidConfig.adUnits[0].bids[1].trueBidSegments[1].id).to.equal('truebidSeg2'); + expect(bidConfig.adUnits[0].bids[1].trueBidSegments[2].id).to.equal('truebidSeg3'); + }); + }); + + describe('Get Real-Time Data', function() { + it('gets rtd from local storage cache', function() { + const rtdConfig = { + params: { + segmentCache: true + } + }; + + const bidConfig = {}; + + const rtdUserObj1 = { + name: 'www.dataprovider3.com', + ext: { + taxonomyname: 'iab_audience_taxonomy' + }, + segment: [ + { + id: '1918' + }, + { + id: '1939' + } + ] + }; + + const cachedRtd = { + rtd: { + ortb2: { + user: { + data: [rtdUserObj1] + } + } + } + }; + + getDataFromLocalStorageStub.withArgs(RTD_LOCAL_NAME).returns(JSON.stringify(cachedRtd)); + + expect(config.getConfig().ortb2).to.be.undefined; + getRealTimeData(bidConfig, () => {}, rtdConfig, {}); + expect(config.getConfig().ortb2.user.data).to.deep.include.members([rtdUserObj1]); + }); + + it('gets real-time data via async request', function() { + const setConfigSiteObj1 = { + name: 'www.audigent.com', + ext: { + segtax: '1', + taxprovider: '1' + }, + segment: [ + { + id: 'pubseg1' + }, + { + id: 'pubseg2' + } + ] + } + + config.setConfig({ + ortb2: { + site: { + content: { + data: [setConfigSiteObj1] + } + } + } + }); + + const rtdConfig = { + params: { + segmentCache: false, + usePubHalo: true, + requestParams: { + publisherId: 'testPub1' + } + } + }; + + let bidConfig = {}; + + const rtdUserObj1 = { + name: 'www.audigent.com', + ext: { + segtax: '1', + taxprovider: '1' + }, + segment: [ + { + id: 'pubseg1' + }, + { + id: 'pubseg2' + } + ] + }; + + const data = { + rtd: { + ortb2: { + user: { + data: [rtdUserObj1] + } + } + } + }; + + getDataFromLocalStorageStub.withArgs(HALOID_LOCAL_NAME).returns('testHaloId1'); + getRealTimeData(bidConfig, () => {}, rtdConfig, {}); + + let request = server.requests[0]; + let postData = JSON.parse(request.requestBody); + expect(postData.config).to.have.deep.property('publisherId', 'testPub1'); + expect(postData.userIds).to.have.deep.property('haloId', 'testHaloId1'); + + request.respond(200, responseHeader, JSON.stringify(data)); + + expect(config.getConfig().ortb2.user.data).to.deep.include.members([rtdUserObj1]); + }); + }); +}); diff --git a/test/spec/modules/haxmediaBidAdapter_spec.js b/test/spec/modules/haxmediaBidAdapter_spec.js new file mode 100644 index 00000000000..2e39d771bdf --- /dev/null +++ b/test/spec/modules/haxmediaBidAdapter_spec.js @@ -0,0 +1,304 @@ +import {expect} from 'chai'; +import {spec} from '../../../modules/haxmediaBidAdapter.js'; +import { BANNER, VIDEO, NATIVE } from '../../../src/mediaTypes.js'; + +describe('haxmediaBidAdapter', function () { + const bid = { + bidId: '23fhj33i987f', + bidder: 'haxmedia', + 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://balancer.haxmedia.io/?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; + }); + }); +}); diff --git a/test/spec/modules/hybridBidAdapter_spec.js b/test/spec/modules/hybridBidAdapter_spec.js index e5ad878c9b1..d1899ef3d81 100644 --- a/test/spec/modules/hybridBidAdapter_spec.js +++ b/test/spec/modules/hybridBidAdapter_spec.js @@ -25,16 +25,22 @@ describe('Hybrid.ai Adapter', function() { placeId: PLACE_ID, placement: 'video' } + const inImageMandatoryParams = { + placeId: PLACE_ID, + placement: 'inImage', + imageUrl: 'https://hybrid.ai/images/image.jpg' + } const validBidRequests = [ getSlotConfigs({ banner: {} }, bannerMandatoryParams), - getSlotConfigs({ video: {playerSize: [[640, 480]]} }, videoMandatoryParams) + 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 placement has the correct value', function() { + describe('and banner placement has the correct value', function() { const slotConfig = getSlotConfigs( - { banner: {} }, + {banner: {}}, { placeId: PLACE_ID, placement: 'banner' @@ -43,6 +49,22 @@ describe('Hybrid.ai Adapter', function() { 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]] + } + }, + { + placeId: 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( @@ -84,6 +106,15 @@ describe('Hybrid.ai Adapter', function() { ) expect(isBidRequestValid).to.equal(false) }) + it('does not have the imageUrl.', function() { + const isBidRequestValid = spec.isBidRequestValid( + createSlotconfig({ + placeId: PLACE_ID, + placement: 'inImage' + }) + ) + expect(isBidRequestValid).to.equal(false) + }) it('does not have a the correct placement.', function() { const isBidRequestValid = spec.isBidRequestValid( createSlotconfig({ @@ -240,6 +271,36 @@ describe('Hybrid.ai Adapter', function() { 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: 'html', + inImage: { + actionUrls: {} + }, + 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() { @@ -253,7 +314,8 @@ describe('Hybrid.ai Adapter', function() { currency: 'USD', content: 'html', width: 100, - height: 100 + height: 100, + transactionId: '31a58515-3634-4e90-9c96-f86196db1459' } ] } diff --git a/test/spec/modules/id5IdSystem_spec.js b/test/spec/modules/id5IdSystem_spec.js index 05ecec8dc36..9ba9aee9c63 100644 --- a/test/spec/modules/id5IdSystem_spec.js +++ b/test/spec/modules/id5IdSystem_spec.js @@ -1,9 +1,21 @@ +import { + id5IdSubmodule, + ID5_STORAGE_NAME, + ID5_PRIVACY_STORAGE_NAME, + getFromLocalStorage, + storeInLocalStorage, + expDaysStr, + nbCacheName, + getNbFromCache, + storeNbInCache, + isInControlGroup +} from 'modules/id5IdSystem.js'; import { init, requestBidsHook, setSubmoduleRegistry, coreStorage } from 'modules/userId/index.js'; import { config } from 'src/config.js'; -import { id5IdSubmodule } from 'modules/id5IdSystem.js'; import { server } from 'test/mocks/xhr.js'; import events from 'src/events.js'; import CONSTANTS from 'src/constants.json'; +import * as utils from 'src/utils.js'; let expect = require('chai').expect; @@ -11,33 +23,31 @@ describe('ID5 ID System', function() { const ID5_MODULE_NAME = 'id5Id'; const ID5_EIDS_NAME = ID5_MODULE_NAME.toLowerCase(); const ID5_SOURCE = 'id5-sync.com'; - const ID5_PARTNER = 173; - const ID5_ENDPOINT = `https://id5-sync.com/g/v2/${ID5_PARTNER}.json`; - const ID5_COOKIE_NAME = 'id5idcookie'; - const ID5_NB_COOKIE_NAME = `id5id.1st_${ID5_PARTNER}_nb`; - const ID5_EXPIRED_COOKIE_DATE = 'Thu, 01 Jan 1970 00:00:01 GMT'; + const ID5_TEST_PARTNER_ID = 173; + const ID5_ENDPOINT = `https://id5-sync.com/g/v2/${ID5_TEST_PARTNER_ID}.json`; + 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_LEGACY_STORED_OBJ = { - 'ID5ID': ID5_STORED_ID - } 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_COOKIE_NAME, storageType = 'cookie') { + function getId5FetchConfig(storageName = ID5_STORAGE_NAME, storageType = 'html5') { return { name: ID5_MODULE_NAME, params: { - partner: ID5_PARTNER + partner: ID5_TEST_PARTNER_ID }, storage: { name: storageName, @@ -50,7 +60,9 @@ describe('ID5 ID System', function() { return { name: ID5_MODULE_NAME, value: { - id5id: value + id5id: { + uid: value + } } } } @@ -63,10 +75,10 @@ describe('ID5 ID System', function() { } } function getFetchCookieConfig() { - return getUserSyncConfig([getId5FetchConfig()]); + return getUserSyncConfig([getId5FetchConfig(ID5_STORAGE_NAME, 'cookie')]); } function getFetchLocalStorageConfig() { - return getUserSyncConfig([getId5FetchConfig(ID5_COOKIE_NAME, 'html5')]); + return getUserSyncConfig([getId5FetchConfig(ID5_STORAGE_NAME, 'html5')]); } function getValueConfig(value) { return getUserSyncConfig([getId5ValueConfig(value)]); @@ -80,6 +92,37 @@ describe('ID5 ID System', function() { }; } + describe('Check for valid publisher config', function() { + it('should fail with invalid config', function() { + // no config + expect(id5IdSubmodule.getId()).to.be.eq(undefined); + expect(id5IdSubmodule.getId({ })).to.be.eq(undefined); + + // valid params, invalid storage + expect(id5IdSubmodule.getId({ params: { partner: 123 } })).to.be.eq(undefined); + expect(id5IdSubmodule.getId({ params: { partner: 123 }, storage: {} })).to.be.eq(undefined); + expect(id5IdSubmodule.getId({ params: { partner: 123 }, storage: { name: '' } })).to.be.eq(undefined); + expect(id5IdSubmodule.getId({ params: { partner: 123 }, storage: { type: '' } })).to.be.eq(undefined); + + // valid storage, invalid params + expect(id5IdSubmodule.getId({ storage: { name: 'name', type: 'html5', }, })).to.be.eq(undefined); + expect(id5IdSubmodule.getId({ storage: { name: 'name', type: 'html5', }, params: { } })).to.be.eq(undefined); + expect(id5IdSubmodule.getId({ storage: { name: 'name', type: 'html5', }, params: { partner: 'abc' } })).to.be.eq(undefined); + }); + + it('should warn with non-recommended storage params', function() { + let logWarnStub = sinon.stub(utils, 'logWarn'); + + id5IdSubmodule.getId({ storage: { name: 'name', type: 'html5', }, params: { partner: 123 } }); + expect(logWarnStub.calledOnce).to.be.true; + logWarnStub.restore(); + + id5IdSubmodule.getId({ storage: { name: ID5_STORAGE_NAME, type: 'cookie', }, params: { partner: 123 } }); + expect(logWarnStub.calledOnce).to.be.true; + logWarnStub.restore(); + }); + }); + describe('Xhr Requests from getId()', function() { const responseHeader = { 'Content-Type': 'application/json' }; let callbackSpy = sinon.spy(); @@ -91,121 +134,177 @@ describe('ID5 ID System', function() { }); - it('should fail if no partner is provided in the config', function() { - expect(id5IdSubmodule.getId()).to.be.eq(undefined); - }); - - it('should call the ID5 server with 1puid field for legacy storedObj format', function () { - let submoduleCallback = id5IdSubmodule.getId(getId5FetchConfig().params, undefined, ID5_LEGACY_STORED_OBJ).callback; + it('should call the ID5 server and handle a valid response', function () { + let submoduleCallback = id5IdSubmodule.getId(getId5FetchConfig(), undefined, undefined).callback; submoduleCallback(callbackSpy); let request = server.requests[0]; let requestBody = JSON.parse(request.requestBody); expect(request.url).to.contain(ID5_ENDPOINT); expect(request.withCredentials).to.be.true; - expect(requestBody.s).to.eq(''); - expect(requestBody.partner).to.eq(ID5_PARTNER); - expect(requestBody['1puid']).to.eq(ID5_STORED_ID); + expect(requestBody.partner).to.eq(ID5_TEST_PARTNER_ID); + expect(requestBody.o).to.eq('pbjs'); + expect(requestBody.pd).to.be.undefined; + expect(requestBody.s).to.be.undefined; + expect(requestBody.provider).to.be.undefined + expect(requestBody.v).to.eq('$prebid.version$'); + expect(requestBody.gdpr).to.exist; + expect(requestBody.gdpr_consent).to.be.undefined; + expect(requestBody.us_privacy).to.be.undefined; request.respond(200, responseHeader, JSON.stringify(ID5_JSON_RESPONSE)); expect(callbackSpy.calledOnce).to.be.true; expect(callbackSpy.lastCall.lastArg).to.deep.equal(ID5_JSON_RESPONSE); }); - it('should call the ID5 server with signature field for new storedObj format', function () { - let submoduleCallback = id5IdSubmodule.getId(getId5FetchConfig().params, undefined, ID5_STORED_OBJ).callback; + it('should call the ID5 server with no signature field when no stored object', function () { + let submoduleCallback = id5IdSubmodule.getId(getId5FetchConfig(), undefined, undefined).callback; + submoduleCallback(callbackSpy); + + let request = server.requests[0]; + let requestBody = JSON.parse(request.requestBody); + expect(requestBody.s).to.be.undefined; + + request.respond(200, responseHeader, JSON.stringify(ID5_JSON_RESPONSE)); + }); + + it('should call the ID5 server with signature field from stored object', function () { + let submoduleCallback = id5IdSubmodule.getId(getId5FetchConfig(), undefined, ID5_STORED_OBJ).callback; submoduleCallback(callbackSpy); let request = server.requests[0]; let requestBody = JSON.parse(request.requestBody); - expect(request.url).to.contain(ID5_ENDPOINT); - expect(request.withCredentials).to.be.true; expect(requestBody.s).to.eq(ID5_STORED_SIGNATURE); - expect(requestBody.partner).to.eq(ID5_PARTNER); - expect(requestBody['1puid']).to.eq(''); request.respond(200, responseHeader, JSON.stringify(ID5_JSON_RESPONSE)); - expect(callbackSpy.calledOnce).to.be.true; - expect(callbackSpy.lastCall.lastArg).to.deep.equal(ID5_JSON_RESPONSE); }); it('should call the ID5 server with pd field when pd config is set', function () { const pubData = 'b50ca08271795a8e7e4012813f23d505193d75c0f2e2bb99baa63aa822f66ed3'; - let config = getId5FetchConfig().params; - config.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]; let requestBody = JSON.parse(request.requestBody); - expect(request.url).to.contain(ID5_ENDPOINT); - expect(request.withCredentials).to.be.true; - expect(requestBody.s).to.eq(ID5_STORED_SIGNATURE); expect(requestBody.pd).to.eq(pubData); - expect(requestBody['1puid']).to.eq(''); request.respond(200, responseHeader, JSON.stringify(ID5_JSON_RESPONSE)); - expect(callbackSpy.calledOnce).to.be.true; - expect(callbackSpy.lastCall.lastArg).to.deep.equal(ID5_JSON_RESPONSE); }); - it('should call the ID5 server with empty pd field when pd config is not set', function () { - let config = getId5FetchConfig().params; - config.pd = undefined; + it('should call the ID5 server with no pd field when pd config is not set', function () { + 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]; let requestBody = JSON.parse(request.requestBody); - expect(request.url).to.contain(ID5_ENDPOINT); - expect(request.withCredentials).to.be.true; - expect(requestBody.pd).to.eq(''); + expect(requestBody.pd).to.be.undefined; request.respond(200, responseHeader, JSON.stringify(ID5_JSON_RESPONSE)); - expect(callbackSpy.calledOnce).to.be.true; - expect(callbackSpy.lastCall.lastArg).to.deep.equal(ID5_JSON_RESPONSE); }); - it('should call the ID5 server with nb=1 when no stored value exists', function () { - coreStorage.setCookie(ID5_NB_COOKIE_NAME, '', ID5_EXPIRED_COOKIE_DATE); + it('should call the ID5 server with nb=1 when no stored value exists and reset after', function () { + coreStorage.removeDataFromLocalStorage(ID5_NB_STORAGE_NAME); - let submoduleCallback = id5IdSubmodule.getId(getId5FetchConfig().params, undefined, ID5_STORED_OBJ).callback; + let submoduleCallback = id5IdSubmodule.getId(getId5FetchConfig(), undefined, ID5_STORED_OBJ).callback; submoduleCallback(callbackSpy); let request = server.requests[0]; let requestBody = JSON.parse(request.requestBody); - expect(request.url).to.contain(ID5_ENDPOINT); - expect(request.withCredentials).to.be.true; expect(requestBody.nbPage).to.eq(1); request.respond(200, responseHeader, JSON.stringify(ID5_JSON_RESPONSE)); - expect(callbackSpy.calledOnce).to.be.true; - expect(callbackSpy.lastCall.lastArg).to.deep.equal(ID5_JSON_RESPONSE); - expect(coreStorage.getCookie(ID5_NB_COOKIE_NAME)).to.be.eq('0'); + expect(getNbFromCache(ID5_TEST_PARTNER_ID)).to.be.eq(0); }); - it('should call the ID5 server with incremented nb when stored value exists', function () { - let expStr = (new Date(Date.now() + 25000).toUTCString()); - coreStorage.setCookie(ID5_NB_COOKIE_NAME, '1', expStr); + it('should call the ID5 server with incremented nb when stored value exists and reset after', function () { + storeNbInCache(ID5_TEST_PARTNER_ID, 1); - let submoduleCallback = id5IdSubmodule.getId(getId5FetchConfig().params, undefined, ID5_STORED_OBJ).callback; + let submoduleCallback = id5IdSubmodule.getId(getId5FetchConfig(), undefined, ID5_STORED_OBJ).callback; submoduleCallback(callbackSpy); let request = server.requests[0]; let requestBody = JSON.parse(request.requestBody); - expect(request.url).to.contain(ID5_ENDPOINT); - expect(request.withCredentials).to.be.true; expect(requestBody.nbPage).to.eq(2); request.respond(200, responseHeader, JSON.stringify(ID5_JSON_RESPONSE)); - expect(callbackSpy.calledOnce).to.be.true; - expect(callbackSpy.lastCall.lastArg).to.deep.equal(ID5_JSON_RESPONSE); - expect(coreStorage.getCookie(ID5_NB_COOKIE_NAME)).to.be.eq('0'); + 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; }); }); @@ -214,34 +313,39 @@ describe('ID5 ID System', function() { beforeEach(function() { sinon.stub(events, 'getEvents').returns([]); - coreStorage.setCookie(ID5_COOKIE_NAME, '', ID5_EXPIRED_COOKIE_DATE); - coreStorage.setCookie(`${ID5_COOKIE_NAME}_last`, '', ID5_EXPIRED_COOKIE_DATE); - coreStorage.setCookie(ID5_NB_COOKIE_NAME, '', ID5_EXPIRED_COOKIE_DATE); + coreStorage.removeDataFromLocalStorage(ID5_STORAGE_NAME); + coreStorage.removeDataFromLocalStorage(`${ID5_STORAGE_NAME}_last`); + coreStorage.removeDataFromLocalStorage(ID5_NB_STORAGE_NAME); adUnits = [getAdUnitMock()]; }); afterEach(function() { events.getEvents.restore(); - coreStorage.setCookie(ID5_COOKIE_NAME, '', ID5_EXPIRED_COOKIE_DATE); - coreStorage.setCookie(`${ID5_COOKIE_NAME}_last`, '', ID5_EXPIRED_COOKIE_DATE); - coreStorage.setCookie(ID5_NB_COOKIE_NAME, '', ID5_EXPIRED_COOKIE_DATE); + coreStorage.removeDataFromLocalStorage(ID5_STORAGE_NAME); + coreStorage.removeDataFromLocalStorage(`${ID5_STORAGE_NAME}_last`); + coreStorage.removeDataFromLocalStorage(ID5_NB_STORAGE_NAME); }); - it('should add stored ID from cookie to bids', function (done) { - let expStr = (new Date(Date.now() + 25000).toUTCString()); - coreStorage.setCookie(ID5_COOKIE_NAME, JSON.stringify(ID5_STORED_OBJ), expStr); + it('should add stored ID from cache to bids', function (done) { + storeInLocalStorage(ID5_STORAGE_NAME, JSON.stringify(ID5_STORED_OBJ), 1); setSubmoduleRegistry([id5IdSubmodule]); init(config); - config.setConfig(getFetchCookieConfig()); + config.setConfig(getFetchLocalStorageConfig()); requestBidsHook(function () { adUnits.forEach(unit => { unit.bids.forEach(bid => { expect(bid).to.have.deep.nested.property(`userId.${ID5_EIDS_NAME}`); - expect(bid.userId.id5id).to.equal(ID5_STORED_ID); + 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 }] + uids: [{ + id: ID5_STORED_ID, + atype: 1, + ext: { + linkType: ID5_STORED_LINK_TYPE + } + }] }); }); }); @@ -258,7 +362,7 @@ describe('ID5 ID System', function() { adUnits.forEach(unit => { unit.bids.forEach(bid => { expect(bid).to.have.deep.nested.property(`userId.${ID5_EIDS_NAME}`); - expect(bid.userId.id5id).to.equal(ID5_STORED_ID); + 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 }] @@ -269,43 +373,40 @@ describe('ID5 ID System', function() { }, { adUnits }); }); - it('should set nb=1 in cookie when no stored value exists', function () { - let expStr = (new Date(Date.now() + 25000).toUTCString()); - coreStorage.setCookie(ID5_COOKIE_NAME, JSON.stringify(ID5_STORED_OBJ), expStr); - coreStorage.setCookie(ID5_NB_COOKIE_NAME, '', ID5_EXPIRED_COOKIE_DATE); + it('should set nb=1 in cache when no stored nb value exists and cached ID', function () { + storeInLocalStorage(ID5_STORAGE_NAME, JSON.stringify(ID5_STORED_OBJ), 1); + coreStorage.removeDataFromLocalStorage(ID5_NB_STORAGE_NAME); setSubmoduleRegistry([id5IdSubmodule]); init(config); - config.setConfig(getFetchCookieConfig()); + config.setConfig(getFetchLocalStorageConfig()); let innerAdUnits; - requestBidsHook((config) => { innerAdUnits = config.adUnits }, {adUnits}); + requestBidsHook((adUnitConfig) => { innerAdUnits = adUnitConfig.adUnits }, {adUnits}); - expect(coreStorage.getCookie(ID5_NB_COOKIE_NAME)).to.be.eq('1'); + expect(getNbFromCache(ID5_TEST_PARTNER_ID)).to.be.eq(1); }); - it('should increment nb in cookie when stored value exists', function () { - let expStr = (new Date(Date.now() + 25000).toUTCString()); - coreStorage.setCookie(ID5_COOKIE_NAME, JSON.stringify(ID5_STORED_OBJ), expStr); - coreStorage.setCookie(ID5_NB_COOKIE_NAME, '1', expStr); + it('should increment nb in cache when stored nb value exists and cached ID', function () { + storeInLocalStorage(ID5_STORAGE_NAME, JSON.stringify(ID5_STORED_OBJ), 1); + storeNbInCache(ID5_TEST_PARTNER_ID, 1); setSubmoduleRegistry([id5IdSubmodule]); init(config); - config.setConfig(getFetchCookieConfig()); + config.setConfig(getFetchLocalStorageConfig()); let innerAdUnits; - requestBidsHook((config) => { innerAdUnits = config.adUnits }, {adUnits}); + requestBidsHook((adUnitConfig) => { innerAdUnits = adUnitConfig.adUnits }, {adUnits}); - expect(coreStorage.getCookie(ID5_NB_COOKIE_NAME)).to.be.eq('2'); + expect(getNbFromCache(ID5_TEST_PARTNER_ID)).to.be.eq(2); }); it('should call ID5 servers with signature and incremented nb post auction if refresh needed', function () { - let expStr = (new Date(Date.now() + 25000).toUTCString()); - coreStorage.setCookie(ID5_COOKIE_NAME, JSON.stringify(ID5_STORED_OBJ), expStr); - coreStorage.setCookie(`${ID5_COOKIE_NAME}_last`, (new Date(Date.now() - 50000).toUTCString()), expStr); - coreStorage.setCookie(ID5_NB_COOKIE_NAME, '1', expStr); + storeInLocalStorage(ID5_STORAGE_NAME, JSON.stringify(ID5_STORED_OBJ), 1); + storeInLocalStorage(`${ID5_STORAGE_NAME}_last`, expDaysStr(-1), 1); + storeNbInCache(ID5_TEST_PARTNER_ID, 1); - let id5Config = getFetchCookieConfig(); + let id5Config = getFetchLocalStorageConfig(); id5Config.userSync.userIds[0].storage.refreshInSeconds = 2; setSubmoduleRegistry([id5IdSubmodule]); @@ -313,9 +414,9 @@ describe('ID5 ID System', function() { config.setConfig(id5Config); let innerAdUnits; - requestBidsHook((config) => { innerAdUnits = config.adUnits }, {adUnits}); + requestBidsHook((adUnitConfig) => { innerAdUnits = adUnitConfig.adUnits }, {adUnits}); - expect(coreStorage.getCookie(ID5_NB_COOKIE_NAME)).to.be.eq('2'); + expect(getNbFromCache(ID5_TEST_PARTNER_ID)).to.be.eq(2); expect(server.requests).to.be.empty; events.emit(CONSTANTS.EVENTS.AUCTION_END, {}); @@ -329,55 +430,203 @@ describe('ID5 ID System', function() { const responseHeader = { 'Content-Type': 'application/json' }; request.respond(200, responseHeader, JSON.stringify(ID5_JSON_RESPONSE)); - expect(coreStorage.getCookie(ID5_COOKIE_NAME)).to.be.eq(JSON.stringify(ID5_JSON_RESPONSE)); - expect(coreStorage.getCookie(ID5_NB_COOKIE_NAME)).to.be.eq('0'); + expect(decodeURIComponent(getFromLocalStorage(ID5_STORAGE_NAME))).to.be.eq(JSON.stringify(ID5_JSON_RESPONSE)); + expect(getNbFromCache(ID5_TEST_PARTNER_ID)).to.be.eq(0); }); + }); - it('should call ID5 servers with 1puid and nb=1 post auction if refresh needed for legacy stored object', function () { - let expStr = (new Date(Date.now() + 25000).toUTCString()); - coreStorage.setCookie(ID5_COOKIE_NAME, JSON.stringify(ID5_LEGACY_STORED_OBJ), expStr); - coreStorage.setCookie(`${ID5_COOKIE_NAME}_last`, (new Date(Date.now() - 50000).toUTCString()), expStr); + describe('Decode stored object', function() { + const expectedDecodedObject = { id5id: { uid: ID5_STORED_ID, ext: { linkType: ID5_STORED_LINK_TYPE } } }; - let id5Config = getFetchCookieConfig(); - id5Config.userSync.userIds[0].storage.refreshInSeconds = 2; + it('should properly decode from a stored object', function() { + expect(id5IdSubmodule.decode(ID5_STORED_OBJ, getId5FetchConfig())).to.deep.equal(expectedDecodedObject); + }); + it('should return undefined if passed a string', function() { + expect(id5IdSubmodule.decode('somestring', getId5FetchConfig())).to.eq(undefined); + }); + }); - setSubmoduleRegistry([id5IdSubmodule]); - init(config); - config.setConfig(id5Config); + 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: '', ext: { linkType: 0, abTestingControlGroup: true } } }; + let testConfig; - let innerAdUnits; - requestBidsHook((config) => { innerAdUnits = config.adUnits }, {adUnits}); + beforeEach(function() { + testConfig = getId5FetchConfig(); + }); - expect(coreStorage.getCookie(ID5_NB_COOKIE_NAME)).to.be.eq('1'); + 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); + }); + }); + + // 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); + }); + }); + }); - expect(server.requests).to.be.empty; - events.emit(CONSTANTS.EVENTS.AUCTION_END, {}); + describe('A/B Testing Config is not Set', function() { + let randStub; - let request = server.requests[0]; - let requestBody = JSON.parse(request.requestBody); - expect(request.url).to.contain(ID5_ENDPOINT); - expect(requestBody['1puid']).to.eq(ID5_STORED_ID); - expect(requestBody.nbPage).to.eq(1); + 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); + }); + }); - const responseHeader = { 'Content-Type': 'application/json' }; - request.respond(200, responseHeader, JSON.stringify(ID5_JSON_RESPONSE)); + describe('A/B Testing Config is Set', function() { + let randStub; - expect(coreStorage.getCookie(ID5_COOKIE_NAME)).to.be.eq(JSON.stringify(ID5_JSON_RESPONSE)); - expect(coreStorage.getCookie(ID5_NB_COOKIE_NAME)).to.be.eq('0'); - }); - }); + 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; + }); - describe('Decode stored object', function() { - const decodedObject = { 'id5id': ID5_STORED_ID }; + 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('should properly decode from a stored object', function() { - expect(id5IdSubmodule.decode(ID5_STORED_OBJ)).to.deep.equal(decodedObject); - }); - it('should properly decode from a legacy stored object', function() { - expect(id5IdSubmodule.decode(ID5_LEGACY_STORED_OBJ)).to.deep.equal(decodedObject); - }); - it('should return undefined if passed a string', function() { - expect(id5IdSubmodule.decode('somestring')).to.eq(undefined); + 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/idImportLibrary_spec.js b/test/spec/modules/idImportLibrary_spec.js new file mode 100644 index 00000000000..699c2c43a94 --- /dev/null +++ b/test/spec/modules/idImportLibrary_spec.js @@ -0,0 +1,61 @@ +import * as utils from 'src/utils.js'; +import * as idImportlibrary from 'modules/idImportLibrary.js'; + +var expect = require('chai').expect; + +describe('currency', function () { + let fakeCurrencyFileServer; + let sandbox; + let clock; + + let fn = sinon.spy(); + + beforeEach(function () { + fakeCurrencyFileServer = sinon.fakeServer.create(); + sinon.stub(utils, 'logInfo'); + sinon.stub(utils, 'logError'); + }); + + afterEach(function () { + utils.logInfo.restore(); + utils.logError.restore(); + fakeCurrencyFileServer.restore(); + idImportlibrary.setConfig({}); + }); + + describe('setConfig', function () { + beforeEach(function() { + sandbox = sinon.sandbox.create(); + clock = sinon.useFakeTimers(1046952000000); // 2003-03-06T12:00:00Z + }); + + afterEach(function () { + sandbox.restore(); + clock.restore(); + }); + + it('results when no config available', function () { + idImportlibrary.setConfig({}); + sinon.assert.called(utils.logError); + }); + it('results with config available', function () { + idImportlibrary.setConfig({ 'url': 'URL' }); + sinon.assert.called(utils.logInfo); + }); + it('results with config default debounce ', function () { + let config = { 'url': 'URL' } + idImportlibrary.setConfig(config); + expect(config.debounce).to.be.equal(250); + }); + it('results with config default fullscan ', function () { + let config = { 'url': 'URL' } + idImportlibrary.setConfig(config); + expect(config.fullscan).to.be.equal(false); + }); + it('results with config fullscan ', function () { + let config = { 'url': 'URL', 'fullscan': true } + idImportlibrary.setConfig(config); + expect(config.fullscan).to.be.equal(true); + }); + }); +}); diff --git a/test/spec/modules/identityLinkIdSystem_spec.js b/test/spec/modules/identityLinkIdSystem_spec.js index 0d539d5988c..a31270c86c7 100644 --- a/test/spec/modules/identityLinkIdSystem_spec.js +++ b/test/spec/modules/identityLinkIdSystem_spec.js @@ -1,29 +1,36 @@ import {identityLinkSubmodule} from 'modules/identityLinkIdSystem.js'; import * as utils from 'src/utils.js'; import {server} from 'test/mocks/xhr.js'; +import {getStorageManager} from '../../../src/storageManager.js'; + +export const storage = getStorageManager(); const pid = '14'; -const defaultConfigParams = {pid: pid}; +let defaultConfigParams; const responseHeader = {'Content-Type': 'application/json'} describe('IdentityLinkId tests', function () { let logErrorStub; beforeEach(function () { + defaultConfigParams = { params: {pid: pid} }; logErrorStub = sinon.stub(utils, 'logError'); + // remove _lr_retry_request cookie before test + storage.setCookie('_lr_retry_request', 'true', 'Thu, 01 Jan 1970 00:00:01 GMT'); }); afterEach(function () { + defaultConfigParams = {}; logErrorStub.restore(); }); it('should log an error if no configParams were passed when getId', function () { - identityLinkSubmodule.getId(); + identityLinkSubmodule.getId({ params: {} }); expect(logErrorStub.calledOnce).to.be.true; }); it('should log an error if pid configParam was not passed when getId', function () { - identityLinkSubmodule.getId({}); + identityLinkSubmodule.getId({ params: {} }); expect(logErrorStub.calledOnce).to.be.true; }); @@ -41,7 +48,22 @@ describe('IdentityLinkId tests', function () { expect(callBackSpy.calledOnce).to.be.true; }); - it('should call the LiveRamp envelope endpoint with consent string', function () { + it('should NOT call the LiveRamp envelope endpoint if gdpr applies but consent string is empty string', function () { + let consentData = { + gdprApplies: true, + consentString: '' + }; + let submoduleCallback = identityLinkSubmodule.getId(defaultConfigParams, consentData); + expect(submoduleCallback).to.be.undefined; + }); + + it('should NOT call the LiveRamp envelope endpoint if gdpr applies but consent string is missing', function () { + let consentData = { gdprApplies: true }; + let submoduleCallback = identityLinkSubmodule.getId(defaultConfigParams, consentData); + expect(submoduleCallback).to.be.undefined; + }); + + it('should call the LiveRamp envelope endpoint with IAB consent string v1', function () { let callBackSpy = sinon.spy(); let consentData = { gdprApplies: true, @@ -59,6 +81,27 @@ describe('IdentityLinkId tests', function () { expect(callBackSpy.calledOnce).to.be.true; }); + it('should call the LiveRamp envelope endpoint with IAB consent string v2', function () { + let callBackSpy = sinon.spy(); + let consentData = { + gdprApplies: true, + consentString: 'CO4VThZO4VTiuADABBENAzCgAP_AAEOAAAAAAwwAgAEABhAAgAgAAA.YAAAAAAAAAA', + vendorData: { + tcfPolicyVersion: 2 + } + }; + let submoduleCallback = identityLinkSubmodule.getId(defaultConfigParams, consentData).callback; + submoduleCallback(callBackSpy); + let request = server.requests[0]; + expect(request.url).to.be.eq('https://api.rlcdn.com/api/identity/envelope?pid=14&ct=4&cv=CO4VThZO4VTiuADABBENAzCgAP_AAEOAAAAAAwwAgAEABhAAgAgAAA.YAAAAAAAAAA'); + request.respond( + 200, + responseHeader, + JSON.stringify({}) + ); + expect(callBackSpy.calledOnce).to.be.true; + }); + it('should not throw Uncaught TypeError when envelope endpoint returns empty response', function () { let callBackSpy = sinon.spy(); let submoduleCallback = identityLinkSubmodule.getId(defaultConfigParams).callback; @@ -88,4 +131,38 @@ describe('IdentityLinkId tests', function () { ); expect(callBackSpy.calledOnce).to.be.true; }); + + it('should not call the LiveRamp envelope endpoint if cookie _lr_retry_request exist', function () { + let now = new Date(); + now.setTime(now.getTime() + 3000); + storage.setCookie('_lr_retry_request', 'true', now.toUTCString()); + let callBackSpy = sinon.spy(); + let submoduleCallback = identityLinkSubmodule.getId(defaultConfigParams).callback; + submoduleCallback(callBackSpy); + let request = server.requests[0]; + expect(request).to.be.eq(undefined); + }); + + it('should call the LiveRamp envelope endpoint if cookie _lr_retry_request does not exist and notUse3P config property was not set', function () { + let callBackSpy = sinon.spy(); + let submoduleCallback = identityLinkSubmodule.getId(defaultConfigParams).callback; + submoduleCallback(callBackSpy); + let request = server.requests[0]; + expect(request.url).to.be.eq('https://api.rlcdn.com/api/identity/envelope?pid=14'); + request.respond( + 200, + responseHeader, + JSON.stringify({}) + ); + expect(callBackSpy.calledOnce).to.be.true; + }); + + it('should not call the LiveRamp envelope endpoint if config property notUse3P is set to true', function () { + defaultConfigParams.params.notUse3P = true; + let callBackSpy = sinon.spy(); + let submoduleCallback = identityLinkSubmodule.getId(defaultConfigParams).callback; + submoduleCallback(callBackSpy); + let request = server.requests[0]; + expect(request).to.be.eq(undefined); + }); }); diff --git a/test/spec/modules/idxIdSystem_spec.js b/test/spec/modules/idxIdSystem_spec.js new file mode 100644 index 00000000000..14cd9a88d13 --- /dev/null +++ b/test/spec/modules/idxIdSystem_spec.js @@ -0,0 +1,117 @@ +import { expect } from 'chai'; +import find from 'core-js-pure/features/array/find.js'; +import { config } from 'src/config.js'; +import { init, requestBidsHook, setSubmoduleRegistry } from 'modules/userId/index.js'; +import { storage, idxIdSubmodule } from 'modules/idxIdSystem.js'; + +const IDX_COOKIE_NAME = '_idx'; +const IDX_DUMMY_VALUE = 'idx value for testing'; +const IDX_COOKIE_STORED = '{ "idx": "' + IDX_DUMMY_VALUE + '" }'; +const ID_COOKIE_OBJECT = { id: IDX_DUMMY_VALUE }; +const IDX_COOKIE_OBJECT = { idx: IDX_DUMMY_VALUE }; + +function getConfigMock() { + return { + userSync: { + syncDelay: 0, + userIds: [{ + name: 'idx' + }] + } + } +} + +function getAdUnitMock(code = 'adUnit-code') { + return { + code, + mediaTypes: {banner: {}, native: {}}, + sizes: [ + [300, 200], + [300, 600] + ], + bids: [{ + bidder: 'sampleBidder', + params: { placementId: 'banner-only-bidder' } + }] + }; +} + +describe('IDx ID System', () => { + let getDataFromLocalStorageStub, localStorageIsEnabledStub; + let getCookieStub, cookiesAreEnabledStub; + + beforeEach(() => { + getDataFromLocalStorageStub = sinon.stub(storage, 'getDataFromLocalStorage'); + localStorageIsEnabledStub = sinon.stub(storage, 'localStorageIsEnabled'); + getCookieStub = sinon.stub(storage, 'getCookie'); + cookiesAreEnabledStub = sinon.stub(storage, 'cookiesAreEnabled'); + }); + + afterEach(() => { + getDataFromLocalStorageStub.restore(); + localStorageIsEnabledStub.restore(); + getCookieStub.restore(); + cookiesAreEnabledStub.restore(); + }); + + describe('IDx: test "getId" method', () => { + it('provides the stored IDx if a cookie exists', () => { + getCookieStub.withArgs(IDX_COOKIE_NAME).returns(IDX_COOKIE_STORED); + let idx = idxIdSubmodule.getId(); + expect(idx).to.deep.equal(ID_COOKIE_OBJECT); + }); + + it('provides the stored IDx if cookie is absent but present in local storage', () => { + getDataFromLocalStorageStub.withArgs(IDX_COOKIE_NAME).returns(IDX_COOKIE_STORED); + let idx = idxIdSubmodule.getId(); + expect(idx).to.deep.equal(ID_COOKIE_OBJECT); + }); + + it('returns undefined if both cookie and local storage are empty', () => { + let idx = idxIdSubmodule.getId(); + expect(idx).to.be.undefined; + }) + }); + + describe('IDx: test "decode" method', () => { + it('provides the IDx from a stored object', () => { + expect(idxIdSubmodule.decode(ID_COOKIE_OBJECT)).to.deep.equal(IDX_COOKIE_OBJECT); + }); + + it('provides the IDx from a stored string', () => { + expect(idxIdSubmodule.decode(IDX_DUMMY_VALUE)).to.deep.equal(IDX_COOKIE_OBJECT); + }); + }); + + describe('requestBids hook', () => { + let adUnits; + + beforeEach(() => { + adUnits = [getAdUnitMock()]; + setSubmoduleRegistry([idxIdSubmodule]); + init(config); + config.setConfig(getConfigMock()); + getCookieStub.withArgs(IDX_COOKIE_NAME).returns(IDX_COOKIE_STORED); + }); + + it('when a stored IDx exists it is added to bids', (done) => { + requestBidsHook(() => { + adUnits.forEach(unit => { + unit.bids.forEach(bid => { + expect(bid).to.have.deep.nested.property('userId.idx'); + expect(bid.userId.idx).to.equal(IDX_DUMMY_VALUE); + const idxIdAsEid = find(bid.userIdAsEids, e => e.source == 'idx.lat'); + expect(idxIdAsEid).to.deep.equal({ + source: 'idx.lat', + uids: [{ + id: IDX_DUMMY_VALUE, + atype: 1, + }] + }); + }); + }); + done(); + }, { adUnits }); + }); + }); +}); diff --git a/test/spec/modules/impactifyBidAdapter_spec.js b/test/spec/modules/impactifyBidAdapter_spec.js new file mode 100644 index 00000000000..d14cea8cad3 --- /dev/null +++ b/test/spec/modules/impactifyBidAdapter_spec.js @@ -0,0 +1,398 @@ +import { expect } from 'chai'; +import { spec } from 'modules/impactifyBidAdapter.js'; +import * as utils from 'src/utils.js'; + +const BIDDER_CODE = 'impactify'; +const BIDDER_ALIAS = ['imp']; +const DEFAULT_CURRENCY = 'USD'; +const DEFAULT_VIDEO_WIDTH = 640; +const DEFAULT_VIDEO_HEIGHT = 480; +const ORIGIN = 'https://sonic.impactify.media'; +const LOGGER_URI = 'https://logger.impactify.media'; +const AUCTIONURI = '/bidder'; +const COOKIESYNCURI = '/static/cookie_sync.html'; +const GVLID = 606; + +var gdprData = { + 'consentString': 'BOh7mtYOh7mtYAcABBENCU-AAAAncgPIXJiiAoao0PxBFkgCAC8ACIAAQAQQAAIAAAIAAAhBGAAAQAQAEQgAAAAAAABAAAAAAAAAAAAAAACAAAAAAAACgAAAAABAAAAQAAAAAAA', + 'gdprApplies': true +}; + +describe('ImpactifyAdapter', function () { + describe('isBidRequestValid', function () { + let validBid = { + bidder: 'impactify', + params: { + appId: '1', + format: 'screen', + style: 'inline' + } + }; + + it('should return true when required params found', function () { + expect(spec.isBidRequestValid(validBid)).to.equal(true); + }); + + it('should return false when required params are not passed', function () { + let bid = Object.assign({}, validBid); + delete bid.params; + bid.params = {}; + expect(spec.isBidRequestValid(bid)).to.equal(false); + }); + + it('should return false when appId is missing', () => { + const bid = utils.deepClone(validBid); + delete bid.params.appId; + + expect(spec.isBidRequestValid(bid)).to.equal(false); + }); + + it('should return false when appId is not a string', () => { + const bid = utils.deepClone(validBid); + + bid.params.appId = 123; + expect(spec.isBidRequestValid(bid)).to.equal(false); + + bid.params.appId = false; + expect(spec.isBidRequestValid(bid)).to.equal(false); + + bid.params.appId = void (0); + expect(spec.isBidRequestValid(bid)).to.equal(false); + + bid.params.appId = {}; + expect(spec.isBidRequestValid(bid)).to.equal(false); + }); + + it('should return false when format is missing', () => { + const bid = utils.deepClone(validBid); + delete bid.params.format; + + expect(spec.isBidRequestValid(bid)).to.equal(false); + }); + + it('should return false when format is not a string', () => { + const bid = utils.deepClone(validBid); + + bid.params.format = 123; + expect(spec.isBidRequestValid(bid)).to.equal(false); + + bid.params.format = false; + expect(spec.isBidRequestValid(bid)).to.equal(false); + + bid.params.format = void (0); + expect(spec.isBidRequestValid(bid)).to.equal(false); + + bid.params.format = {}; + expect(spec.isBidRequestValid(bid)).to.equal(false); + }); + + it('should return false when format is not equals to screen or display', () => { + const bid = utils.deepClone(validBid); + if (bid.params.format != 'screen' && bid.params.format != 'display') { + expect(spec.isBidRequestValid(bid)).to.equal(false); + } + }); + + it('should return false when style is missing', () => { + const bid = utils.deepClone(validBid); + delete bid.params.style; + + expect(spec.isBidRequestValid(bid)).to.equal(false); + }); + + it('should return false when style is not a string', () => { + const bid = utils.deepClone(validBid); + + bid.params.style = 123; + expect(spec.isBidRequestValid(bid)).to.equal(false); + + bid.params.style = false; + expect(spec.isBidRequestValid(bid)).to.equal(false); + + bid.params.style = void (0); + expect(spec.isBidRequestValid(bid)).to.equal(false); + + bid.params.style = {}; + expect(spec.isBidRequestValid(bid)).to.equal(false); + }); + }); + describe('buildRequests', function () { + let videoBidRequests = [ + { + bidder: 'impactify', + params: { + appId: '1', + format: 'screen', + style: 'inline' + }, + mediaTypes: { + video: { + context: 'instream' + } + }, + adUnitCode: 'adunit-code', + sizes: [[DEFAULT_VIDEO_WIDTH, DEFAULT_VIDEO_HEIGHT]], + bidId: '123456789', + bidderRequestId: '987654321', + auctionId: '19ab94a9-b0d7-4ed7-9f80-ad0c033cf1b1', + transactionId: 'f7b2c372-7a7b-11eb-9439-0242ac130002' + } + ]; + let videoBidderRequest = { + bidderRequestId: '98845765110', + auctionId: '165410516454', + bidderCode: 'impactify', + bids: [ + { + ...videoBidRequests[0] + } + ], + refererInfo: { + referer: 'https://impactify.io' + } + }; + + it('sends video bid request to ENDPOINT via POST', function () { + const request = spec.buildRequests(videoBidRequests, videoBidderRequest); + expect(request.url).to.equal(ORIGIN + AUCTIONURI); + expect(request.method).to.equal('POST'); + }); + }); + describe('interpretResponse', function () { + it('should get correct bid response', function () { + let response = { + id: '19ab94a9-b0d7-4ed7-9f80-ad0c033cf1b1', + seatbid: [ + { + bid: [ + { + id: '65820304700829014', + impid: '462c08f20d428', + price: 3.40, + adm: '', + adid: '97517771', + adomain: [ + '' + ], + iurl: 'https://fra1-ib.adnxs.com/cr?id=97517771', + cid: '9325', + crid: '97517771', + w: 1, + h: 1, + ext: { + prebid: { + 'type': 'video' + }, + bidder: { + prebid: { + type: 'video', + video: { + duration: 30, + primary_category: '' + } + }, + bidder: { + appnexus: { + brand_id: 182979, + auction_id: 8657683934873599656, + bidder_id: 2, + bid_ad_type: 1, + creative_info: { + video: { + duration: 30, + mimes: [ + 'video/x-flv', + 'video/mp4', + 'video/webm' + ] + } + } + } + } + } + } + } + ], + seat: 'impactify' + } + ], + cur: DEFAULT_CURRENCY, + ext: { + responsetimemillis: { + impactify: 114 + }, + prebid: { + auctiontimestamp: 1614587024591 + } + } + }; + let bidderRequest = { + bids: [ + { + bidId: '462c08f20d428', + adUnitCode: '/19968336/header-bid-tag-1', + auctionId: '19ab94a9-b0d7-4ed7-9f80-ad0c033cf1b1', + bidder: 'impactify', + sizes: [[DEFAULT_VIDEO_WIDTH, DEFAULT_VIDEO_HEIGHT]], + mediaTypes: { + video: { + context: 'outstream' + } + } + }, + ] + } + let expectedResponse = [ + { + id: '65820304700829014', + requestId: '462c08f20d428', + cpm: 3.40, + currency: DEFAULT_CURRENCY, + netRevenue: true, + ad: '', + width: 1, + height: 1, + hash: 'test', + expiry: 166192938, + ttl: 300, + creativeId: '97517771' + } + ]; + let result = spec.interpretResponse({ body: response }, bidderRequest); + expect(Object.keys(result[0])).to.have.members(Object.keys(expectedResponse[0])); + }); + }); + describe('getUserSyncs', function () { + let videoBidRequests = [ + { + bidder: 'impactify', + params: { + appId: '1', + format: 'screen', + style: 'inline' + }, + mediaTypes: { + video: { + context: 'instream' + } + }, + adUnitCode: 'adunit-code', + sizes: [[DEFAULT_VIDEO_WIDTH, DEFAULT_VIDEO_HEIGHT]], + bidId: '123456789', + bidderRequestId: '987654321', + auctionId: '19ab94a9-b0d7-4ed7-9f80-ad0c033cf1b1', + transactionId: 'f7b2c372-7a7b-11eb-9439-0242ac130002' + } + ]; + let videoBidderRequest = { + bidderRequestId: '98845765110', + auctionId: '165410516454', + bidderCode: 'impactify', + bids: [ + { + ...videoBidRequests[0] + } + ], + refererInfo: { + referer: 'https://impactify.io' + } + }; + let validResponse = { + id: '19ab94a9-b0d7-4ed7-9f80-ad0c033cf1b1', + seatbid: [ + { + bid: [ + { + id: '65820304700829014', + impid: '462c08f20d428', + price: 3.40, + adm: '', + adid: '97517771', + adomain: [ + '' + ], + iurl: 'https://fra1-ib.adnxs.com/cr?id=97517771', + cid: '9325', + crid: '97517771', + w: 1, + h: 1, + ext: { + prebid: { + 'type': 'video' + }, + bidder: { + prebid: { + type: 'video', + video: { + duration: 30, + primary_category: '' + } + }, + bidder: { + appnexus: { + brand_id: 182979, + auction_id: 8657683934873599656, + bidder_id: 2, + bid_ad_type: 1, + creative_info: { + video: { + duration: 30, + mimes: [ + 'video/x-flv', + 'video/mp4', + 'video/webm' + ] + } + } + } + } + } + } + } + ], + seat: 'impactify' + } + ], + cur: DEFAULT_CURRENCY, + ext: { + responsetimemillis: { + impactify: 114 + }, + prebid: { + auctiontimestamp: 1614587024591 + } + } + }; + it('should return empty response if server response is false', function () { + const result = spec.getUserSyncs('bad', false, gdprData); + expect(result).to.be.empty; + }); + it('should return empty response if server response is empty', function () { + const result = spec.getUserSyncs('bad', [], gdprData); + expect(result).to.be.empty; + }); + it('should append the various values if they exist', function() { + const result = spec.getUserSyncs({iframeEnabled: true}, validResponse, gdprData); + expect(result[0].url).to.include('gdpr=1'); + expect(result[0].url).to.include('gdpr_consent=BOh7mtYOh7mtYAcABBENCU-AAAAncgPIXJiiAoao0PxBFkgCAC8ACIAAQAQQAAIAAAIAAAhBGAAAQAQAEQgAAAAAAABAAAAAAAAAAAAAAACAAAAAAAACgAAAAABAAAAQAAAAAAA'); + }); + }); + + describe('On winning bid', function () { + const bid = { + ad: '', + cpm: '2' + }; + const result = spec.onBidWon(bid); + assert.ok(result); + }); + + describe('On bid Time out', function () { + const bid = { + ad: '', + cpm: '2' + }; + const result = spec.onTimeout(bid); + assert.ok(result); + }); +}) diff --git a/test/spec/modules/improvedigitalBidAdapter_spec.js b/test/spec/modules/improvedigitalBidAdapter_spec.js index 5a20944a6ed..f34a75ef8f3 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: { @@ -154,6 +160,8 @@ describe('Improve Digital Adapter Tests', function () { expect(params.bid_request.version).to.equal(`${spec.version}-${idClient.CONSTANTS.CLIENT_VERSION}`); expect(params.bid_request.gdpr).to.not.exist; expect(params.bid_request.us_privacy).to.not.exist; + expect(params.bid_request.schain).to.not.exist; + expect(params.bid_request.user).to.not.exist; expect(params.bid_request.imp).to.deep.equal([ { id: '33e9500b21129f', @@ -278,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', @@ -295,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 () { @@ -345,6 +433,22 @@ describe('Improve Digital Adapter Tests', function () { expect(params.bid_request.schain).to.equal(schain); }); + it('should add eids', function () { + const userId = { id5id: { uid: '1111' } }; + const expectedUserObject = { ext: { eids: [{ + source: 'id5-sync.com', + uids: [{ + atype: 1, + id: '1111' + }] + }]}}; + const bidRequest = Object.assign({}, simpleBidRequest); + bidRequest.userId = userId; + const request = spec.buildRequests([bidRequest], bidderRequestReferrer)[0]; + const params = JSON.parse(decodeURIComponent(request.data.substring(PARAM_PREFIX.length))); + expect(params.bid_request.user).to.deep.equal(expectedUserObject); + }); + it('should return 2 requests', function () { const requests = spec.buildRequests([ simpleBidRequest, @@ -646,7 +750,6 @@ describe('Improve Digital Adapter Tests', function () { const expectedBid = [ { 'ad': '', - 'adId': '33e9500b21129f', 'creativeId': '422031', 'cpm': 1.45888594164456, 'currency': 'USD', @@ -663,7 +766,6 @@ describe('Improve Digital Adapter Tests', function () { expectedBid[0], { 'ad': '', - 'adId': '1234', 'creativeId': '422033', 'cpm': 1.23, 'currency': 'USD', @@ -679,7 +781,6 @@ describe('Improve Digital Adapter Tests', function () { const expectedBidNative = [ { mediaType: 'native', - adId: '33e9500b21129f', creativeId: '422031', cpm: 1.45888594164456, currency: 'USD', @@ -728,7 +829,6 @@ describe('Improve Digital Adapter Tests', function () { const expectedBidInstreamVideo = [ { 'vastXml': '', - 'adId': '33e9500b21129f', 'creativeId': '422031', 'cpm': 1.45888594164456, 'currency': 'USD', diff --git a/test/spec/modules/inmarBidAdapter_spec.js b/test/spec/modules/inmarBidAdapter_spec.js new file mode 100644 index 00000000000..998fe20d369 --- /dev/null +++ b/test/spec/modules/inmarBidAdapter_spec.js @@ -0,0 +1,240 @@ +// import or require modules necessary for the test, e.g.: +import {expect} from 'chai'; // may prefer 'assert' in place of 'expect' +import { + spec +} from 'modules/inmarBidAdapter.js'; +import {config} from 'src/config.js'; + +describe('Inmar adapter tests', function () { + var DEFAULT_PARAMS_NEW_SIZES = [{ + adUnitCode: 'test-div', + bidId: '2c7c8e9c900244', + mediaTypes: { + banner: { + sizes: [ + [300, 250], [300, 600], [728, 90], [970, 250]] + } + }, + bidder: 'inmar', + params: { + partnerId: 12345 + }, + auctionId: '0cb3144c-d084-4686-b0d6-f5dbe917c563', + bidRequestsCount: 1, + bidderRequestId: '1858b7382993ca', + transactionId: '29df2112-348b-4961-8863-1b33684d95e6', + user: {} + }]; + + var DEFAULT_PARAMS_VIDEO = [{ + adUnitCode: 'test-div', + bidId: '2c7c8e9c900244', + mediaTypes: { + video: { + context: 'instream', // or 'outstream' + playerSize: [640, 480], + mimes: ['video/mp4'] + } + }, + bidder: 'inmar', + params: { + partnerId: 12345 + }, + auctionId: '0cb3144c-d084-4686-b0d6-f5dbe917c563', + bidRequestsCount: 1, + bidderRequestId: '1858b7382993ca', + transactionId: '29df2112-348b-4961-8863-1b33684d95e6', + user: {} + }]; + + var DEFAULT_PARAMS_WO_OPTIONAL = [{ + adUnitCode: 'test-div', + bidId: '2c7c8e9c900244', + sizes: [ + [300, 250], + [300, 600], + [728, 90], + [970, 250] + ], + bidder: 'inmar', + params: { + partnerId: 12345, + }, + auctionId: '851adee7-d843-48f9-a7e9-9ff00573fcbf', + bidRequestsCount: 1, + bidderRequestId: '1858b7382993ca', + transactionId: '29df2112-348b-4961-8863-1b33684d95e6' + }]; + + var BID_RESPONSE = { + body: { + cpm: 1.50, + ad: '', + meta: { + mediaType: 'banner', + }, + width: 300, + height: 250, + creativeId: '189198063', + netRevenue: true, + currency: 'USD', + ttl: 300, + dealId: 'dealId' + + } + }; + + var BID_RESPONSE_VIDEO = { + body: { + cpm: 1.50, + meta: { + mediaType: 'video', + }, + width: 1, + height: 1, + creativeId: '189198063', + netRevenue: true, + currency: 'USD', + ttl: 300, + vastUrl: 'https://vast.com/vast.xml', + dealId: 'dealId' + } + }; + + it('Verify build request to prebid 3.0 display test', function() { + const request = spec.buildRequests(DEFAULT_PARAMS_NEW_SIZES, { + gdprConsent: { + consentString: 'BOZcQl_ObPFjWAeABAESCD-AAAAjx7_______9______9uz_Ov_v_f__33e8__9v_l_7_-___u_-33d4-_1vf99yfm1-7ftr3tp_87ues2_Xur__59__3z3_NohBgA', + gdprApplies: true + }, + refererInfo: { + referer: 'https://domain.com', + numIframes: 0 + } + }); + + expect(request).to.have.property('method').and.to.equal('POST'); + const requestContent = JSON.parse(request.data); + 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'); + expect(requestContent.bidRequests[0]).to.have.property('bidRequestsCount').and.to.equal(1); + expect(requestContent.bidRequests[0]).to.have.property('bidder').and.to.equal('inmar'); + expect(requestContent.bidRequests[0]).to.have.property('bidderRequestId').and.to.equal('1858b7382993ca'); + expect(requestContent.bidRequests[0]).to.have.property('adUnitCode').and.to.equal('test-div'); + expect(requestContent.refererInfo).to.have.property('referer').and.to.equal('https://domain.com'); + expect(requestContent.bidRequests[0].mediaTypes.banner).to.have.property('sizes'); + expect(requestContent.bidRequests[0].mediaTypes.banner.sizes[0]).to.have.ordered.members([300, 250]); + expect(requestContent.bidRequests[0].mediaTypes.banner.sizes[1]).to.have.ordered.members([300, 600]); + expect(requestContent.bidRequests[0].mediaTypes.banner.sizes[2]).to.have.ordered.members([728, 90]); + expect(requestContent.bidRequests[0].mediaTypes.banner.sizes[3]).to.have.ordered.members([970, 250]); + expect(requestContent.bidRequests[0]).to.have.property('transactionId').and.to.equal('29df2112-348b-4961-8863-1b33684d95e6'); + expect(requestContent.refererInfo).to.have.property('numIframes').and.to.equal(0); + }) + + it('Verify interprete response', function () { + const request = spec.buildRequests(DEFAULT_PARAMS_NEW_SIZES, { + gdprConsent: { + consentString: 'BOZcQl_ObPFjWAeABAESCD-AAAAjx7_______9______9uz_Ov_v_f__33e8__9v_l_7_-___u_-33d4-_1vf99yfm1-7ftr3tp_87ues2_Xur__59__3z3_NohBgA', + gdprApplies: true + }, + refererInfo: { + referer: 'https://domain.com', + numIframes: 0 + } + }); + + const bids = spec.interpretResponse(BID_RESPONSE, request); + expect(bids).to.have.lengthOf(1); + const bid = bids[0]; + expect(bid.cpm).to.equal(1.50); + expect(bid.ad).to.equal(''); + expect(bid.meta.mediaType).to.equal('banner'); + expect(bid.width).to.equal(300); + expect(bid.height).to.equal(250); + expect(bid.creativeId).to.equal('189198063'); + expect(bid.netRevenue).to.equal(true); + expect(bid.currency).to.equal('USD'); + expect(bid.ttl).to.equal(300); + expect(bid.dealId).to.equal('dealId'); + }); + + it('no banner media response', function () { + const request = spec.buildRequests(DEFAULT_PARAMS_NEW_SIZES, { + gdprConsent: { + consentString: 'BOZcQl_ObPFjWAeABAESCD-AAAAjx7_______9______9uz_Ov_v_f__33e8__9v_l_7_-___u_-33d4-_1vf99yfm1-7ftr3tp_87ues2_Xur__59__3z3_NohBgA', + gdprApplies: true + }, + refererInfo: { + referer: 'https://domain.com', + numIframes: 0 + } + }); + + const bids = spec.interpretResponse(BID_RESPONSE_VIDEO, request); + const bid = bids[0]; + expect(bid.vastUrl).to.equal('https://vast.com/vast.xml'); + }); + + it('Verifies bidder_code', function () { + expect(spec.code).to.equal('inmar'); + }); + + it('Verifies bidder aliases', function () { + expect(spec.aliases).to.have.lengthOf(1); + expect(spec.aliases[0]).to.equal('inm'); + }); + + it('Verifies if bid request is valid', function () { + expect(spec.isBidRequestValid(DEFAULT_PARAMS_NEW_SIZES[0])).to.equal(true); + expect(spec.isBidRequestValid(DEFAULT_PARAMS_WO_OPTIONAL[0])).to.equal(true); + expect(spec.isBidRequestValid({})).to.equal(false); + expect(spec.isBidRequestValid({ + params: {} + })).to.equal(false); + expect(spec.isBidRequestValid({ + params: { + } + })).to.equal(false); + expect(spec.isBidRequestValid({ + params: { + partnerId: 12345 + } + })).to.equal(true); + }); + + it('Verifies user syncs image', function () { + var syncs = spec.getUserSyncs({ + iframeEnabled: false, + pixelEnabled: true + }, [BID_RESPONSE], { + consentString: 'BOZcQl_ObPFjWAeABAESCD-AAAAjx7_______9______9uz_Ov_v_f__33e8__9v_l_7_-___u_-33d4-_1vf99yfm1-7ftr3tp_87ues2_Xur__59__3z3_NohBgA', + referer: 'http://domain.com', + gdprApplies: true + }) + expect(syncs).to.have.lengthOf(1); + expect(syncs[0].type).to.equal('image'); + + syncs = spec.getUserSyncs({ + iframeEnabled: false, + pixelEnabled: true + }, [BID_RESPONSE], { + consentString: '', + referer: 'http://domain.com', + gdprApplies: true + }) + expect(syncs).to.have.lengthOf(1); + expect(syncs[0].type).to.equal('image'); + + syncs = spec.getUserSyncs({ + iframeEnabled: false, + pixelEnabled: true + }, [], { + consentString: null, + referer: 'http://domain.com', + gdprApplies: true + }) + expect(syncs).to.have.lengthOf(1); + expect(syncs[0].type).to.equal('image'); + }); +}); 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/instreamTracking_spec.js b/test/spec/modules/instreamTracking_spec.js new file mode 100644 index 00000000000..8d795fec88b --- /dev/null +++ b/test/spec/modules/instreamTracking_spec.js @@ -0,0 +1,221 @@ +import { assert } from 'chai'; +import { trackInstreamDeliveredImpressions } from 'modules/instreamTracking.js'; +import { config } from 'src/config.js'; +import events from 'src/events.js'; +import * as utils from 'src/utils.js'; +import * as sinon from 'sinon'; +import { INSTREAM, OUTSTREAM } from 'src/video.js'; + +const BIDDER_CODE = 'sampleBidder'; +const VIDEO_CACHE_KEY = '4cf395af-8fee-4960-af0e-88d44e399f14'; + +let sandbox; + +function enableInstreamTracking(regex) { + let configStub = sandbox.stub(config, 'getConfig'); + configStub.withArgs('instreamTracking').returns(Object.assign( + { + enabled: true, + maxWindow: 10, + pollingFreq: 0 + }, + regex && {urlPattern: regex}, + )); +} + +function mockPerformanceApi({adServerCallSent, videoPresent}) { + let performanceStub = sandbox.stub(window.performance, 'getEntriesByType'); + let entries = [{ + name: 'https://domain.com/img.png', + initiatorType: 'img' + }, { + name: 'https://domain.com/script.js', + initiatorType: 'script' + }, { + name: 'https://domain.com/xhr', + initiatorType: 'xmlhttprequest' + }, { + name: 'https://domain.com/fetch', + initiatorType: 'fetch' + }]; + + if (adServerCallSent || videoPresent) { + entries.push({ + name: 'https://adserver.com/ads?custom_params=hb_uuid%3D' + VIDEO_CACHE_KEY + '%26pos%3D' + VIDEO_CACHE_KEY, + initiatorType: 'xmlhttprequest' + }); + } + + if (videoPresent) { + entries.push({ + name: 'https://prebid-vast-cache.com/cache?key=' + VIDEO_CACHE_KEY, + initiatorType: 'xmlhttprequest' + }); + } + + performanceStub.withArgs('resource').returns(entries); +} + +function mockBidResponse(adUnit, requestId) { + const bid = { + 'adUnitCod': adUnit.code, + 'bidderCode': adUnit.bids[0].bidder, + 'width': adUnit.sizes[0][0], + 'height': adUnit.sizes[0][1], + 'statusMessage': 'Bid available', + 'adId': 'id', + 'requestId': requestId, + 'source': 'client', + 'no_bid': false, + 'cpm': '1.1495', + 'ttl': 180, + 'creativeId': 'id', + 'netRevenue': true, + 'currency': 'USD', + } + if (adUnit.mediaTypes.video) { + bid.videoCacheKey = VIDEO_CACHE_KEY; + } + return bid +} + +function mockBidRequest(adUnit, bidResponse) { + return { + 'bidderCode': bidResponse.bidderCode, + 'auctionId': '20882439e3238c', + 'bidderRequestId': 'bidderRequestId', + 'bids': [ + { + 'adUnitCode': adUnit.code, + 'mediaTypes': adUnit.mediaTypes, + 'bidder': bidResponse.bidderCode, + 'bidId': bidResponse.requestId, + 'sizes': adUnit.sizes, + 'params': adUnit.bids[0].params, + 'bidderRequestId': 'bidderRequestId', + 'auctionId': '20882439e3238c', + } + ], + 'auctionStart': 1505250713622, + 'timeout': 3000 + }; +} + +function getMockInput(mediaType) { + const bannerAdUnit = { + code: 'banner', + mediaTypes: {banner: {sizes: [[300, 250]]}}, + sizes: [[300, 250]], + bids: [{bidder: BIDDER_CODE, params: {placementId: 'id'}}] + }; + const outStreamAdUnit = { + code: 'video-' + OUTSTREAM, + mediaTypes: {video: {playerSize: [640, 480], context: OUTSTREAM}}, + sizes: [[640, 480]], + bids: [{bidder: BIDDER_CODE, params: {placementId: 'id'}}] + }; + const inStreamAdUnit = { + code: 'video-' + INSTREAM, + mediaTypes: {video: {playerSize: [640, 480], context: INSTREAM}}, + sizes: [[640, 480]], + bids: [{bidder: BIDDER_CODE, params: {placementId: 'id'}}] + }; + + let adUnit; + switch (mediaType) { + default: + case 'banner': + adUnit = bannerAdUnit; + break; + case OUTSTREAM: + adUnit = outStreamAdUnit; + break; + case INSTREAM: + adUnit = inStreamAdUnit; + break; + } + + const bidResponse = mockBidResponse(adUnit, utils.getUniqueIdentifierStr()); + const bidderRequest = mockBidRequest(adUnit, bidResponse); + return { + adUnits: [adUnit], + bidsReceived: [bidResponse], + bidderRequests: [bidderRequest], + }; +} + +describe('Instream Tracking', function () { + beforeEach(function () { + sandbox = sinon.sandbox.create(); + }); + + afterEach(function () { + sandbox.restore(); + }); + + describe('gaurd checks', function () { + it('skip if tracking not enable', function () { + sandbox.stub(config, 'getConfig').withArgs('instreamTracking').returns(undefined); + assert.isNotOk(trackInstreamDeliveredImpressions({ + adUnits: [], + bidsReceived: [], + bidderRequests: [] + }), 'should not start tracking when tracking is disabled'); + }); + + it('run only if instream bids are present', function () { + enableInstreamTracking(); + assert.isNotOk(trackInstreamDeliveredImpressions({adUnits: [], bidsReceived: [], bidderRequests: []})); + }); + + it('checks for instream bids', function (done) { + enableInstreamTracking(); + assert.isNotOk(trackInstreamDeliveredImpressions(getMockInput('banner')), 'should not start tracking when banner bids are present') + assert.isNotOk(trackInstreamDeliveredImpressions(getMockInput(OUTSTREAM)), 'should not start tracking when outstream bids are present') + mockPerformanceApi({}); + assert.isOk(trackInstreamDeliveredImpressions(getMockInput(INSTREAM)), 'should start tracking when instream bids are present') + setTimeout(done, 10); + }); + }); + + describe('instream bids check', function () { + let spyEventsOn; + + beforeEach(function () { + spyEventsOn = sandbox.spy(events, 'emit'); + }); + + it('BID WON event is not emitted when no video cache key entries are present', function (done) { + enableInstreamTracking(); + trackInstreamDeliveredImpressions(getMockInput(INSTREAM)); + mockPerformanceApi({}); + setTimeout(function () { + assert.isNotOk(spyEventsOn.calledWith('bidWon')) + done() + }, 10); + }); + + it('BID WON event is not emitted when ad server call is sent', function (done) { + enableInstreamTracking(); + mockPerformanceApi({adServerCallSent: true}); + setTimeout(function () { + assert.isNotOk(spyEventsOn.calledWith('bidWon')) + done() + }, 10); + }); + + it('BID WON event is emitted when video cache key is present', function (done) { + enableInstreamTracking(/cache/); + const bidWonSpy = sandbox.spy(); + events.on('bidWon', bidWonSpy); + mockPerformanceApi({adServerCallSent: true, videoPresent: true}); + + trackInstreamDeliveredImpressions(getMockInput(INSTREAM)); + setTimeout(function () { + assert.isOk(spyEventsOn.calledWith('bidWon')) + assert(bidWonSpy.args[0][0].videoCacheKey, VIDEO_CACHE_KEY, 'Video cache key in bid won should be equal to video cache call'); + done() + }, 10); + }); + }); +}); diff --git a/test/spec/modules/intentIqIdSystem_spec.js b/test/spec/modules/intentIqIdSystem_spec.js new file mode 100644 index 00000000000..7e819195b9f --- /dev/null +++ b/test/spec/modules/intentIqIdSystem_spec.js @@ -0,0 +1,181 @@ +import { expect } from 'chai'; +import {intentIqIdSubmodule, readData, FIRST_PARTY_KEY} from 'modules/intentIqIdSystem.js'; +import * as utils from 'src/utils.js'; +import {server} from 'test/mocks/xhr.js'; + +const partner = 10; +const pai = '11'; +const pcid = '12'; +const defaultConfigParams = { params: {partner: partner} }; +const paiConfigParams = { params: {partner: partner, pai: pai} }; +const pcidConfigParams = { params: {partner: partner, pcid: pcid} }; +const allConfigParams = { params: {partner: partner, pai: pai, pcid: pcid} }; +const responseHeader = {'Content-Type': 'application/json'} + +describe('IntentIQ tests', function () { + let logErrorStub; + + beforeEach(function () { + logErrorStub = sinon.stub(utils, 'logError'); + }); + + afterEach(function () { + logErrorStub.restore(); + }); + + it('should log an error if no configParams were passed when getId', function () { + let submodule = intentIqIdSubmodule.getId({ params: {} }); + expect(logErrorStub.calledOnce).to.be.true; + expect(submodule).to.be.undefined; + }); + + it('should log an error if partner configParam was not passed when getId', function () { + let submodule = intentIqIdSubmodule.getId({ params: {} }); + expect(logErrorStub.calledOnce).to.be.true; + expect(submodule).to.be.undefined; + }); + + it('should log an error if partner configParam was not a numeric value', function () { + let submodule = intentIqIdSubmodule.getId({ params: {partner: '10'} }); + expect(logErrorStub.calledOnce).to.be.true; + expect(submodule).to.be.undefined; + }); + + it('should call the IntentIQ endpoint with only partner', function () { + let callBackSpy = sinon.spy(); + let submoduleCallback = intentIqIdSubmodule.getId(defaultConfigParams).callback; + submoduleCallback(callBackSpy); + let request = server.requests[0]; + expect(request.url).to.contain('https://api.intentiq.com/profiles_engine/ProfilesEngineServlet?at=39&mi=10&dpi=10&pt=17&dpn=1&iiqidtype=2&iiqpcid='); + request.respond( + 200, + responseHeader, + JSON.stringify({}) + ); + expect(callBackSpy.calledOnce).to.be.true; + }); + + it('should ignore NA and invalid responses in decode', function () { + // let resp = JSON.stringify({'RESULT': 'NA'}); + // expect(intentIqIdSubmodule.decode(resp)).to.equal(undefined); + expect(intentIqIdSubmodule.decode('NA')).to.equal(undefined); + expect(intentIqIdSubmodule.decode('')).to.equal(undefined); + expect(intentIqIdSubmodule.decode(undefined)).to.equal(undefined); + }); + + it('should call the IntentIQ endpoint with only partner, pai', function () { + let callBackSpy = sinon.spy(); + let submoduleCallback = intentIqIdSubmodule.getId(paiConfigParams).callback; + submoduleCallback(callBackSpy); + let request = server.requests[0]; + expect(request.url).to.contain('https://api.intentiq.com/profiles_engine/ProfilesEngineServlet?at=39&mi=10&dpi=10&pt=17&dpn=1&pai=11&iiqidtype=2&iiqpcid='); + request.respond( + 200, + responseHeader, + JSON.stringify({}) + ); + expect(callBackSpy.calledOnce).to.be.true; + }); + + it('should call the IntentIQ endpoint with only partner, pcid', function () { + let callBackSpy = sinon.spy(); + let submoduleCallback = intentIqIdSubmodule.getId(pcidConfigParams).callback; + submoduleCallback(callBackSpy); + let request = server.requests[0]; + expect(request.url).to.contain('https://api.intentiq.com/profiles_engine/ProfilesEngineServlet?at=39&mi=10&dpi=10&pt=17&dpn=1&pcid=12&iiqidtype=2&iiqpcid='); + request.respond( + 200, + responseHeader, + JSON.stringify({}) + ); + expect(callBackSpy.calledOnce).to.be.true; + }); + + it('should call the IntentIQ endpoint with partner, pcid, pai', function () { + let callBackSpy = sinon.spy(); + let submoduleCallback = intentIqIdSubmodule.getId(allConfigParams).callback; + submoduleCallback(callBackSpy); + let request = server.requests[0]; + expect(request.url).to.contain('https://api.intentiq.com/profiles_engine/ProfilesEngineServlet?at=39&mi=10&dpi=10&pt=17&dpn=1&pcid=12&pai=11&iiqidtype=2&iiqpcid='); + request.respond( + 200, + responseHeader, + JSON.stringify({}) + ); + expect(callBackSpy.calledOnce).to.be.true; + }); + + it('should not throw Uncaught TypeError when IntentIQ endpoint returns empty response', function () { + let callBackSpy = sinon.spy(); + let submoduleCallback = intentIqIdSubmodule.getId(defaultConfigParams).callback; + submoduleCallback(callBackSpy); + let request = server.requests[0]; + expect(request.url).to.contain('https://api.intentiq.com/profiles_engine/ProfilesEngineServlet?at=39&mi=10&dpi=10&pt=17&dpn=1&iiqidtype=2&iiqpcid='); + request.respond( + 204, + responseHeader, + '' + ); + expect(callBackSpy.calledOnce).to.be.true; + expect(request.response).to.equal(''); + }); + + it('should log an error and continue to callback if ajax request errors', function () { + let callBackSpy = sinon.spy(); + let submoduleCallback = intentIqIdSubmodule.getId(defaultConfigParams).callback; + submoduleCallback(callBackSpy); + let request = server.requests[0]; + expect(request.url).to.contain('https://api.intentiq.com/profiles_engine/ProfilesEngineServlet?at=39&mi=10&dpi=10&pt=17&dpn=1&iiqidtype=2&iiqpcid='); + request.respond( + 503, + responseHeader, + 'Unavailable' + ); + expect(callBackSpy.calledOnce).to.be.true; + }); + + it('should ignore {RESULT: NA} in get id', function () { + let callBackSpy = sinon.spy(); + let submoduleCallback = intentIqIdSubmodule.getId(defaultConfigParams).callback; + submoduleCallback(callBackSpy); + let request = server.requests[0]; + expect(request.url).to.contain('https://api.intentiq.com/profiles_engine/ProfilesEngineServlet?at=39&mi=10&dpi=10&pt=17&dpn=1&iiqidtype=2&iiqpcid='); + request.respond( + 200, + responseHeader, + JSON.stringify({RESULT: 'NA'}) + ); + expect(callBackSpy.calledOnce).to.be.true; + expect(callBackSpy.args[0][0]).to.be.undefined; + }); + + it('should ignore NA in get id', function () { + let callBackSpy = sinon.spy(); + let submoduleCallback = intentIqIdSubmodule.getId(defaultConfigParams).callback; + submoduleCallback(callBackSpy); + let request = server.requests[0]; + expect(request.url).to.contain('https://api.intentiq.com/profiles_engine/ProfilesEngineServlet?at=39&mi=10&dpi=10&pt=17&dpn=1&iiqidtype=2&iiqpcid='); + request.respond( + 200, + responseHeader, + 'NA' + ); + expect(callBackSpy.calledOnce).to.be.true; + expect(callBackSpy.args[0][0]).to.be.undefined; + }); + + it('should parse result from json response', function () { + let callBackSpy = sinon.spy(); + let submoduleCallback = intentIqIdSubmodule.getId(allConfigParams).callback; + submoduleCallback(callBackSpy); + let request = server.requests[0]; + expect(request.url).to.contain('https://api.intentiq.com/profiles_engine/ProfilesEngineServlet?at=39&mi=10&dpi=10&pt=17&dpn=1&pcid=12&pai=11&iiqidtype=2&iiqpcid='); + request.respond( + 200, + responseHeader, + JSON.stringify({pid: 'test_pid', data: 'test_personid'}) + ); + expect(callBackSpy.calledOnce).to.be.true; + expect(callBackSpy.args[0][0]).to.be.eq('test_personid'); + }); +}); diff --git a/test/spec/modules/interactiveOffersBidAdapter_spec.js b/test/spec/modules/interactiveOffersBidAdapter_spec.js new file mode 100644 index 00000000000..2ea620dc30c --- /dev/null +++ b/test/spec/modules/interactiveOffersBidAdapter_spec.js @@ -0,0 +1,43 @@ +import { expect } from 'chai'; +import {spec} from 'modules/interactiveOffersBidAdapter.js'; + +describe('Interactive Offers Prebbid.js Adapter', function() { + describe('isBidRequestValid function', function() { + let bid = {bidder: 'interactiveOffers', params: {pubid: 100, tmax: 300}, mediaTypes: {banner: {sizes: [[300, 250]]}}, adUnitCode: 'pageAd01', transactionId: '16526f30-3be2-43f6-ab37-f1ab1f2ac25d', sizes: [[300, 250]], bidId: '227faa83f86546', bidderRequestId: '1eb79bc9dd44a', auctionId: '1aad860c-e04b-482b-acac-0da55ed491c8', src: 'client', bidRequestsCount: 1, bidderRequestsCount: 1, bidderWinsCount: 0}; + + it('returns true if all the required params are present and properly formatted', function() { + expect(spec.isBidRequestValid(bid)).to.be.true; + }); + + it('returns false if any if the required params is missing', function() { + bid.params = {}; + expect(spec.isBidRequestValid(bid)).to.be.false; + }); + + it('returns false if any if the required params is not properly formatted', function() { + bid.params = {pubid: 'abcd123', tmax: 250}; + expect(spec.isBidRequestValid(bid)).to.be.false; + bid.params = {pubid: 100, tmax: '+250'}; + expect(spec.isBidRequestValid(bid)).to.be.false; + }); + }); + describe('buildRequests function', function() { + let validBidRequests = [{bidder: 'interactiveOffers', params: {pubid: 100, tmax: 300}, mediaTypes: {banner: {sizes: [[300, 250]]}}, adUnitCode: 'pageAd01', transactionId: '16526f30-3be2-43f6-ab37-f1ab1f2ac25d', sizes: [[300, 250]], bidId: '227faa83f86546', bidderRequestId: '1eb79bc9dd44a', auctionId: '1aad860c-e04b-482b-acac-0da55ed491c8', src: 'client', bidRequestsCount: 1, bidderRequestsCount: 1, bidderWinsCount: 0}]; + let bidderRequest = {bidderCode: 'interactiveOffers', auctionId: '1aad860c-e04b-482b-acac-0da55ed491c8', bidderRequestId: '1eb79bc9dd44a', bids: [{bidder: 'interactiveOffers', params: {pubid: 100, tmax: 300}, mediaTypes: {banner: {sizes: [[300, 250]]}}, adUnitCode: 'pageAd01', transactionId: '16526f30-3be2-43f6-ab37-f1ab1f2ac25d', sizes: [[300, 250]], bidId: '227faa83f86546', bidderRequestId: '1eb79bc9dd44a', auctionId: '1aad860c-e04b-482b-acac-0da55ed491c8', src: 'client', bidRequestsCount: 1, bidderRequestsCount: 1, bidderWinsCount: 0}], timeout: 5000, refererInfo: {referer: 'http://www.google.com', reachedTop: true, isAmp: false, numIframes: 0, stack: ['http://www.google.com'], canonicalUrl: null}}; + + it('returns a Prebid.js request object with a valid json string at the "data" property', function() { + let request = spec.buildRequests(validBidRequests, bidderRequest); + expect(request.data).length !== 0; + }); + }); + describe('interpretResponse function', function() { + let openRTBResponse = {body: [{cur: 'USD', id: '2052afa35febb79baa9893cc3ae8b83b89740df65fe98b1bd358dbae6e912801', seatbid: [{seat: 1493, bid: [{ext: {tagid: '227faa83f86546'}, crid: '24477', adm: '', nurl: '', adid: '1138', adomain: ['url.com'], price: '1.53', w: 300, h: 250, iurl: 'http://url.com', cat: ['IAB13-11'], id: '5507ced7a39c06942d3cb260197112ba712e4180', attr: [], impid: 1, cid: '13280'}]}], 'bidid': '0959b9d58ba71b3db3fa29dce3b117c01fc85de0'}], 'headers': {}}; + let prebidRequest = {method: 'POST', url: 'https://url.com', data: '{"id": "1aad860c-e04b-482b-acac-0da55ed491c8", "site": {"id": "url.com", "name": "url.com", "domain": "url.com", "page": "http://url.com", "ref": "http://url.com", "publisher": {"id": 100, "name": "http://url.com", "domain": "url.com"}, "content": {"language": "pt-PT"}}, "source": {"fd": 0, "tid": "1aad860c-e04b-482b-acac-0da55ed491c8", "pchain": ""}, "device": {"ua": "Mozilla/5.0 (Macintosh; Intel Mac OS X 11_2_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.192 Safari/537.36", "language": "pt-PT"}, "user": {}, "imp": [{"id":1, "secure": 0, "tagid": "227faa83f86546", "banner": {"pos": 0, "w": 300, "h": 250, "format": [{"w": 300, "h": 250}]}}], "tmax": 300}', bidderRequest: {bidderCode: 'interactiveOffers', auctionId: '1aad860c-e04b-482b-acac-0da55ed491c8', bidderRequestId: '1eb79bc9dd44a', bids: [{bidder: 'interactiveOffers', params: {pubid: 100, tmax: 300}, mediaTypes: {banner: {sizes: [[300, 250]]}}, adUnitCode: 'pageAd01', transactionId: '16526f30-3be2-43f6-ab37-f1ab1f2ac25d', sizes: [[300, 250]], bidId: '227faa83f86546', bidderRequestId: '1eb79bc9dd44a', auctionId: '1aad860c-e04b-482b-acac-0da55ed491c8', src: 'client', bidRequestsCount: 1, bidderRequestsCount: 1, bidderWinsCount: 0}], timeout: 5000, refererInfo: {referer: 'http://url.com', reachedTop: true, isAmp: false, numIframes: 0, stack: ['http://url.com'], canonicalUrl: null}}}; + + it('returns an array of Prebid.js response objects', function() { + let prebidResponses = spec.interpretResponse(openRTBResponse, prebidRequest); + expect(prebidResponses).to.not.be.empty; + expect(prebidResponses[0].meta.advertiserDomains[0]).to.equal('url.com'); + }); + }); +}); diff --git a/test/spec/modules/invibesBidAdapter_spec.js b/test/spec/modules/invibesBidAdapter_spec.js index b75c6a4578f..ee3624b5b16 100644 --- a/test/spec/modules/invibesBidAdapter_spec.js +++ b/test/spec/modules/invibesBidAdapter_spec.js @@ -22,7 +22,11 @@ describe('invibesBidAdapter:', function () { [400, 300], [125, 125] ], - transactionId: 't1' + transactionId: 't1', + userId: { + pubcid: 'pub-cid-code', + pubProvidedId: 'pub-provided-id' + } }, { bidId: 'b2', bidder: BIDDER_CODE, @@ -137,11 +141,19 @@ describe('invibesBidAdapter:', function () { }); it('has capped ids if local storage variable is correctly formatted', function () { + top.window.invibes.optIn = 1; + top.window.invibes.purposes = [true, false, false, false, false, false, false, false, false, false]; localStorage.ivvcap = '{"9731":[1,1768600800000]}'; const request = spec.buildRequests(bidRequests, {auctionStart: Date.now()}); expect(request.data.capCounts).to.equal('9731=1'); }); + it('does not have capped ids if local storage variable is correctly formatted but no opt in', function () { + localStorage.ivvcap = '{"9731":[1,1768600800000]}'; + const request = spec.buildRequests(bidRequests, {auctionStart: Date.now()}); + expect(request.data.capCounts).to.equal(''); + }); + it('does not have capped ids if local storage variable is incorrectly formatted', function () { localStorage.ivvcap = ':[1,1574334216992]}'; const request = spec.buildRequests(bidRequests, {auctionStart: Date.now()}); @@ -178,106 +190,143 @@ describe('invibesBidAdapter:', function () { expect(JSON.parse(request.data.bidParamsJson).placementIds).to.contain(bidRequests[1].params.placementId); }); - it('uses cookies', function () { - global.document.cookie = 'ivNoCookie=1'; + it('sends undefined lid when no cookie', function () { let request = spec.buildRequests(bidRequests); expect(request.data.lId).to.be.undefined; }); - it('doesnt send the domain id if not graduated', function () { - global.document.cookie = 'ivbsdid={"id":"dvdjkams6nkq","cr":1522929537626,"hc":1}'; - let request = spec.buildRequests(bidRequests); - expect(request.data.lId).to.not.exist; - }); - - it('try to graduate but not enough count - doesnt send the domain id', function () { - global.document.cookie = 'ivbsdid={"id":"dvdjkams6nkq","cr":1521818537626,"hc":0}'; - let bidderRequest = {gdprConsent: {vendorData: {vendorConsents: {436: true}}}}; - let request = spec.buildRequests(bidRequests, bidderRequest); - expect(request.data.lId).to.not.exist; - }); - - it('try to graduate but not old enough - doesnt send the domain id', function () { - let bidderRequest = {gdprConsent: {vendorData: {vendorConsents: {436: true}}}}; - global.document.cookie = 'ivbsdid={"id":"dvdjkams6nkq","cr":' + Date.now() + ',"hc":5}'; - let request = spec.buildRequests(bidRequests, bidderRequest); - expect(request.data.lId).to.not.exist; - }); - - it('graduate and send the domain id', function () { - let bidderRequest = {gdprConsent: {vendorData: {vendorConsents: {436: true}}}}; - stubDomainOptions(new StubbedPersistence('{"id":"dvdjkams6nkq","cr":1521818537626,"hc":7}')); - let request = spec.buildRequests(bidRequests, bidderRequest); - expect(request.data.lId).to.exist; - }); - - it('send the domain id if already graduated', function () { - let bidderRequest = {gdprConsent: {vendorData: {vendorConsents: {436: true}}}}; - stubDomainOptions(new StubbedPersistence('{"id":"f8zoh044p9oi"}')); - let request = spec.buildRequests(bidRequests, bidderRequest); - expect(request.data.lId).to.exist; - expect(top.window.invibes.dom.tempId).to.exist; - }); - - it('send the domain id after replacing it with new format', function () { + it('sends lid when comes on cookie', function () { + top.window.invibes.optIn = 1; + top.window.invibes.purposes = [true, false, false, false, false, false, false, false, false, false]; + global.document.cookie = 'ivbsdid={"id":"dvdjkams6nkq","cr":' + Date.now() + ',"hc":0}'; let bidderRequest = {gdprConsent: {vendorData: {vendorConsents: {436: true}}}}; - stubDomainOptions(new StubbedPersistence('{"id":"f8zoh044p9oi.8537626"}')); let request = spec.buildRequests(bidRequests, bidderRequest); expect(request.data.lId).to.exist; - expect(top.window.invibes.dom.tempId).to.exist; }); - it('dont send the domain id if consent declined on tcf v1', function () { + it('should send purpose 1', function () { let bidderRequest = { gdprConsent: { vendorData: { gdprApplies: true, hasGlobalConsent: false, - vendorConsents: {436: false} + vendor: {consents: {436: true}}, + purpose: { + consents: { + 1: true, + 2: true, + 3: true, + 4: true, + 5: true, + 6: true, + 7: true, + 8: true, + 9: true, + 10: true + } + } } } }; - stubDomainOptions(new StubbedPersistence('{"id":"f8zoh044p9oi.8537626"}')); let request = spec.buildRequests(bidRequests, bidderRequest); - expect(request.data.lId).to.not.exist; - expect(top.window.invibes.dom.tempId).to.not.exist; - expect(request.data.oi).to.equal(0); + expect(request.data.purposes.split(',')[0]).to.equal('true'); }); - it('dont send the domain id if consent declined on tcf v2', function () { + it('should send purpose 2 & 7', function () { let bidderRequest = { gdprConsent: { vendorData: { gdprApplies: true, hasGlobalConsent: false, - vendor: {consents: {436: false}} + vendor: {consents: {436: true}}, + purpose: { + consents: { + 1: true, + 2: true, + 3: true, + 4: true, + 5: true, + 6: true, + 7: true, + 8: true, + 9: true, + 10: true + } + } } } }; - stubDomainOptions(new StubbedPersistence('{"id":"f8zoh044p9oi.8537626"}')); let request = spec.buildRequests(bidRequests, bidderRequest); - expect(request.data.lId).to.not.exist; - expect(top.window.invibes.dom.tempId).to.not.exist; - expect(request.data.oi).to.equal(0); + expect(request.data.purposes.split(',')[1] && request.data.purposes.split(',')[6]).to.equal('true'); }); - it('dont send the domain id if no consent', function () { - let bidderRequest = {}; - stubDomainOptions(new StubbedPersistence('{"id":"f8zoh044p9oi.8537626"}')); + it('should send purpose 9', function () { + let bidderRequest = { + gdprConsent: { + vendorData: { + gdprApplies: true, + hasGlobalConsent: false, + vendor: {consents: {436: true}}, + purpose: { + consents: { + 1: true, + 2: true, + 3: true, + 4: true, + 5: true, + 6: true, + 7: true, + 8: true, + 9: true, + 10: true + } + } + } + } + }; let request = spec.buildRequests(bidRequests, bidderRequest); - expect(request.data.lId).to.not.exist; - expect(top.window.invibes.dom.tempId).to.not.exist; + expect(request.data.purposes.split(',')[9]).to.equal('true'); }); - it('try to init id but was already loaded on page - does not increment the id again', function () { - let bidderRequest = {gdprConsent: {vendorData: {vendorConsents: {436: true}}}}; - global.document.cookie = 'ivbsdid={"id":"dvdjkams6nkq","cr":1521818537626,"hc":0}'; + it('should send legitimateInterests 2 & 7', function () { + let bidderRequest = { + gdprConsent: { + vendorData: { + gdprApplies: true, + hasGlobalConsent: false, + vendor: {consents: {436: true}}, + purpose: { + consents: { + 1: true, + 2: true, + 3: true, + 4: true, + 5: true, + 6: true, + 7: true, + 8: true, + 9: true, + 10: true + }, + legitimateInterests: { + 1: true, + 2: true, + 3: true, + 4: true, + 5: true, + 6: true, + 7: true, + 8: true, + 9: true, + 10: true + } + } + } + } + }; let request = spec.buildRequests(bidRequests, bidderRequest); - request = spec.buildRequests(bidRequests, bidderRequest); - expect(request.data.lId).to.not.exist; - expect(top.window.invibes.dom.tempId).to.exist; + expect(request.data.li.split(',')[1] && request.data.li.split(',')[6]).to.equal('true'); }); - it('should send oi = 0 when vendorData is null', function () { let bidderRequest = { gdprConsent: { @@ -315,7 +364,6 @@ describe('invibesBidAdapter:', function () { let request = spec.buildRequests(bidRequests, bidderRequest); expect(request.data.oi).to.equal(2); }); - it('should send oi = 0 when vendor consents for invibes are false on tcf v2', function () { let bidderRequest = { gdprConsent: { @@ -343,8 +391,76 @@ describe('invibesBidAdapter:', function () { let request = spec.buildRequests(bidRequests, bidderRequest); expect(request.data.oi).to.equal(0); }); + it('should send oi = 2 when vendor consent for invibes are false and vendor legitimate interest for invibes are true on tcf v2', function () { + let bidderRequest = { + gdprConsent: { + vendorData: { + gdprApplies: true, + hasGlobalConsent: false, + vendor: {consents: {436: false}, legitimateInterests: {436: true}}, + purpose: { + consents: { + 1: true, + 2: true, + 3: true, + 4: true, + 5: true, + 6: true, + 7: true, + 8: true, + 9: true, + 10: true + } + } + } + } + }; + let request = spec.buildRequests(bidRequests, bidderRequest); + expect(request.data.oi).to.equal(2); + }); + it('should send oi = 0 when vendor consents and legitimate interests for invibes are false on tcf v2', function () { + let bidderRequest = { + gdprConsent: { + vendorData: { + gdprApplies: true, + hasGlobalConsent: false, + vendor: {consents: {436: false}, legitimateInterests: {436: false}}, + purpose: { + consents: { + 1: true, + 2: true, + 3: true, + 4: true, + 5: true, + 6: true, + 7: true, + 8: true, + 9: true, + 10: true + } + } + } + } + }; + let request = spec.buildRequests(bidRequests, bidderRequest); + expect(request.data.oi).to.equal(0); + }); + it('should send oi = 0 when purpose consents is null', function () { + let bidderRequest = { + gdprConsent: { + vendorData: { + gdprApplies: true, + hasGlobalConsent: false, + vendor: {consents: {436: false}}, + purpose: {} + } + } + }; + let request = spec.buildRequests(bidRequests, bidderRequest); + expect(request.data.oi).to.equal(0); + }); - it('should send oi = 0 when purpose consents weren\'t approved on tcf v2', function () { + it('should send oi = 2 when purpose consents weren\'t approved on tcf v2', function () { let bidderRequest = { gdprConsent: { vendorData: { @@ -369,10 +485,10 @@ describe('invibesBidAdapter:', function () { } }; let request = spec.buildRequests(bidRequests, bidderRequest); - expect(request.data.oi).to.equal(0); + expect(request.data.oi).to.equal(2); }); - it('should send oi = 0 when purpose consents are less then 10 on tcf v2', function () { + it('should send oi = 2 when purpose consents are less then 10 on tcf v2', function () { let bidderRequest = { gdprConsent: { vendorData: { @@ -392,7 +508,7 @@ describe('invibesBidAdapter:', function () { } }; let request = spec.buildRequests(bidRequests, bidderRequest); - expect(request.data.oi).to.equal(0); + expect(request.data.oi).to.equal(2); }); it('should send oi = 4 when vendor consents are null on tcf v2', function () { @@ -527,7 +643,7 @@ describe('invibesBidAdapter:', function () { expect(request.data.oi).to.equal(2); }); - it('should send oi = 0 when purpose consents weren\'t approved on tcf v1', function () { + it('should send oi = 2 when purpose consents weren\'t approved on tcf v1', function () { let bidderRequest = { gdprConsent: { vendorData: { @@ -545,10 +661,10 @@ describe('invibesBidAdapter:', function () { } }; let request = spec.buildRequests(bidRequests, bidderRequest); - expect(request.data.oi).to.equal(0); + expect(request.data.oi).to.equal(2); }); - it('should send oi = 0 when purpose consents are less then 5 on tcf v1', function () { + it('should send oi = 2 when purpose consents are less then 5 on tcf v1', function () { let bidderRequest = { gdprConsent: { vendorData: { @@ -564,7 +680,7 @@ describe('invibesBidAdapter:', function () { } }; let request = spec.buildRequests(bidRequests, bidderRequest); - expect(request.data.oi).to.equal(0); + expect(request.data.oi).to.equal(2); }); it('should send oi = 0 when vendor consents for invibes are false on tcf v1', function () { @@ -669,8 +785,8 @@ describe('invibesBidAdapter:', function () { context('when the response is valid', function () { it('responds with a valid bid', function () { - top.window.invibes.setCookie('a', 'b', 370); - top.window.invibes.setCookie('c', 'd', 0); + // top.window.invibes.setCookie('a', 'b', 370); + // top.window.invibes.setCookie('c', 'd', 0); let result = spec.interpretResponse({body: response}, {bidRequests}); expect(Object.keys(result[0])).to.have.members(Object.keys(expectedResponse[0])); }); @@ -705,6 +821,16 @@ describe('invibesBidAdapter:', function () { expect(response.url).to.include(SYNC_ENDPOINT); expect(response.url).to.include('optIn'); expect(response.url).to.include('ivvbks'); + }); + + it('returns an iframe with params including if enabled', function () { + top.window.invibes.optIn = 1; + global.document.cookie = 'ivvbks=17639.0,1,2;ivbsdid={"id":"dvdjkams6nkq","cr":' + Date.now() + ',"hc":0}'; + let response = spec.getUserSyncs({iframeEnabled: true}); + expect(response.type).to.equal('iframe'); + expect(response.url).to.include(SYNC_ENDPOINT); + expect(response.url).to.include('optIn'); + expect(response.url).to.include('ivvbks'); expect(response.url).to.include('ivbsdid'); }); 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/ironsourceBidAdapter_spec.js b/test/spec/modules/ironsourceBidAdapter_spec.js new file mode 100644 index 00000000000..cca928ff28b --- /dev/null +++ b/test/spec/modules/ironsourceBidAdapter_spec.js @@ -0,0 +1,381 @@ +import { expect } from 'chai'; +import { spec } from 'modules/ironsourceBidAdapter.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('ironsourceAdapter', 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': { + 'isOrg': '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 = { + 'isOrg': null + }; + expect(spec.isBidRequestValid(newBid)).to.equal(false); + }); + }); + + describe('buildRequests', function () { + const bidRequests = [ + { + 'bidder': spec.code, + 'adUnitCode': 'adunit-code', + 'sizes': [[640, 480]], + 'params': { + 'isOrg': 'jdye8weeyirk00000001' + }, + 'bidId': '299ffc8cca0b87', + 'bidderRequestId': '1144f487e563f9', + 'auctionId': 'bfc420c3-8577-4568-9766-a8a935fb620d', + } + ]; + + const testModeBidRequests = [ + { + 'bidder': spec.code, + 'adUnitCode': 'adunit-code', + 'sizes': [[640, 480]], + 'params': { + 'isOrg': 'jdye8weeyirk00000001', + 'testMode': true + }, + 'bidId': '299ffc8cca0b87', + 'bidderRequestId': '1144f487e563f9', + 'auctionId': 'bfc420c3-8577-4568-9766-a8a935fb620d', + } + ]; + + const bidderRequest = { + bidderCode: 'ironsource', + } + + const customSessionId = '12345678'; + + 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('sends the is_wrapper query param', function () { + bidRequests[0].params.isWrapper = true; + const requests = spec.buildRequests(bidRequests, bidderRequest); + for (const request of requests) { + expect(request.data.is_wrapper).to.equal(true); + } + }); + + it('sends the custom session id as a query param', function () { + bidRequests[0].params.sessionId = customSessionId; + const requests = spec.buildRequests(bidRequests, bidderRequest); + for (const request of requests) { + expect(request.data.session_id).to.equal(customSessionId); + } + }); + + 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/ixBidAdapter_spec.js b/test/spec/modules/ixBidAdapter_spec.js index 63b04077f4e..e0d45913fd9 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'; @@ -18,7 +19,6 @@ describe('IndexexchangeAdapter', function () { 'sid': '00001', 'hp': 1 }, - { 'asi': 'indirectseller-2.com', 'sid': '00002', @@ -26,6 +26,129 @@ describe('IndexexchangeAdapter', function () { } ] }; + const div_many_sizes = [ + [300, 250], + [600, 410], + [336, 280], + [400, 300], + [320, 50], + [360, 360], + [250, 250], + [320, 250], + [400, 250], + [387, 359], + [300, 50], + [372, 250], + [320, 320], + [412, 412], + [327, 272], + [312, 260], + [384, 320], + [335, 250], + [366, 305], + [374, 250], + [375, 375], + [272, 391], + [364, 303], + [414, 414], + [366, 375], + [272, 360], + [364, 373], + [366, 359], + [320, 100], + [360, 250], + [468, 60], + [480, 300], + [600, 400], + [600, 300], + [33, 28], + [40, 30], + [32, 5], + [36, 36], + [25, 25], + [320, 25], + [400, 25], + [387, 35], + [300, 5], + [372, 20], + [320, 32], + [412, 41], + [327, 27], + [312, 26], + [384, 32], + [335, 25], + [366, 30], + [374, 25], + [375, 37], + [272, 31], + [364, 303], + [414, 41], + [366, 35], + [272, 60], + [364, 73], + [366, 59], + [320, 10], + [360, 25], + [468, 6], + [480, 30], + [600, 40], + [600, 30] + ]; + + const ONE_VIDEO = [ + { + bidder: 'ix', + params: { + siteId: '456', + video: { + skippable: false, + mimes: [ + 'video/mp4', + 'video/webm' + ], + minduration: 0, + maxduration: 60, + protocols: [2] + }, + size: [400, 100] + }, + sizes: [[400, 100]], + mediaTypes: { + video: { + context: 'instream', + playerSize: [[400, 100]] + } + }, + adUnitCode: 'div-gpt-ad-1460505748562-0', + transactionId: '173f49a8-7549-4218-a23c-e7ba59b47230', + bidId: '1a2b3c4e', + bidderRequestId: '11a22b33c44e', + auctionId: '1aa2bb3cc4de', + schain: SAMPLE_SCHAIN + } + ]; + + const ONE_BANNER = [ + { + bidder: 'ix', + params: { + siteId: '123', + size: [300, 250] + }, + sizes: [[300, 250]], + mediaTypes: { + banner: { + sizes: [[300, 250]] + } + }, + adUnitCode: 'div-gpt-ad-1460505748561-0', + transactionId: '173f49a8-7549-4218-a23c-e7ba59b47229', + bidId: '1a2b3c4d', + bidderRequestId: '11a22b33c44d', + auctionId: '1aa2bb3cc4dd', + schain: SAMPLE_SCHAIN + } + ]; const DEFAULT_BANNER_VALID_BID = [ { @@ -60,7 +183,9 @@ describe('IndexexchangeAdapter', function () { 'video/mp4', 'video/webm' ], - minduration: 0 + minduration: 0, + maxduration: 60, + protocols: [2] }, size: [400, 100] }, @@ -80,6 +205,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', @@ -221,8 +408,14 @@ describe('IndexexchangeAdapter', function () { const DEFAULT_USERID_DATA = { idl_env: '1234-5678-9012-3456', // Liveramp + netId: 'testnetid123', // NetId + IDP: 'userIDP000', // IDP + fabrickId: 'fabrickId9000', // FabrickId + uid2: {id: 'testuid2'} // UID 2.0 }; + const DEFAULT_USERIDASEIDS_DATA = createEidsArray(DEFAULT_USERID_DATA); + const DEFAULT_USERID_PAYLOAD = [ { source: 'liveramp.com', @@ -232,9 +425,46 @@ 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' + } + }] + }, { + source: 'uidapi.com', + uids: [{ + // when calling createEidsArray, UID2's getValue func returns .id, which is then set in uids + id: DEFAULT_USERID_DATA.uid2.id, + ext: { + rtiPartner: 'UID2' + } + }] } ]; + const DEFAULT_USERID_BID_DATA = { + lotamePanoramaId: 'bd738d136bdaa841117fe9b331bb4' + }; + describe('inherited functions', function () { it('should exists and is a function', function () { const adapter = newBidder(spec); @@ -370,6 +600,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; @@ -395,6 +635,71 @@ 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('Roundel alias adapter', function () { + const vaildBids = [DEFAULT_BANNER_VALID_BID, DEFAULT_VIDEO_VALID_BID, DEFAULT_MULTIFORMAT_BANNER_VALID_BID, DEFAULT_MULTIFORMAT_VIDEO_VALID_BID]; + const ALIAS_OPTIONS = Object.assign({ + bidderCode: 'roundel' + }, DEFAULT_OPTION); + + it('should not build requests for mediaTypes if liveramp data is unavaliable', function () { + vaildBids.forEach((validBid) => { + const request = spec.buildRequests(validBid, ALIAS_OPTIONS); + expect(request).to.be.an('array'); + expect(request).to.have.lengthOf(0); + }); + }); + + it('should build requests for mediaTypes if liveramp data is avaliable', function () { + vaildBids.forEach((validBid) => { + const cloneValidBid = utils.deepClone(validBid); + cloneValidBid[0].userIdAsEids = utils.deepClone(DEFAULT_USERIDASEIDS_DATA); + const request = spec.buildRequests(cloneValidBid, ALIAS_OPTIONS); + const payload = JSON.parse(request[0].data.r); + expect(request).to.be.an('array'); + expect(request).to.have.lengthOf(1); + expect(payload.user.eids).to.have.lengthOf(5); + expect(payload.user.eids).to.deep.include(DEFAULT_USERID_PAYLOAD[0]); + }); + }); }); describe('buildRequestsIdentity', function () { @@ -584,15 +889,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].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(5); 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]); + expect(payload.user.eids).to.deep.include(DEFAULT_USERID_PAYLOAD[4]); }); it('We continue to send in IXL identity info and Prebid takes precedence over IXL', function () { @@ -646,11 +954,44 @@ 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].userIdAsEids = utils.deepClone(DEFAULT_USERIDASEIDS_DATA); const request = spec.buildRequests(cloneValidBid, DEFAULT_OPTION)[0]; const payload = JSON.parse(request.data.r); @@ -691,10 +1032,15 @@ describe('IndexexchangeAdapter', function () { }) expect(payload.user).to.exist; - expect(payload.user.eids).to.have.lengthOf(3); + expect(payload.user.eids).to.have.lengthOf(7); + 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]); + expect(payload.user.eids).to.deep.include(validUserIdPayload[6]); }); it('IXL and Prebid are mutually exclusive', function () { @@ -716,7 +1062,7 @@ describe('IndexexchangeAdapter', function () { }; const cloneValidBid = utils.deepClone(DEFAULT_VIDEO_VALID_BID); - cloneValidBid[0].userId = utils.deepClone(DEFAULT_USERID_DATA); + cloneValidBid[0].userIdAsEids = utils.deepClone(DEFAULT_USERIDASEIDS_DATA); const request = spec.buildRequests(cloneValidBid, DEFAULT_OPTION)[0]; @@ -734,9 +1080,39 @@ 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(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]); + }); + }); + + describe('getUserIds', function () { + it('request should contain userId information if configured and within bid request', function () { + config.setConfig({ + userSync: { + syncDelay: 0, + userIds: [ + { name: 'lotamePanoramaId' }, + { name: 'merkleId' }, + { name: 'parrableId' }, + ] + } + }); + + const bid = utils.deepClone(DEFAULT_BANNER_VALID_BID[0]); + bid.userId = DEFAULT_USERID_BID_DATA; + + const request = spec.buildRequests([bid], DEFAULT_OPTION)[0]; + const r = JSON.parse(request.data.r); + + expect(r.ext.ixdiag.userIds).to.be.an('array'); + expect(r.ext.ixdiag.userIds.should.include('lotamePanoramaId')); + expect(r.ext.ixdiag.userIds.should.not.include('merkleId')); + expect(r.ext.ixdiag.userIds.should.not.include('parrableId')); }); }); @@ -805,6 +1181,84 @@ describe('IndexexchangeAdapter', function () { expect(impression.ext.sid).to.equal(sidValue); }); + it('video impression has #priceFloors floors', function () { + const bid = utils.deepClone(ONE_VIDEO[0]); + const flr = 5.5 + const floorInfo = {floor: flr, currency: 'USD'}; + bid.getFloor = function () { + return floorInfo; + } + + // check if floors are in imp + const requestBidFloor = spec.buildRequests([bid])[0]; + const imp1 = JSON.parse(requestBidFloor.data.r).imp[0]; + expect(imp1.bidfloor).to.equal(flr); + expect(imp1.bidfloorcur).to.equal('USD'); + expect(imp1.ext.fl).to.equal('p'); + }); + + it('banner imp has floors from #priceFloors module', function () { + const floor300x250 = 3.25 + const bid = utils.deepClone(DEFAULT_BANNER_VALID_BID[0]) + + const floorInfo = { floor: floor300x250, currency: 'USD' }; + bid.getFloor = function () { + return floorInfo; + }; + + // check if floors are in imp + const requestBidFloor = spec.buildRequests([bid])[0]; + const imp1 = JSON.parse(requestBidFloor.data.r).imp[0]; + + expect(imp1.bidfloorcur).to.equal('USD'); + expect(imp1.bidfloor).to.equal(floor300x250); + expect(imp1.ext.fl).to.equal('p'); + }); + + it('ix adapter floors chosen over #priceFloors ', function () { + const bid = utils.deepClone(ONE_BANNER[0]); + + const floorhi = 4.5 + const floorlow = 3.5 + + bid.params.bidFloor = floorhi + bid.params.bidFloorCur = 'USD' + + const floorInfo = { floor: floorlow, currency: 'USD' }; + bid.getFloor = function () { + return floorInfo; + }; + + // check if floors are in imp + const requestBidFloor = spec.buildRequests([bid])[0]; + const imp1 = JSON.parse(requestBidFloor.data.r).imp[0]; + expect(imp1.bidfloor).to.equal(floorhi); + expect(imp1.bidfloorcur).to.equal(bid.params.bidFloorCur); + expect(imp1.ext.fl).to.equal('x'); + }); + + it(' #priceFloors floors chosen over ix adapter floors', function () { + const bid = utils.deepClone(ONE_BANNER[0]); + + const floorhi = 4.5 + const floorlow = 3.5 + + bid.params.bidFloor = floorlow + bid.params.bidFloorCur = 'USD' + + const floorInfo = { floor: floorhi, currency: 'USD' }; + bid.getFloor = function () { + return floorInfo; + }; + + // check if floors are in imp + const requestBidFloor = spec.buildRequests([bid])[0]; + const imp1 = JSON.parse(requestBidFloor.data.r).imp[0]; + expect(imp1.bidfloor).to.equal(floorhi); + expect(imp1.bidfloorcur).to.equal(bid.params.bidFloorCur); + expect(imp1.ext.fl).to.equal('p'); + }); + it('impression should have bidFloor and bidFloorCur if configured', function () { const bid = utils.deepClone(DEFAULT_BANNER_VALID_BID[0]); bid.params.bidFloor = 50; @@ -814,6 +1268,73 @@ describe('IndexexchangeAdapter', function () { expect(impression.bidfloor).to.equal(bid.params.bidFloor); expect(impression.bidfloorcur).to.equal(bid.params.bidFloorCur); + expect(impression.ext.fl).to.equal('x'); + }); + + it('missing sizes #priceFloors ', function () { + const bid = utils.deepClone(ONE_BANNER[0]); + bid.mediaTypes.banner.sizes.push([500, 400]) + + const floorInfo = { floor: 3.25, currency: 'USD' }; + bid.getFloor = function () { + return floorInfo; + }; + + sinon.spy(bid, 'getFloor'); + + const requestBidFloor = spec.buildRequests([bid])[0]; + // called getFloor with 300 x 250 + expect(bid.getFloor.getCall(0).args[0].mediaType).to.equal('banner'); + expect(bid.getFloor.getCall(0).args[0].size[0]).to.equal(300); + expect(bid.getFloor.getCall(0).args[0].size[1]).to.equal(250); + + // called getFloor with 500 x 400 + expect(bid.getFloor.getCall(1).args[0].mediaType).to.equal('banner'); + expect(bid.getFloor.getCall(1).args[0].size[0]).to.equal(500); + expect(bid.getFloor.getCall(1).args[0].size[1]).to.equal(400); + + // both will have same floors due to mock getFloor + const imp1 = JSON.parse(requestBidFloor.data.r).imp[0]; + expect(imp1.bidfloor).to.equal(3.25); + expect(imp1.bidfloorcur).to.equal('USD'); + + const imp2 = JSON.parse(requestBidFloor.data.r).imp[1]; + expect(imp2.bidfloor).to.equal(3.25); + expect(imp2.bidfloorcur).to.equal('USD'); + }); + + it('#pricefloors inAdUnit, banner impressions should have floors', function () { + const bid = utils.deepClone(ONE_BANNER[0]); + + const flr = 4.3 + bid.floors = { + currency: 'USD', + schema: { + delimiter: '|', + fields: ['mediaType', 'size'] + }, + values: { + 'banner|300x250': flr, + 'banner|600x500': 6.5, + 'banner|*': 7.5 + } + }; + const floorInfo = { floor: flr, currency: 'USD' }; + bid.getFloor = function () { + return floorInfo; + }; + + sinon.spy(bid, 'getFloor'); + + const requestBidFloor = spec.buildRequests([bid])[0]; + // called getFloor with 300 x 250 + expect(bid.getFloor.getCall(0).args[0].mediaType).to.equal('banner'); + expect(bid.getFloor.getCall(0).args[0].size[0]).to.equal(300); + expect(bid.getFloor.getCall(0).args[0].size[1]).to.equal(250); + + const imp1 = JSON.parse(requestBidFloor.data.r).imp[0]; + expect(imp1.bidfloor).to.equal(flr); + expect(imp1.bidfloorcur).to.equal('USD'); }); it('payload without mediaType should have correct format and value', function () { @@ -893,7 +1414,6 @@ describe('IndexexchangeAdapter', function () { const requestWithFirstPartyData = spec.buildRequests(DEFAULT_BANNER_VALID_BID, DEFAULT_OPTION)[0]; const pageUrl = JSON.parse(requestWithFirstPartyData.data.r).site.page; const expectedPageUrl = DEFAULT_OPTION.refererInfo.referer + '?ab=123&cd=123%23ab&e%2Ff=456&h%3Fg=456%23cd'; - expect(pageUrl).to.equal(expectedPageUrl); }); @@ -972,6 +1492,97 @@ describe('IndexexchangeAdapter', function () { expect(videoImp.video.h).to.equal(DEFAULT_VIDEO_VALID_BID[0].params.size[1]); }); + it('single request under 8k size limit for large ad unit', function () { + const options = {}; + const bid1 = utils.deepClone(DEFAULT_BANNER_VALID_BID[0]); + bid1.mediaTypes.banner.sizes = div_many_sizes; + const requests = spec.buildRequests([bid1], options); + + const reqSize = new Blob([`${requests[0].url}?${utils.parseQueryStringParameters(requests[0].data)}`]).size; + expect(requests).to.be.an('array'); + expect(requests).to.have.lengthOf(1); + expect(reqSize).to.be.lessThan(8000); + }); + + it('2 requests due to 2 ad units, one larger than url size', function () { + const bid1 = utils.deepClone(DEFAULT_BANNER_VALID_BID[0]); + bid1.mediaTypes.banner.sizes = div_many_sizes; + bid1.params.siteId = '124'; + bid1.adUnitCode = 'div-gpt-1' + bid1.transactionId = '152e36d1-1241-4242-t35e-y1dv34d12315'; + bid1.bidId = '2f6g5s5e'; + + const requests = spec.buildRequests([bid1, DEFAULT_BANNER_VALID_BID[0]], DEFAULT_OPTION); + expect(requests).to.be.an('array'); + expect(requests).to.have.lengthOf(2); + expect(requests[0].data.sn).to.be.equal(0); + expect(requests[1].data.sn).to.be.equal(1); + }); + + it('6 ad units should generate only 4 requests', function () { + const bid1 = utils.deepClone(DEFAULT_BANNER_VALID_BID[0]); + bid1.mediaTypes.banner.sizes = div_many_sizes; + bid1.params.siteId = '121'; + bid1.adUnitCode = 'div-gpt-1' + bid1.transactionId = 'tr1'; + bid1.bidId = '2f6g5s5e'; + + const bid2 = utils.deepClone(bid1); + bid2.transactionId = 'tr2'; + + const bid3 = utils.deepClone(bid1); + bid3.transactionId = 'tr3'; + + const bid4 = utils.deepClone(bid1); + bid4.transactionId = 'tr4'; + + const bid5 = utils.deepClone(bid1); + bid5.transactionId = 'tr5'; + + const bid6 = utils.deepClone(bid1); + bid6.transactionId = 'tr6'; + + const requests = spec.buildRequests([bid1, bid2, bid3, bid4, bid5, bid6], DEFAULT_OPTION); + + expect(requests).to.be.an('array'); + expect(requests).to.have.lengthOf(4); + + // check if seq number increases + for (var i = 0; i < requests.length; i++) { + const reqSize = new Blob([`${requests[i].url}?${utils.parseQueryStringParameters(requests[i].data)}`]).size; + expect(reqSize).to.be.lessThan(8000); + let payload = JSON.parse(requests[i].data.r); + if (requests.length > 1) { + expect(requests[i].data.sn).to.equal(i); + } + expect(payload.source.ext.schain).to.deep.equal(SAMPLE_SCHAIN); + } + }); + + it('multiple ad units in one request', function () { + const bid1 = utils.deepClone(DEFAULT_BANNER_VALID_BID[0]); + bid1.mediaTypes.banner.sizes = [[300, 250], [300, 600], [100, 200]]; + bid1.params.siteId = '121'; + bid1.adUnitCode = 'div-gpt-1' + bid1.transactionId = 'tr1'; + bid1.bidId = '2f6g5s5e'; + + const bid2 = utils.deepClone(bid1); + bid2.transactionId = 'tr2'; + bid2.mediaTypes.banner.sizes = [[220, 221], [222, 223], [300, 250]]; + const bid3 = utils.deepClone(bid1); + bid3.transactionId = 'tr3'; + bid3.mediaTypes.banner.sizes = [[330, 331], [332, 333], [300, 250]]; + + const requests = spec.buildRequests([bid1, bid2, bid3], DEFAULT_OPTION); + expect(requests).to.be.an('array'); + expect(requests).to.have.lengthOf(1); + + const impressions = JSON.parse(requests[0].data.r).imp; + expect(impressions).to.be.an('array'); + expect(impressions).to.have.lengthOf(9); + }); + it('request should contain the extra banner ad sizes that IX is not configured for using the first site id in the ad unit', function () { const bid = utils.deepClone(DEFAULT_BANNER_VALID_BID[0]); bid.sizes.push([336, 280], [970, 90]); @@ -1017,23 +1628,26 @@ describe('IndexexchangeAdapter', function () { const impressions = JSON.parse(request.data.r).imp; expect(impressions).to.be.an('array'); expect(impressions).to.have.lengthOf(4); - - expect(impressions[0].ext.siteID).to.equal(DEFAULT_BANNER_VALID_BID[0].params.siteId.toString()) - expect(impressions[1].ext.siteID).to.equal(bid.params.siteId) - expect(impressions[2].ext.siteID).to.equal(DEFAULT_BANNER_VALID_BID[0].params.siteId.toString()) - expect(impressions[3].ext.siteID).to.equal(bid.params.siteId) + expect(impressions[0].ext.siteID).to.equal(DEFAULT_BANNER_VALID_BID[0].params.siteId.toString()); + expect(impressions[1].ext.siteID).to.equal(bid.params.siteId); + expect(impressions[2].ext.siteID).to.equal(DEFAULT_BANNER_VALID_BID[0].params.siteId.toString()); + expect(impressions[3].ext.siteID).to.equal(bid.params.siteId); expect(impressions[0].banner.w).to.equal(DEFAULT_BANNER_VALID_BID[0].params.size[0]); expect(impressions[0].banner.h).to.equal(DEFAULT_BANNER_VALID_BID[0].params.size[1]); + expect(impressions[1].banner.w).to.equal(bid.params.size[0]); expect(impressions[1].banner.h).to.equal(bid.params.size[1]); + expect(impressions[2].banner.w).to.equal(DEFAULT_BANNER_VALID_BID[0].mediaTypes.banner.sizes[1][0]); expect(impressions[2].banner.h).to.equal(DEFAULT_BANNER_VALID_BID[0].mediaTypes.banner.sizes[1][1]); + expect(impressions[3].banner.w).to.equal(bid.mediaTypes.banner.sizes[1][0]); expect(impressions[3].banner.h).to.equal(bid.mediaTypes.banner.sizes[1][1]); expect(impressions[0].ext.sid).to.equal(`${DEFAULT_BANNER_VALID_BID[0].params.size[0].toString()}x${DEFAULT_BANNER_VALID_BID[0].params.size[1].toString()}`); expect(impressions[1].ext.sid).to.equal(`${bid.params.size[0].toString()}x${bid.params.size[1].toString()}`); + expect(impressions[2].ext.sid).to.equal(`${DEFAULT_BANNER_VALID_BID[0].mediaTypes.banner.sizes[1][0].toString()}x${DEFAULT_BANNER_VALID_BID[0].mediaTypes.banner.sizes[1][1].toString()}`); expect(impressions[3].ext.sid).to.equal(`${bid.mediaTypes.banner.sizes[1][0].toString()}x${bid.mediaTypes.banner.sizes[1][1].toString()}`); }); @@ -1045,6 +1659,28 @@ describe('IndexexchangeAdapter', function () { expect(impressions).to.be.an('array'); expect(impressions).to.have.lengthOf(1); }); + + describe('detect missing sizes', function () { + beforeEach(function () { + config.setConfig({ + ix: { + detectMissingSizes: false + } + }); + }) + + it('request should not contain missing sizes if detectMissingSizes = false', function () { + const bid1 = utils.deepClone(DEFAULT_BANNER_VALID_BID[0]); + bid1.mediaTypes.banner.sizes = div_many_sizes; + + const requests = spec.buildRequests([bid1, DEFAULT_BANNER_VALID_BID[0]], DEFAULT_OPTION); + + const impressions = JSON.parse(requests[0].data.r).imp; + + expect(impressions).to.be.an('array'); + expect(impressions).to.have.lengthOf(2); + }); + }); }); describe('buildRequestVideo', function () { @@ -1111,6 +1747,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 allowlisted', 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 allowlisted 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 () { @@ -1385,5 +2117,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/jixieBidAdapter_spec.js b/test/spec/modules/jixieBidAdapter_spec.js new file mode 100644 index 00000000000..842f9e0ed30 --- /dev/null +++ b/test/spec/modules/jixieBidAdapter_spec.js @@ -0,0 +1,597 @@ +import { expect } from 'chai'; +import { spec, internal as jixieaux, storage } from 'modules/jixieBidAdapter.js'; +import { newBidder } from 'src/adapters/bidderFactory.js'; +import { config } from 'src/config.js'; + +describe('jixie Adapter', function () { + const pageurl_ = 'https://testdomain.com/testpage.html'; + const domain_ = 'https://testdomain.com'; + const device_ = 'desktop'; + const timeout_ = 1000; + const currency_ = 'USD'; + + /** + * Basic + */ + 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'); + }); + }); + + /** + * isBidRequestValid + */ + describe('isBidRequestValid', function () { + let bid = { + 'bidder': 'jixie', + 'params': { + 'unit': 'prebidsampleunit' + }, + 'adUnitCode': 'adunit-code', + 'sizes': [[300, 250], [300, 600]], + 'bidId': '30b31c1838de1e', + 'bidderRequestId': '22edbae2733bf6', + 'auctionId': '1d1a030790a475', + }; + + it('should return true when required params found', function () { + expect(spec.isBidRequestValid(bid)).to.equal(true); + }); + + it('should return false when required params obj does not exist', function () { + let bid0 = Object.assign({}, bid); + delete bid0.params; + expect(spec.isBidRequestValid(bid0)).to.equal(false); + }); + + it('should return false when params obj does not contain unit property', function () { + let bid1 = Object.assign({}, bid); + bid1.params = { rubbish: '' }; + expect(spec.isBidRequestValid(bid1)).to.equal(false); + }); + });// describe + + /** + * buildRequests + */ + describe('buildRequests', function () { + const adUnitCode0_ = 'adunit-code-0'; + const adUnitCode1_ = 'adunit-code-1'; + const adUnitCode2_ = 'adunit-code-2'; + + const bidId0_ = '22a9eb5004cf082'; + const bidId1_ = '230fceb12fd754f'; + const bidId2_ = '24dbe5c4fb80ed8'; + + const bidderRequestId_ = '2131ce076eeaa1b'; + const auctionId_ = '26d68819-d6ce-4a2c-a4d3-f1a97b159d66'; + + const clientIdTest1_ = '1aba6a40-f711-11e9-868c-53a2ae972xxx'; + const sessionIdTest1_ = '1594782644-1aba6a40-f711-11e9-868c-53a2ae972xxx'; + + // to serve as the object that prebid will call jixie buildRequest with: (param2) + const bidderRequest_ = { + refererInfo: {referer: pageurl_}, + auctionId: auctionId_, + timeout: timeout_ + }; + // to serve as the object that prebid will call jixie buildRequest with: (param1) + let bidRequests_ = [ + { + 'bidder': 'jixie', + 'params': { + 'unit': 'prebidsampleunit' + }, + 'sizes': [[300, 250], [300, 600]], + 'adUnitCode': adUnitCode0_, + 'bidId': bidId0_, + 'bidderRequestId': bidderRequestId_, + 'auctionId': auctionId_ + }, + { + 'bidder': 'jixie', + 'params': { + 'unit': 'prebidsampleunit' + }, + 'sizes': [[300, 250]], + 'mediaTypes': { + 'video': { + 'playerSize': [640, 360] + }, + 'banner': { + 'sizes': [[300, 250]] + } + }, + 'adUnitCode': adUnitCode1_, + 'bidId': bidId1_, + 'bidderRequestId': bidderRequestId_, + 'auctionId': auctionId_ + }, + { + 'bidder': 'jixie', + 'params': { + 'unit': 'prebidsampleunit' + }, + 'sizes': [[300, 250], [300, 600]], + 'mediaTypes': { + 'video': { + 'playerSize': [640, 360] + }, + 'banner': { + 'sizes': [[300, 250], [300, 600]] + } + }, + 'adUnitCode': adUnitCode2_, + 'bidId': bidId2_, + 'bidderRequestId': bidderRequestId_, + 'auctionId': auctionId_ + } + ]; + + // To serve as a reference to check against the bids array portion of the blob that the call to + // buildRequest returns + const refBids_ = [ + { + 'bidId': bidId0_, + 'adUnitCode': adUnitCode0_, + 'sizes': [[300, 250], [300, 600]], + 'params': { + 'unit': 'prebidsampleunit' + } + }, + { + 'bidId': bidId1_, + 'adUnitCode': adUnitCode1_, + 'mediaTypes': { + 'video': { + 'playerSize': [640, 360] + }, + 'banner': { + 'sizes': [[300, 250]] + } + }, + 'sizes': [[300, 250]], + 'params': { + 'unit': 'prebidsampleunit' + } + }, + { + 'bidId': bidId2_, + 'adUnitCode': adUnitCode2_, + 'mediaTypes': { + 'video': { + 'playerSize': [640, 360] + }, + 'banner': { + 'sizes': [[300, 250], [300, 600]] + } + }, + 'sizes': [[300, 250], [300, 600]], + 'params': { + 'unit': 'prebidsampleunit' + } + } + ]; + + it('should attach valid params to the adserver endpoint (1)', function () { + // this one we do not intercept the cookie stuff so really don't know + // what will be in there. so we do not check here (using expect) + // The next next below we check + const request = spec.buildRequests(bidRequests_, bidderRequest_); + it('sends bid request to ENDPOINT via POST', function () { + expect(request.method).to.equal('POST') + }) + expect(request.data).to.be.an('string'); + const payload = JSON.parse(request.data); + expect(payload).to.have.property('auctionid', auctionId_); + expect(payload).to.have.property('timeout', timeout_); + expect(payload).to.have.property('currency', currency_); + expect(payload).to.have.property('bids').that.deep.equals(refBids_); + });// it + + it('should attach valid params to the adserver endpoint (2)', function () { + // similar to above test case but here we force some clientid sessionid values + // and domain, pageurl + // get the interceptors ready: + let getCookieStub = sinon.stub(storage, 'getCookie'); + let getLocalStorageStub = sinon.stub(storage, 'getDataFromLocalStorage'); + getCookieStub + .withArgs('_jx') + .returns(clientIdTest1_); + getCookieStub + .withArgs('_jxs') + .returns(sessionIdTest1_); + getLocalStorageStub + .withArgs('_jx') + .returns(clientIdTest1_); + getLocalStorageStub + .withArgs('_jxs') + .returns(sessionIdTest1_ + ); + let miscDimsStub = sinon.stub(jixieaux, 'getMiscDims'); + miscDimsStub + .returns({ device: device_, pageurl: pageurl_, domain: domain_ }); + + // actual api call: + const request = spec.buildRequests(bidRequests_, bidderRequest_); + it('sends bid request to ENDPOINT via POST', function () { + expect(request.method).to.equal('POST') + }) + expect(request.data).to.be.an('string'); + const payload = JSON.parse(request.data); + expect(payload).to.have.property('auctionid', auctionId_); + expect(payload).to.have.property('client_id_c', clientIdTest1_); + expect(payload).to.have.property('client_id_ls', clientIdTest1_); + expect(payload).to.have.property('session_id_c', sessionIdTest1_); + expect(payload).to.have.property('session_id_ls', sessionIdTest1_); + expect(payload).to.have.property('device', device_); + expect(payload).to.have.property('domain', domain_); + expect(payload).to.have.property('pageurl', pageurl_); + expect(payload).to.have.property('timeout', timeout_); + expect(payload).to.have.property('currency', currency_); + expect(payload).to.have.property('bids').that.deep.equals(refBids_); + + // unwire interceptors + getCookieStub.restore(); + getLocalStorageStub.restore(); + miscDimsStub.restore(); + });// it + });// describe + + /** + * interpretResponse: + */ + const JX_OTHER_OUTSTREAM_RENDERER_URL = 'https://scripts.jixie.io/dummyscript.js'; + const JX_OUTSTREAM_RENDERER_URL = 'https://scripts.jixie.io/jxhboutstream.js'; + + const mockVastXml_ = `JXADSERVERAlway%20Live%20Prebid%20CreativeHybrid in-stream00:00:10`; + const responseBody_ = { + 'bids': [ + // video (vast tag url) returned here + { + 'trackingUrlBase': 'https://tr.jixie.io/sync/ad?', + 'jxBidId': '62847e4c696edcb-028d5dee-2c83-44e3-bed1-b75002475cdf', + 'requestId': '62847e4c696edcb', + 'cpm': 2.19, + 'width': 640, + 'height': 360, + 'ttl': 300, + 'adUnitCode': 'demoslot3-div', + 'netRevenue': true, + 'currency': 'USD', + 'creativeId': 'jixie522', + 'meta': { + 'networkId': 123, + 'networkName': 'network123', + 'agencyId': 123, + 'agencyName': 'agency123', + 'advertiserId': 123, + 'advertiserName': 'advertiser123', + 'brandId': 123, + 'brandName': 'brand123', + 'primaryCatId': 1, + 'secondaryCatIds': [ + 2, + 3, + 4 + ], + 'mediaType': 'VIDEO' + }, + 'vastUrl': 'https://ad.jixie.io/v1/video?creativeid=522' + }, + // display ad returned here: + { + 'trackingUrlBase': 'https://tr.jixie.io/sync/ad?', + 'jxBidId': '600c9ae6fda1acb-028d5dee-2c83-44e3-bed1-b75002475cdf', + 'requestId': '600c9ae6fda1acb', + 'cpm': 1.999, + 'width': 300, + 'height': 250, + 'ttl': 300, + 'adUnitCode': 'demoslot1-div', + 'netRevenue': true, + 'currency': 'USD', + 'creativeId': 'jixie520', + 'meta': { + 'networkId': 123, + 'networkName': 'network123', + 'agencyId': 123, + 'agencyName': 'agency123', + 'advertiserId': 123, + 'advertiserName': 'advertiser123', + 'advertiserDomains': [ + 'advdom1', + 'advdom2', + 'advdom3' + ], + 'brandId': 123, + 'brandName': 'brand123', + 'primaryCatId': 1, + 'secondaryCatIds': [ + 2, + 3, + 4 + ], + 'mediaType': 'BANNER' + }, + 'ad': '
' + }, + // outstream, jx non-default renderer specified: + { + 'trackingUrlBase': 'https://tr.jixie.io/sync/ad?', + 'jxBidId': '99bc539c81b00ce-028d5dee-2c83-44e3-bed1-b75002475cdf', + 'requestId': '99bc539c81b00ce', + 'cpm': 2.99, + 'width': 640, + 'height': 360, + 'ttl': 300, + 'netRevenue': true, + 'currency': 'USD', + 'creativeId': 'jixie521', + 'adUnitCode': 'demoslot4-div', + 'osplayer': 'jixie', + 'osparams': { + 'script': JX_OTHER_OUTSTREAM_RENDERER_URL + }, + 'vastXml': mockVastXml_ + }, + // outstream, jx default renderer: + { + 'trackingUrlBase': 'https://tr.jixie.io/sync/ad?', + 'jxBidId': '61bc539c81b00ce-028d5dee-2c83-44e3-bed1-b75002475cdf', + 'requestId': '61bc539c81b00ce', + 'cpm': 1.99, + 'width': 640, + 'height': 360, + 'ttl': 300, + 'netRevenue': true, + 'currency': 'USD', + 'creativeId': 'jixie521', + 'meta': { + 'networkId': 123, + 'networkName': 'network123', + 'agencyId': 123, + 'agencyName': 'agency123', + 'advertiserId': 123, + 'advertiserName': 'advertiser123', + 'brandId': 123, + 'brandName': 'brand123', + 'primaryCatId': 1, + 'secondaryCatIds': [ + 2, + 3, + 4 + ], + 'mediaType': 'VIDEO' + }, + 'adUnitCode': 'demoslot2-div', + 'osplayer': 'jixie', + 'osparams': {}, + 'vastXml': mockVastXml_ + } + ], + 'setids': { + 'client_id': '43aacc10-f643-11ea-8a10-c5fe2d394e7e', + 'session_id': '1600057934-43aacc10-f643-11ea-8a10-c5fe2d394e7e' + }, + }; + const requestObj_ = + { + 'method': 'POST', + 'url': 'http://localhost:8080/v2/hbpost', + 'data': '{"auctionid":"028d5dee-2c83-44e3-bed1-b75002475cdf","timeout":1000,"currency":"USD","timestamp":1600057934665,"device":"desktop","domain":"mock.com","pageurl":"https://mock.com/tests/jxprebidtest_pbjs.html","bids":[{"bidId":"600c9ae6fda1acb","adUnitCode":"demoslot1-div","mediaTypes":{"banner":{"sizes":[[300,250],[300,600],[728,90]]}},"params":{"unit":"prebidsampleunit"}},{"bidId":"61bc539c81b00ce","adUnitCode":"demoslot2-div","mediaTypes":{"video":{"playerSize":[[640,360]],"context":"outstream"}},"params":{"unit":"prebidsampleunit"}},{"bidId":"99bc539c81b00ce","adUnitCode":"demoslot4-div","mediaTypes":{"video":{"playerSize":[[640,360]],"context":"outstream"}},"params":{"unit":"prebidsampleunit"}},{"bidId":"62847e4c696edcb","adUnitCode":"demoslot3-div","mediaTypes":{"video":{"playerSize":[[640,360]],"context":"instream"}},"params":{"unit":"prebidsampleunit"}},{"bidId":"6360235ab01d2cd","adUnitCode":"woo-div","mediaTypes":{"video":{"context":"outstream","playerSize":[[640,360]]}},"params":{"unit":"80b76fc951e161d7c019d21b6639e408"}},{"bidId":"64d9724c7a5e512","adUnitCode":"test-div","mediaTypes":{"video":{"context":"outstream","playerSize":[[300,250]]}},"params":{"unit":"80b76fc951e161d7c019d21b6639e408"}},{"bidId":"65bea7e80fed44b","adUnitCode":"test-div","mediaTypes":{"banner":{"sizes":[[300,250],[300,600],[728,90]]}},"params":{"unit":"7854f723e932b951b6c51fc80b23a410"}},{"bidId":"6642054c4ba1b7f","adUnitCode":"div-banner-native-1","mediaTypes":{"banner":{"sizes":[[640,360]]},"video":{"context":"outstream","sizes":[[640,361]],"playerSize":[[640,360]]},"native":{"type":"image"}},"params":{"unit":"632e7695f0910ce0fa74c19859060a04"}},{"bidId":"675ecf4b44db228","adUnitCode":"div-banner-native-2","mediaTypes":{"banner":{"sizes":[[300,250]]},"native":{"title":{"required":true},"image":{"required":true},"sponsoredBy":{"required":true}}},"params":{"unit":"1000008-b1Q2UMQfZx"}},{"bidId":"68f2dbf5dc23f94","adUnitCode":"div-Top-MediumRectangle","mediaTypes":{"banner":{"sizes":[[300,250],[300,100],[320,50]]}},"params":{"unit":"1000008-b1Q2UMQfZx"}},{"bidId":"6991cf107bb7f1a","adUnitCode":"div-Middle-MediumRectangle","mediaTypes":{"banner":{"sizes":[[300,250],[300,100],[320,50]]}},"params":{"unit":"1000008-b1Q2UMQfZx"}},{"bidId":"706be1b011eac83","adUnitCode":"div-Inside-MediumRectangle","mediaTypes":{"banner":{"sizes":[[300,600],[300,250],[300,100],[320,480]]}},"params":{"unit":"1000008-b1Q2UMQfZx"}}],"client_id_c":"ebd0dea0-f5c8-11ea-a2c7-a5b37aa7fe95","client_id_ls":"ebd0dea0-f5c8-11ea-a2c7-a5b37aa7fe95","session_id_c":"","session_id_ls":"1600005388-ebd0dea0-f5c8-11ea-a2c7-a5b37aa7fe95"}', + 'currency': 'USD' + }; + + 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('should get correct bid response', function () { + let setCookieSpy = sinon.spy(storage, 'setCookie'); + let setLocalStorageSpy = sinon.spy(storage, 'setDataInLocalStorage'); + const result = spec.interpretResponse({body: responseBody_}, requestObj_) + expect(setLocalStorageSpy.calledWith('_jx', '43aacc10-f643-11ea-8a10-c5fe2d394e7e')).to.equal(true); + expect(setLocalStorageSpy.calledWith('_jxs', '1600057934-43aacc10-f643-11ea-8a10-c5fe2d394e7e')).to.equal(true); + expect(setCookieSpy.calledWith('_jxs', '1600057934-43aacc10-f643-11ea-8a10-c5fe2d394e7e')).to.equal(true); + expect(setCookieSpy.calledWith('_jx', '43aacc10-f643-11ea-8a10-c5fe2d394e7e')).to.equal(true); + + // video ad with vastUrl returned by adserver + expect(result[0].requestId).to.equal('62847e4c696edcb') + expect(result[0].cpm).to.equal(2.19) + expect(result[0].width).to.equal(640) + expect(result[0].height).to.equal(360) + expect(result[0].creativeId).to.equal('jixie522') + expect(result[0].currency).to.equal('USD') + expect(result[0].netRevenue).to.equal(true) + expect(result[0].ttl).to.equal(300) + expect(result[0].vastUrl).to.include('https://ad.jixie.io/v1/video?creativeid=') + expect(result[0].trackingUrlBase).to.include('sync') + + // display ad + expect(result[1].requestId).to.equal('600c9ae6fda1acb') + expect(result[1].cpm).to.equal(1.999) + expect(result[1].width).to.equal(300) + expect(result[1].height).to.equal(250) + expect(result[1].creativeId).to.equal('jixie520') + expect(result[1].currency).to.equal('USD') + expect(result[1].netRevenue).to.equal(true) + expect(result[1].ttl).to.equal(300) + expect(result[1].ad).to.include('jxoutstream') + expect(result[1].trackingUrlBase).to.include('sync') + + // should pick up about using alternative outstream renderer + expect(result[2].requestId).to.equal('99bc539c81b00ce') + expect(result[2].cpm).to.equal(2.99) + expect(result[2].width).to.equal(640) + expect(result[2].height).to.equal(360) + expect(result[2].creativeId).to.equal('jixie521') + expect(result[2].currency).to.equal('USD') + expect(result[2].netRevenue).to.equal(true) + expect(result[2].ttl).to.equal(300) + expect(result[2].vastXml).to.include('') + expect(result[2].trackingUrlBase).to.include('sync'); + expect(result[2].renderer.id).to.equal('demoslot4-div') + expect(result[2].renderer.url).to.equal(JX_OTHER_OUTSTREAM_RENDERER_URL); + + // should know to use default outstream renderer + expect(result[3].requestId).to.equal('61bc539c81b00ce') + expect(result[3].cpm).to.equal(1.99) + expect(result[3].width).to.equal(640) + expect(result[3].height).to.equal(360) + expect(result[3].creativeId).to.equal('jixie521') + expect(result[3].currency).to.equal('USD') + expect(result[3].netRevenue).to.equal(true) + expect(result[3].ttl).to.equal(300) + expect(result[3].vastXml).to.include('') + expect(result[3].trackingUrlBase).to.include('sync'); + expect(result[3].renderer.id).to.equal('demoslot2-div') + expect(result[3].renderer.url).to.equal(JX_OUTSTREAM_RENDERER_URL) + + setLocalStorageSpy.restore(); + setCookieSpy.restore(); + });// it + });// describe + + /** + * onBidWon + */ + describe('onBidWon', function() { + let ajaxStub; + let miscDimsStub; + beforeEach(function() { + miscDimsStub = sinon.stub(jixieaux, 'getMiscDims'); + ajaxStub = sinon.stub(jixieaux, 'ajax'); + + miscDimsStub + .returns({ device: device_, pageurl: pageurl_, domain: domain_ }); + }) + + afterEach(function() { + miscDimsStub.restore(); + ajaxStub.restore(); + }) + + let TRACKINGURL_ = 'https://abc.com/sync?action=bidwon'; + + it('Should fire if the adserver trackingUrl flag says so', function() { + spec.onBidWon({ trackingUrl: TRACKINGURL_ }) + expect(jixieaux.ajax.calledWith(TRACKINGURL_)).to.equal(true); + }) + + it('Should not fire if the adserver response indicates no firing', function() { + let called = false; + ajaxStub.callsFake(function fakeFn() { + called = true; + }); + spec.onBidWon({ notrack: 1 }) + expect(called).to.equal(false); + }); + + // A reference to check again: + const QPARAMS_ = { + action: 'hbbidwon', + device: device_, + pageurl: encodeURIComponent(pageurl_), + domain: encodeURIComponent(domain_), + cid: 121, + cpid: 99, + jxbidid: '62847e4c696edcb-028d5dee-2c83-44e3-bed1-b75002475cdf', + auctionid: '028d5dee-2c83-44e3-bed1-b75002475cdf', + cpm: 1.11, + requestid: '62847e4c696edcb' + }; + + it('check it is sending the correct ajax url and qparameters', function() { + spec.onBidWon({ + trackingUrlBase: 'https://mytracker.com/sync?', + cid: 121, + cpid: 99, + jxBidId: '62847e4c696edcb-028d5dee-2c83-44e3-bed1-b75002475cdf', + auctionId: '028d5dee-2c83-44e3-bed1-b75002475cdf', + cpm: 1.11, + requestId: '62847e4c696edcb' + }) + expect(jixieaux.ajax.calledWith('https://mytracker.com/sync?', null, QPARAMS_)).to.equal(true); + }); + }); // describe + + /** + * onTimeout + */ + describe('onTimeout', function() { + let ajaxStub; + let miscDimsStub; + beforeEach(function() { + ajaxStub = sinon.stub(jixieaux, 'ajax'); + miscDimsStub = sinon.stub(jixieaux, 'getMiscDims'); + miscDimsStub + .returns({ device: device_, pageurl: pageurl_, domain: domain_ }); + }) + + afterEach(function() { + miscDimsStub.restore(); + ajaxStub.restore(); + }) + + // reference to check against: + const QPARAMS_ = { + action: 'hbtimeout', + device: device_, + pageurl: encodeURIComponent(pageurl_), + domain: encodeURIComponent(domain_), + auctionid: '028d5dee-2c83-44e3-bed1-b75002475cdf', + timeout: 1000, + count: 2 + }; + + it('check it is sending the correct ajax url and qparameters', function() { + spec.onTimeout([ + {auctionId: '028d5dee-2c83-44e3-bed1-b75002475cdf', timeout: 1000}, + {auctionId: '028d5dee-2c83-44e3-bed1-b75002475cdf', timeout: 1000} + ]) + expect(jixieaux.ajax.calledWith(spec.EVENTS_URL, null, QPARAMS_)).to.equal(true); + }) + + it('if turned off via config then dont do onTimeout sending of event', function() { + let getConfigStub = sinon.stub(config, 'getConfig'); + getConfigStub.callsFake(function fakeFn(prop) { + if (prop == 'jixie') { + return { onTimeout: 'off' }; + } + return null; + }); + let called = false; + ajaxStub.callsFake(function fakeFn() { + called = true; + }); + spec.onTimeout([ + {auctionId: '028d5dee-2c83-44e3-bed1-b75002475cdf', timeout: 1000}, + {auctionId: '028d5dee-2c83-44e3-bed1-b75002475cdf', timeout: 1000} + ]) + expect(called).to.equal(false); + getConfigStub.restore(); + }) + + const otherUrl_ = 'https://other.azurewebsites.net/sync/evt?'; + it('if config specifies a different endpoint then should send there instead', function() { + let getConfigStub = sinon.stub(config, 'getConfig'); + getConfigStub.callsFake(function fakeFn(prop) { + if (prop == 'jixie') { + return { onTimeoutUrl: otherUrl_ }; + } + return null; + }); + spec.onTimeout([ + {auctionId: '028d5dee-2c83-44e3-bed1-b75002475cdf', timeout: 1000}, + {auctionId: '028d5dee-2c83-44e3-bed1-b75002475cdf', timeout: 1000} + ]) + expect(jixieaux.ajax.calledWith(otherUrl_, null, QPARAMS_)).to.equal(true); + getConfigStub.restore(); + }) + });// describe +}); diff --git a/test/spec/modules/justpremiumBidAdapter_spec.js b/test/spec/modules/justpremiumBidAdapter_spec.js index c162b785aed..7cc154254ff 100644 --- a/test/spec/modules/justpremiumBidAdapter_spec.js +++ b/test/spec/modules/justpremiumBidAdapter_spec.js @@ -21,7 +21,9 @@ describe('justpremium adapter', function () { }, userId: { tdid: '1111111', - id5id: '2222222', + id5id: { + uid: '2222222' + }, digitrustid: { data: { id: '3333333' @@ -84,7 +86,7 @@ describe('justpremium adapter', function () { expect(jpxRequest.version.jp_adapter).to.equal('1.7') expect(jpxRequest.pubcid).to.equal('0000000') expect(jpxRequest.uids.tdid).to.equal('1111111') - expect(jpxRequest.uids.id5id).to.equal('2222222') + expect(jpxRequest.uids.id5id.uid).to.equal('2222222') expect(jpxRequest.uids.digitrustid.data.id).to.equal('3333333') expect(jpxRequest.us_privacy).to.equal('1YYN') }) diff --git a/test/spec/modules/jwplayerRtdProvider_spec.js b/test/spec/modules/jwplayerRtdProvider_spec.js new file mode 100644 index 00000000000..458e45e8ae7 --- /dev/null +++ b/test/spec/modules/jwplayerRtdProvider_spec.js @@ -0,0 +1,751 @@ +import { fetchTargetingForMediaId, getVatFromCache, extractPublisherParams, + formatTargetingResponse, getVatFromPlayer, enrichAdUnits, addTargetingToBid, + fetchTargetingInformation, jwplayerSubmodule } from 'modules/jwplayerRtdProvider.js'; +import { server } from 'test/mocks/xhr.js'; + +describe('jwplayerRtdProvider', function() { + const testIdForSuccess = 'test_id_for_success'; + const testIdForFailure = 'test_id_for_failure'; + const validSegments = ['test_seg_1', 'test_seg_2']; + const responseHeader = {'Content-Type': 'application/json'}; + + describe('Fetch targeting for mediaID tests', function () { + let request; + + describe('Fetch succeeds', function () { + beforeEach(function () { + fetchTargetingForMediaId(testIdForSuccess); + request = server.requests[0]; + }); + + afterEach(function () { + server.respond(); + }); + + it('should reach out to media endpoint', function () { + expect(request.url).to.be.eq(`https://cdn.jwplayer.com/v2/media/${testIdForSuccess}`); + }); + + it('should write to cache when successful', function () { + request.respond( + 200, + responseHeader, + JSON.stringify({ + playlist: [ + { + file: 'test.mp4', + jwpseg: validSegments + } + ] + }) + ); + + const targetingInfo = getVatFromCache(testIdForSuccess); + + const validTargeting = { + segments: validSegments, + mediaID: testIdForSuccess + }; + + expect(targetingInfo).to.deep.equal(validTargeting); + }); + }); + + describe('Fetch fails', function () { + beforeEach(function () { + fetchTargetingForMediaId(testIdForFailure); + request = server.requests[0] + }); + + it('should not write to cache when response is malformed', function() { + request.respond('{]'); + const targetingInfo = getVatFromCache(testIdForFailure); + expect(targetingInfo).to.be.null; + }); + + it('should not write to cache when playlist is absent', function() { + request.respond({}); + const targetingInfo = getVatFromCache(testIdForFailure); + expect(targetingInfo).to.be.null; + }); + + it('should not write to cache when segments are absent', function() { + request.respond( + 200, + responseHeader, + JSON.stringify({ + playlist: [ + { + file: 'test.mp4' + } + ] + }) + ); + const targetingInfo = getVatFromCache(testIdForFailure); + expect(targetingInfo).to.be.null; + }); + + it('should not write to cache when request errors', function() { + request.error(); + const targetingInfo = getVatFromCache(testIdForFailure); + expect(targetingInfo).to.be.null; + }); + }); + }); + + describe('Format targeting response', function () { + it('should exclude segment key when absent', function () { + const targeting = formatTargetingResponse({ mediaID: 'test' }); + expect(targeting).to.not.have.property('segments'); + }); + + it('should exclude content block when mediaId is absent', function () { + const targeting = formatTargetingResponse({ segments: ['test'] }); + expect(targeting).to.not.have.property('content'); + }); + + it('should return proper format', function () { + const segments = ['123']; + const mediaID = 'test'; + const expectedContentId = 'jw_' + mediaID; + const expectedContent = { + id: expectedContentId + }; + const targeting = formatTargetingResponse({ + segments, + mediaID + }); + expect(targeting).to.have.deep.property('segments', segments); + expect(targeting).to.have.deep.property('content', expectedContent); + }); + }); + + describe('Get VAT from player', function () { + const mediaIdWithSegment = 'media_ID_1'; + const mediaIdNoSegment = 'media_ID_2'; + const mediaIdForCurrentItem = 'media_ID_current'; + const mediaIdNotCached = 'media_test_ID'; + + const validPlayerID = 'player_test_ID_valid'; + const invalidPlayerID = 'player_test_ID_invalid'; + + it('returns null when jwplayer.js is absent from page', function () { + const targeting = getVatFromPlayer(invalidPlayerID, mediaIdNotCached); + expect(targeting).to.be.null; + }); + + describe('When jwplayer.js is on page', function () { + const playlistItemWithSegmentMock = { + mediaid: mediaIdWithSegment, + jwpseg: validSegments + }; + + const targetingForMediaWithSegment = { + segments: validSegments, + mediaID: mediaIdWithSegment + }; + + const playlistItemNoSegmentMock = { + mediaid: mediaIdNoSegment + }; + + const currentItemSegments = ['test_seg_3', 'test_seg_4']; + const currentPlaylistItemMock = { + mediaid: mediaIdForCurrentItem, + jwpseg: currentItemSegments + }; + const targetingForCurrentItem = { + segments: currentItemSegments, + mediaID: mediaIdForCurrentItem + }; + + const playerInstanceMock = { + getPlaylist: function () { + return [playlistItemWithSegmentMock, playlistItemNoSegmentMock]; + }, + + getPlaylistItem: function () { + return currentPlaylistItemMock; + } + }; + + const jwplayerMock = function(playerID) { + if (playerID === validPlayerID) { + return playerInstanceMock; + } else { + return {}; + } + }; + + beforeEach(function () { + window.jwplayer = jwplayerMock; + }); + + it('returns null when player ID does not match player on page', function () { + const targeting = getVatFromPlayer(invalidPlayerID, mediaIdNotCached); + expect(targeting).to.be.null; + }); + + it('returns segments when media ID matches a playlist item with segments', function () { + const targeting = getVatFromPlayer(validPlayerID, mediaIdWithSegment); + expect(targeting).to.deep.equal(targetingForMediaWithSegment); + }); + + it('caches segments when media ID matches a playist item with segments', function () { + getVatFromPlayer(validPlayerID, mediaIdWithSegment); + const vat = getVatFromCache(mediaIdWithSegment); + expect(vat.segments).to.deep.equal(validSegments); + }); + + it('returns segments of current item when media ID is missing', function () { + const targeting = getVatFromPlayer(validPlayerID); + expect(targeting).to.deep.equal(targetingForCurrentItem); + }); + + it('caches segments from the current item', function () { + getVatFromPlayer(validPlayerID); + + window.jwplayer = null; + const targeting = getVatFromCache(mediaIdForCurrentItem); + expect(targeting).to.deep.equal(targetingForCurrentItem); + }); + + it('returns undefined segments when segments are absent', function () { + const targeting = getVatFromPlayer(validPlayerID, mediaIdNoSegment); + expect(targeting).to.deep.equal({ + mediaID: mediaIdNoSegment, + segments: undefined + }); + }); + + describe('Get Bid Request Data', function () { + it('executes immediately while request is active if player has item', function () { + const bidRequestSpy = sinon.spy(); + const fakeServer = sinon.createFakeServer(); + fakeServer.respondImmediately = false; + fakeServer.autoRespond = false; + + fetchTargetingForMediaId(mediaIdWithSegment); + + const bid = {}; + const adUnit = { + ortb2Imp: { + ext: { + data: { + jwTargeting: { + mediaID: mediaIdWithSegment, + playerID: validPlayerID + } + } + } + }, + bids: [ + bid + ] + }; + const expectedContentId = 'jw_' + mediaIdWithSegment; + const expectedTargeting = { + segments: validSegments, + content: { + id: expectedContentId + } + }; + jwplayerSubmodule.getBidRequestData({ adUnits: [adUnit] }, bidRequestSpy); + expect(bidRequestSpy.calledOnce).to.be.true; + expect(bid.rtd.jwplayer).to.have.deep.property('targeting', expectedTargeting); + fakeServer.respond(); + expect(bidRequestSpy.calledOnce).to.be.true; + }); + }); + }); + }); + + describe('Enrich ad units', function () { + const contentIdForSuccess = 'jw_' + testIdForSuccess; + const expectedTargetingForSuccess = { + segments: validSegments, + content: { + id: contentIdForSuccess + } + }; + let bidRequestSpy; + let fakeServer; + let clock; + + beforeEach(function () { + bidRequestSpy = sinon.spy(); + + fakeServer = sinon.createFakeServer(); + fakeServer.respondImmediately = false; + fakeServer.autoRespond = false; + + clock = sinon.useFakeTimers(); + }); + + afterEach(function () { + clock.restore(); + fakeServer.respond(); + }); + + it('adds targeting when pending request succeeds', function () { + fetchTargetingForMediaId(testIdForSuccess); + const bids = [ + { + id: 'bid1' + }, + { + id: 'bid2' + } + ]; + const adUnit = { + ortb2Imp: { + ext: { + data: { + jwTargeting: { + mediaID: testIdForSuccess + } + } + } + }, + bids + }; + + enrichAdUnits([adUnit]); + const bid1 = bids[0]; + const bid2 = bids[1]; + expect(bid1).to.not.have.property('rtd'); + expect(bid2).to.not.have.property('rtd'); + + const request = fakeServer.requests[0]; + request.respond( + 200, + responseHeader, + JSON.stringify({ + playlist: [ + { + file: 'test.mp4', + jwpseg: validSegments + } + ] + }) + ); + + expect(bid1.rtd.jwplayer).to.have.deep.property('targeting', expectedTargetingForSuccess); + expect(bid2.rtd.jwplayer).to.have.deep.property('targeting', expectedTargetingForSuccess); + }); + + it('immediately adds cached targeting', function () { + fetchTargetingForMediaId(testIdForSuccess); + const bids = [ + { + id: 'bid1' + }, + { + id: 'bid2' + } + ]; + const adUnit = { + ortb2Imp: { + ext: { + data: { + jwTargeting: { + mediaID: testIdForSuccess + } + } + } + }, + bids + }; + const request = fakeServer.requests[0]; + request.respond( + 200, + responseHeader, + JSON.stringify({ + playlist: [ + { + file: 'test.mp4', + jwpseg: validSegments + } + ] + }) + ); + + enrichAdUnits([adUnit]); + const bid1 = bids[0]; + const bid2 = bids[1]; + expect(bid1.rtd.jwplayer).to.have.deep.property('targeting', expectedTargetingForSuccess); + expect(bid2.rtd.jwplayer).to.have.deep.property('targeting', expectedTargetingForSuccess); + }); + + it('adds content block when segments are absent and no request is pending', function () { + const expectedTargetingForFailure = { + content: { + id: 'jw_' + testIdForFailure + } + }; + const bids = [ + { + id: 'bid1' + }, + { + id: 'bid2' + } + ]; + const adUnit = { + ortb2Imp: { + ext: { + data: { + jwTargeting: { + mediaID: testIdForFailure + } + } + } + }, + bids + }; + + enrichAdUnits([adUnit]); + const bid1 = bids[0]; + const bid2 = bids[1]; + expect(bid1.rtd.jwplayer).to.have.deep.property('targeting', expectedTargetingForFailure); + expect(bid2.rtd.jwplayer).to.have.deep.property('targeting', expectedTargetingForFailure); + }); + }); + + describe(' Extract Publisher Params', function () { + const config = { mediaID: 'test' }; + + it('should exclude adUnits that do not support instream video and do not specify jwTargeting', function () { + const oustreamAdUnit = { mediaTypes: { video: { context: 'outstream' } } }; + const oustreamTargeting = extractPublisherParams(oustreamAdUnit, config); + expect(oustreamTargeting).to.be.undefined; + + const bannerAdUnit = { mediaTypes: { banner: {} } }; + const bannerTargeting = extractPublisherParams(bannerAdUnit, config); + expect(bannerTargeting).to.be.undefined; + + 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: {} }, ortb2Imp: { ext: { 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' } }, ortb2Imp: { ext: { 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 = { ortb2Imp: { ext: { data: { jwTargeting: {} } } } }; + const targeting = extractPublisherParams(adUnit, config); + expect(targeting).to.deep.equal(config); + }); + + it('should prioritize adUnit properties ', function () { + const expectedMediaID = 'test_media_id'; + const expectedPlayerID = 'test_player_id'; + const config = { playerID: 'bad_id', mediaID: 'bad_id' }; + + const adUnit = { ortb2Imp: { ext: { data: { jwTargeting: { mediaID: expectedMediaID, playerID: expectedPlayerID } } } } }; + const targeting = extractPublisherParams(adUnit, config); + expect(targeting).to.have.property('mediaID', expectedMediaID); + expect(targeting).to.have.property('playerID', expectedPlayerID); + }); + + it('should use config properties as fallbacks', function () { + const expectedMediaID = 'test_media_id'; + const expectedPlayerID = 'test_player_id'; + const config = { playerID: expectedPlayerID, mediaID: 'bad_id' }; + + const adUnit = { ortb2Imp: { ext: { data: { jwTargeting: { mediaID: expectedMediaID } } } } }; + const targeting = extractPublisherParams(adUnit, config); + expect(targeting).to.have.property('mediaID', expectedMediaID); + expect(targeting).to.have.property('playerID', expectedPlayerID); + }); + + it('should return undefined when Publisher Params are absent', function () { + const targeting = extractPublisherParams({}, null); + expect(targeting).to.be.undefined; + }) + }); + + describe('Add Targeting to Bid', function () { + const targeting = {foo: 'bar'}; + + it('creates realTimeData when absent from Bid', function () { + const targeting = {foo: 'bar'}; + const bid = {}; + addTargetingToBid(bid, targeting); + expect(bid).to.have.property('rtd'); + expect(bid).to.have.nested.property('rtd.jwplayer.targeting', targeting); + }); + + it('adds to existing realTimeData', function () { + const otherRtd = { + targeting: { + seg: 'rtd seg' + } + }; + + const bid = { + rtd: { + otherRtd + } + }; + + addTargetingToBid(bid, targeting); + expect(bid).to.have.property('rtd'); + const rtd = bid.rtd; + expect(rtd).to.have.property('jwplayer'); + expect(rtd).to.have.nested.property('jwplayer.targeting', targeting); + + expect(rtd).to.have.deep.property('otherRtd', otherRtd); + }); + + it('adds to existing realTimeData.jwplayer', function () { + const otherInfo = { seg: 'rtd seg' }; + const bid = { + rtd: { + jwplayer: { + otherInfo + } + } + }; + addTargetingToBid(bid, targeting); + + expect(bid).to.have.property('rtd'); + const rtd = bid.rtd; + expect(rtd).to.have.property('jwplayer'); + expect(rtd).to.have.nested.property('jwplayer.otherInfo', otherInfo); + expect(rtd).to.have.nested.property('jwplayer.targeting', targeting); + }); + + it('overrides existing jwplayer.targeting', function () { + const otherInfo = { seg: 'rtd seg' }; + const bid = { + rtd: { + jwplayer: { + targeting: { + otherInfo + } + } + } + }; + addTargetingToBid(bid, targeting); + + expect(bid).to.have.property('rtd'); + const rtd = bid.rtd; + expect(rtd).to.have.property('jwplayer'); + expect(rtd).to.have.nested.property('jwplayer.targeting', targeting); + }); + + it('creates jwplayer when absent from realTimeData', function () { + const bid = { rtd: {} }; + addTargetingToBid(bid, targeting); + + expect(bid).to.have.property('rtd'); + const rtd = bid.rtd; + expect(rtd).to.have.property('jwplayer'); + expect(rtd).to.have.nested.property('jwplayer.targeting', targeting); + }); + }); + + describe('jwplayerSubmodule', function () { + it('successfully instantiates', function () { + expect(jwplayerSubmodule.init()).to.equal(true); + }); + + describe('Get Bid Request Data', function () { + const validMediaIDs = ['media_ID_1', 'media_ID_2', 'media_ID_3']; + let bidRequestSpy; + let fakeServer; + let clock; + let bidReqConfig; + + beforeEach(function () { + bidReqConfig = { + adUnits: [ + { + ortb2Imp: { + ext: { + data: { + jwTargeting: { + mediaID: validMediaIDs[0] + } + } + } + }, + bids: [ + {}, {} + ] + }, + { + ortb2Imp: { + ext: { + data: { + jwTargeting: { + mediaID: validMediaIDs[1] + } + } + } + }, + bids: [ + {}, {} + ] + } + ] + }; + + bidRequestSpy = sinon.spy(); + + fakeServer = sinon.createFakeServer(); + fakeServer.respondImmediately = false; + fakeServer.autoRespond = false; + + clock = sinon.useFakeTimers(); + }); + + afterEach(function () { + clock.restore(); + fakeServer.respond(); + }); + + it('executes callback immediately when ad units are missing', function () { + jwplayerSubmodule.getBidRequestData({ adUnits: [] }, bidRequestSpy); + expect(bidRequestSpy.calledOnce).to.be.true; + }); + + it('executes callback immediately when no requests are pending', function () { + fetchTargetingInformation({ + mediaIDs: [] + }); + jwplayerSubmodule.getBidRequestData(bidReqConfig, bidRequestSpy); + expect(bidRequestSpy.calledOnce).to.be.true; + }); + + it('executes callback only after requests in adUnit complete', function() { + fetchTargetingInformation({ + mediaIDs: validMediaIDs + }); + jwplayerSubmodule.getBidRequestData(bidReqConfig, bidRequestSpy); + expect(bidRequestSpy.notCalled).to.be.true; + + const req1 = fakeServer.requests[0]; + const req2 = fakeServer.requests[1]; + const req3 = fakeServer.requests[2]; + + req1.respond(); + expect(bidRequestSpy.notCalled).to.be.true; + + req2.respond(); + expect(bidRequestSpy.calledOnce).to.be.true; + + req3.respond(); + expect(bidRequestSpy.calledOnce).to.be.true; + }); + + it('sets targeting data in proper structure', function () { + const bid = {}; + const adUnitWithMediaId = { + ortb2Imp: { + ext: { + data: { + jwTargeting: { + mediaID: testIdForSuccess + } + } + } + }, + bids: [ + bid + ] + }; + const adUnitEmpty = { + code: 'test_ad_unit_empty' + }; + const expectedContentId = 'jw_' + testIdForSuccess; + const expectedTargeting = { + segments: validSegments, + content: { + id: expectedContentId + } + }; + jwplayerSubmodule.getBidRequestData({ adUnits: [adUnitWithMediaId, adUnitEmpty] }, bidRequestSpy); + expect(bidRequestSpy.calledOnce).to.be.true; + expect(bid.rtd.jwplayer).to.have.deep.property('targeting', expectedTargeting); + }); + + it('excludes segments when absent', function () { + const adUnitCode = 'test_ad_unit'; + const bid = {}; + const adUnit = { + ortb2Imp: { + ext: { + data: { + jwTargeting: { + mediaID: testIdForFailure + } + } + } + }, + bids: [ bid ] + }; + const expectedContentId = 'jw_' + adUnit.ortb2Imp.ext.data.jwTargeting.mediaID; + const expectedTargeting = { + content: { + id: expectedContentId + } + }; + + jwplayerSubmodule.getBidRequestData({ adUnits: [ adUnit ] }, bidRequestSpy); + expect(bidRequestSpy.calledOnce).to.be.true; + expect(bid.rtd.jwplayer.targeting).to.not.have.property('segments'); + expect(bid.rtd.jwplayer.targeting).to.not.have.property('segments'); + expect(bid.rtd.jwplayer).to.have.deep.property('targeting', expectedTargeting); + }); + + it('does not modify bid when jwTargeting block is absent', function () { + const adUnitCode = 'test_ad_unit'; + const bid1 = {}; + const bid2 = {}; + const bid3 = {}; + const adUnitWithMediaId = { + code: adUnitCode, + mediaID: testIdForSuccess, + bids: [ bid1 ] + }; + const adUnitEmpty = { + code: 'test_ad_unit_empty', + bids: [ bid2 ] + }; + + const adUnitEmptyfpd = { + code: 'test_ad_unit_empty_fpd', + ortb2Imp: { + ext: { + id: 'sthg' + } + }, + bids: [ bid3 ] + }; + + jwplayerSubmodule.getBidRequestData({ adUnits: [adUnitWithMediaId, adUnitEmpty, adUnitEmptyfpd] }, bidRequestSpy); + expect(bidRequestSpy.calledOnce).to.be.true; + expect(bid1).to.not.have.property('rtd'); + expect(bid2).to.not.have.property('rtd'); + expect(bid3).to.not.have.property('rtd'); + }); + }); + }); +}); diff --git a/test/spec/modules/kargoBidAdapter_spec.js b/test/spec/modules/kargoBidAdapter_spec.js index be73e140839..43968bbef5a 100644 --- a/test/spec/modules/kargoBidAdapter_spec.js +++ b/test/spec/modules/kargoBidAdapter_spec.js @@ -134,6 +134,21 @@ describe('kargo adapter tests', function () { noAdServerCurrency = true; } + function generateGDPR(applies, haveConsent) { + var data = { + consentString: 'gdprconsentstring', + gdprApplies: applies, + }; + return data; + } + + function generateGDPRExpect(applies, haveConsent) { + return { + consent: 'gdprconsentstring', + applies: applies, + }; + } + function initializeKruxUser() { setLocalStorageItem('kxkar_user', 'rsgr9pnij'); } @@ -221,7 +236,7 @@ describe('kargo adapter tests', function () { return spec._getSessionId(); } - function getExpectedKrakenParams(excludeUserIds, excludeKrux, expectedRawCRB, expectedRawCRBCookie) { + function getExpectedKrakenParams(excludeUserIds, excludeKrux, expectedRawCRB, expectedRawCRBCookie, expectedGDPR) { var base = { timeout: 200, requestCount: requestCount++, @@ -299,6 +314,10 @@ describe('kargo adapter tests', function () { rawCRBLocalStorage: expectedRawCRB }; + if (expectedGDPR) { + base.userIDs['gdpr'] = expectedGDPR; + } + if (excludeUserIds === true) { base.userIDs = { crbIDs: {}, @@ -317,12 +336,16 @@ describe('kargo adapter tests', function () { return base; } - function testBuildRequests(excludeTdid, expected) { + function testBuildRequests(excludeTdid, expected, gdpr) { var clonedBids = JSON.parse(JSON.stringify(bids)); if (excludeTdid) { delete clonedBids[0].userId.tdid; } - var request = spec.buildRequests(clonedBids, {timeout: 200, uspConsent: '1---', foo: 'bar'}); + var payload = { timeout: 200, uspConsent: '1---', foo: 'bar' }; + if (gdpr) { + payload['gdprConsent'] = gdpr + } + var request = spec.buildRequests(clonedBids, payload); expected.sessionId = getSessionId(); sessionIds.push(expected.sessionId); var krakenParams = JSON.parse(decodeURIComponent(request.data.slice(5))); @@ -431,6 +454,15 @@ describe('kargo adapter tests', function () { initializeKrgCrb(); testBuildRequests(false, getExpectedKrakenParams(undefined, undefined, getKrgCrb(), getKrgCrbOldStyle())); }); + + it('sends gdpr consent', function () { + initializeKruxUser(); + initializeKruxSegments(); + initializeKrgCrb(); + testBuildRequests(false, getExpectedKrakenParams(undefined, undefined, getKrgCrb(), getKrgCrbOldStyle(), generateGDPRExpect(true, true)), generateGDPR(true, true)); + testBuildRequests(false, getExpectedKrakenParams(undefined, undefined, getKrgCrb(), getKrgCrbOldStyle(), generateGDPRExpect(false, true)), generateGDPR(false, true)); + testBuildRequests(false, getExpectedKrakenParams(undefined, undefined, getKrgCrb(), getKrgCrbOldStyle(), generateGDPRExpect(false, false)), generateGDPR(false, false)); + }); }); describe('response handler', function() { @@ -505,7 +537,8 @@ describe('kargo adapter tests', function () { netRevenue: true, currency: 'USD', meta: { - clickUrl: 'https://foobar.com' + clickUrl: 'https://foobar.com', + advertiserDomains: ['https://foobar.com'] } }, { requestId: '3', @@ -557,8 +590,8 @@ describe('kargo adapter tests', function () { }); }); - function getUserSyncsWhenAllowed() { - return spec.getUserSyncs({iframeEnabled: true}); + function getUserSyncsWhenAllowed(gdprConsent, usPrivacy) { + return spec.getUserSyncs({iframeEnabled: true}, null, gdprConsent, usPrivacy); } function getUserSyncsWhenForbidden() { @@ -573,17 +606,17 @@ describe('kargo adapter tests', function () { shouldSimulateOutdatedBrowser = true; } - function getSyncUrl(index) { + function getSyncUrl(index, gdprApplies, gdprConsentString, usPrivacy) { return { type: 'iframe', - url: `https://crb.kargo.com/api/v1/initsyncrnd/${clientId}?seed=3205e885-8d37-4139-b47e-f82cff268000&idx=${index}` + url: `https://crb.kargo.com/api/v1/initsyncrnd/${clientId}?seed=3205e885-8d37-4139-b47e-f82cff268000&idx=${index}&gdpr=${gdprApplies}&gdpr_consent=${gdprConsentString}&us_privacy=${usPrivacy}` }; } - function getSyncUrls() { + function getSyncUrls(gdprApplies, gdprConsentString, usPrivacy) { var syncs = []; for (var i = 0; i < 5; i++) { - syncs[i] = getSyncUrl(i); + syncs[i] = getSyncUrl(i, gdprApplies || 0, gdprConsentString || '', usPrivacy || ''); } return syncs; } @@ -605,6 +638,21 @@ describe('kargo adapter tests', function () { safelyRun(() => expect(getUserSyncsWhenAllowed()).to.be.an('array').that.is.empty); }); + it('no user syncs when there is no us privacy consent', function() { + turnOnClientId(); + safelyRun(() => expect(getUserSyncsWhenAllowed(null, '1YYY')).to.be.an('array').that.is.empty); + }); + + it('pass through us privacy consent', function() { + turnOnClientId(); + safelyRun(() => expect(getUserSyncsWhenAllowed(null, '1YNY')).to.deep.equal(getSyncUrls(0, '', '1YNY'))); + }); + + it('pass through gdpr consent', function() { + turnOnClientId(); + safelyRun(() => expect(getUserSyncsWhenAllowed({ gdprApplies: true, consentString: 'consentstring' })).to.deep.equal(getSyncUrls(1, 'consentstring', ''))); + }); + it('no user syncs when there is outdated browser', function() { turnOnClientId(); simulateOutdatedBrowser(); diff --git a/test/spec/modules/koblerBidAdapter_spec.js b/test/spec/modules/koblerBidAdapter_spec.js new file mode 100644 index 00000000000..725c9ece118 --- /dev/null +++ b/test/spec/modules/koblerBidAdapter_spec.js @@ -0,0 +1,694 @@ +import {expect} from 'chai'; +import {spec} from 'modules/koblerBidAdapter.js'; +import {newBidder} from 'src/adapters/bidderFactory.js'; +import {config} from 'src/config.js'; +import * as utils from 'src/utils.js'; +import {getRefererInfo} from 'src/refererDetection.js'; + +function createBidderRequest(auctionId, timeout, pageUrl) { + return { + auctionId: auctionId || 'c1243d83-0bed-4fdb-8c76-42b456be17d0', + timeout: timeout || 2000, + refererInfo: { + referer: pageUrl || 'example.com' + } + }; +} + +function createValidBidRequest(params, bidId, sizes) { + return { + adUnitCode: 'adunit-code', + bidId: bidId || '22c4871113f461', + bidder: 'kobler', + bidderRequestId: '15246a574e859f', + bidRequestsCount: 1, + bidderRequestsCount: 1, + mediaTypes: { + banner: { + sizes: sizes || [[300, 250], [320, 100]] + } + }, + params: params || { + placementId: 'tpw58278' + }, + transactionTd: '04314114-15bd-4638-8664-bdb8bdc60bff' + }; +} + +describe('KoblerAdapter', function () { + describe('inherited functions', function () { + it('exists and is a function', function () { + const adapter = newBidder(spec); + expect(adapter.callBids).to.exist.and.to.be.a('function'); + }); + }); + + describe('isBidRequestValid', function () { + it('should not accept a request without bidId as valid', function () { + const bid = { + params: { + someParam: 'abc' + } + }; + + const result = spec.isBidRequestValid(bid); + + expect(result).to.be.false; + }); + + it('should not accept a request without params as valid', function () { + const bid = { + bidId: 'e11768e8-3b71-4453-8698-0a2feb866589' + }; + + const result = spec.isBidRequestValid(bid); + + expect(result).to.be.false; + }); + + it('should not accept a request without placementId as valid', function () { + const bid = { + bidId: 'e11768e8-3b71-4453-8698-0a2feb866589', + params: { + someParam: 'abc' + } + }; + + const result = spec.isBidRequestValid(bid); + + expect(result).to.be.false; + }); + + it('should accept a request with bidId and placementId as valid', function () { + const bid = { + bidId: 'e11768e8-3b71-4453-8698-0a2feb866589', + params: { + someParam: 'abc', + placementId: '8bde0923-1409-4253-9594-495b58d931ba' + } + }; + + const result = spec.isBidRequestValid(bid); + + expect(result).to.be.true; + }); + }); + + describe('buildRequests', function () { + it('should read data from bidder request', function () { + const testUrl = 'kobler.no'; + const auctionId = '8319af54-9795-4642-ba3a-6f57d6ff9100'; + const timeout = 5000; + const validBidRequests = [createValidBidRequest()]; + const bidderRequest = createBidderRequest(auctionId, timeout, testUrl); + + const result = spec.buildRequests(validBidRequests, bidderRequest); + const openRtbRequest = JSON.parse(result.data); + + expect(openRtbRequest.tmax).to.be.equal(timeout); + expect(openRtbRequest.id).to.be.equal(auctionId); + expect(openRtbRequest.site.page).to.be.equal(testUrl); + }); + + it('should read data from valid bid requests', function () { + const firstSize = [400, 800]; + const secondSize = [450, 950]; + const sizes = [firstSize, secondSize]; + const placementId = 'tsjs86325'; + const bidId = '3a56a019-4835-4f75-811c-76fac6853a2c'; + const validBidRequests = [ + createValidBidRequest( + { + placementId: placementId + }, + bidId, + sizes + ) + ]; + const bidderRequest = createBidderRequest(); + + const result = spec.buildRequests(validBidRequests, bidderRequest); + const openRtbRequest = JSON.parse(result.data); + + expect(openRtbRequest.imp.length).to.be.equal(1); + expect(openRtbRequest.imp[0].id).to.be.equal(bidId); + expect(openRtbRequest.imp[0].tagid).to.be.equal(placementId); + expect(openRtbRequest.imp[0].banner.w).to.be.equal(firstSize[0]); + expect(openRtbRequest.imp[0].banner.h).to.be.equal(firstSize[1]); + expect(openRtbRequest.imp[0].banner.format.length).to.be.equal(2); + expect(openRtbRequest.imp[0].banner.format[0].w).to.be.equal(firstSize[0]); + expect(openRtbRequest.imp[0].banner.format[0].h).to.be.equal(firstSize[1]); + expect(openRtbRequest.imp[0].banner.format[1].w).to.be.equal(secondSize[0]); + expect(openRtbRequest.imp[0].banner.format[1].h).to.be.equal(secondSize[1]); + }); + + it('should use 0x0 as default size', function () { + const validBidRequests = [ + createValidBidRequest( + undefined, + undefined, + [] + ) + ]; + const bidderRequest = createBidderRequest(); + + const result = spec.buildRequests(validBidRequests, bidderRequest); + const openRtbRequest = JSON.parse(result.data); + + expect(openRtbRequest.imp.length).to.be.equal(1); + expect(openRtbRequest.imp[0].banner.w).to.be.equal(0); + expect(openRtbRequest.imp[0].banner.h).to.be.equal(0); + expect(openRtbRequest.imp[0].banner.format.length).to.be.equal(1); + expect(openRtbRequest.imp[0].banner.format[0].w).to.be.equal(0); + expect(openRtbRequest.imp[0].banner.format[0].h).to.be.equal(0); + }); + + it('should use 0 as default position', function () { + const validBidRequests = [createValidBidRequest()]; + const bidderRequest = createBidderRequest(); + + const result = spec.buildRequests(validBidRequests, bidderRequest); + const openRtbRequest = JSON.parse(result.data); + + expect(openRtbRequest.imp.length).to.be.equal(1); + expect(openRtbRequest.imp[0].banner.ext.kobler.pos).to.be.equal(0); + }); + + it('should read zip from valid bid requests', function () { + const zip = '700 02'; + const validBidRequests = [ + createValidBidRequest( + { + placementId: 'nmah8324234', + zip: zip + } + ) + ]; + const bidderRequest = createBidderRequest(); + + const result = spec.buildRequests(validBidRequests, bidderRequest); + const openRtbRequest = JSON.parse(result.data); + + expect(openRtbRequest.device.geo.zip).to.be.equal(zip); + }); + + it('should read test from valid bid requests', function () { + const validBidRequests = [ + createValidBidRequest( + { + placementId: 'zwop842799', + test: true + } + ) + ]; + const bidderRequest = createBidderRequest(); + + const result = spec.buildRequests(validBidRequests, bidderRequest); + const openRtbRequest = JSON.parse(result.data); + + expect(openRtbRequest.test).to.be.equal(1); + }); + + it('should read floorPrice from valid bid requests', function () { + const floorPrice = 4.343; + const validBidRequests = [ + createValidBidRequest( + { + placementId: 'oqr3224234', + floorPrice: floorPrice + } + ) + ]; + const bidderRequest = createBidderRequest(); + + const result = spec.buildRequests(validBidRequests, bidderRequest); + const openRtbRequest = JSON.parse(result.data); + + expect(openRtbRequest.imp.length).to.be.equal(1); + expect(openRtbRequest.imp[0].bidfloor).to.be.equal(floorPrice); + }); + + it('should read position from valid bid requests', function () { + const placementId = 'yzksf234592'; + const validBidRequests = [ + createValidBidRequest( + { + placementId: placementId, + position: 1 + } + ), + createValidBidRequest( + { + placementId: placementId, + position: 2 + } + ), + createValidBidRequest( + { + placementId: placementId, + position: 3 + } + ) + ]; + const bidderRequest = createBidderRequest(); + + const result = spec.buildRequests(validBidRequests, bidderRequest); + const openRtbRequest = JSON.parse(result.data); + + expect(openRtbRequest.imp.length).to.be.equal(3); + expect(openRtbRequest.imp[0].banner.ext.kobler.pos).to.be.equal(1); + expect(openRtbRequest.imp[0].tagid).to.be.equal(placementId); + expect(openRtbRequest.imp[1].banner.ext.kobler.pos).to.be.equal(2); + expect(openRtbRequest.imp[1].tagid).to.be.equal(placementId); + expect(openRtbRequest.imp[2].banner.ext.kobler.pos).to.be.equal(3); + expect(openRtbRequest.imp[2].tagid).to.be.equal(placementId); + }); + + it('should read dealIds from valid bid requests', function () { + const dealIds1 = ['78214682234823']; + const dealIds2 = ['89913861235234', '27368423545328640']; + const validBidRequests = [ + createValidBidRequest( + { + placementId: 'rsl1239823', + dealIds: dealIds1 + } + ), + createValidBidRequest( + { + placementId: 'pqw234232', + dealIds: dealIds2 + } + ) + ]; + const bidderRequest = createBidderRequest(); + + const result = spec.buildRequests(validBidRequests, bidderRequest); + const openRtbRequest = JSON.parse(result.data); + + expect(openRtbRequest.imp.length).to.be.equal(2); + expect(openRtbRequest.imp[0].pmp.deals.length).to.be.equal(1); + expect(openRtbRequest.imp[0].pmp.deals[0].id).to.be.equal(dealIds1[0]); + expect(openRtbRequest.imp[1].pmp.deals.length).to.be.equal(2); + expect(openRtbRequest.imp[1].pmp.deals[0].id).to.be.equal(dealIds2[0]); + expect(openRtbRequest.imp[1].pmp.deals[1].id).to.be.equal(dealIds2[1]); + }); + + it('should read timeout from config', function () { + const timeout = 4000; + const validBidRequests = [createValidBidRequest()]; + // No timeout field + const bidderRequest = { + auctionId: 'c1243d83-0bed-4fdb-8c76-42b456be17d0', + refererInfo: { + referer: 'example.com' + } + }; + config.setConfig({ + bidderTimeout: timeout + }); + + const result = spec.buildRequests(validBidRequests, bidderRequest); + const openRtbRequest = JSON.parse(result.data); + + expect(openRtbRequest.tmax).to.be.equal(timeout); + }); + + it('should read floor price using floors module', function () { + const floorPriceFor580x400 = 6.5148; + const floorPriceForAnySize = 4.2343; + const validBidRequests = [ + createValidBidRequest(undefined, '98efe127-f926-4dde-b988-db8e5dba5a76', [[580, 400]]), + createValidBidRequest(undefined, 'c7698d4a-94f4-4a6b-a928-7e1facfbf752', []) + ]; + validBidRequests.forEach(validBidRequest => { + validBidRequest.getFloor = function (params) { + let floorPrice; + if (utils.isArray(params.size) && params.size[0] === 580 && params.size[1] === 400) { + floorPrice = floorPriceFor580x400; + } else if (params.size === '*') { + floorPrice = floorPriceForAnySize + } else { + floorPrice = 0 + } + return { + currency: params.currency, + floor: floorPrice + } + } + }) + const bidderRequest = createBidderRequest(); + + const result = spec.buildRequests(validBidRequests, bidderRequest); + const openRtbRequest = JSON.parse(result.data); + + expect(openRtbRequest.imp.length).to.be.equal(2); + expect(openRtbRequest.imp[0].id).to.be.equal('98efe127-f926-4dde-b988-db8e5dba5a76'); + expect(openRtbRequest.imp[0].bidfloor).to.be.equal(floorPriceFor580x400); + expect(openRtbRequest.imp[1].id).to.be.equal('c7698d4a-94f4-4a6b-a928-7e1facfbf752'); + expect(openRtbRequest.imp[1].bidfloor).to.be.equal(floorPriceForAnySize); + }); + + it('should create whole OpenRTB request', function () { + const validBidRequests = [ + createValidBidRequest( + { + placementId: 'pcha322364', + zip: '0015', + floorPrice: 5.6234, + position: 1, + dealIds: ['623472534328234'] + }, + '953ee65d-d18a-484f-a840-d3056185a060', + [[400, 600]] + ), + createValidBidRequest( + { + placementId: 'sdfgoi32y4', + floorPrice: 3.2543, + position: 2, + dealIds: ['92368234753283', '263845832942'] + }, + '8320bf79-9d90-4a17-87c6-5d505706a921', + [[400, 500], [200, 250], [300, 350]] + ), + createValidBidRequest( + { + placementId: 'gwms2738647', + position: 3 + }, + 'd0de713b-32e3-4191-a2df-a007f08ffe72', + [[800, 900]] + ) + ]; + const bidderRequest = createBidderRequest( + '9ff580cf-e10e-4b66-add7-40ac0c804e21', + 4500, + 'bid.kobler.no' + ); + + const result = spec.buildRequests(validBidRequests, bidderRequest); + const openRtbRequest = JSON.parse(result.data); + + const expectedOpenRtbRequest = { + id: '9ff580cf-e10e-4b66-add7-40ac0c804e21', + at: 1, + tmax: 4500, + cur: ['USD'], + imp: [ + { + id: '953ee65d-d18a-484f-a840-d3056185a060', + banner: { + format: [ + { + w: 400, + h: 600 + } + ], + w: 400, + h: 600, + ext: { + kobler: { + pos: 1 + } + } + }, + tagid: 'pcha322364', + bidfloor: 5.6234, + bidfloorcur: 'USD', + pmp: { + deals: [ + { + id: '623472534328234' + } + ] + } + }, + { + id: '8320bf79-9d90-4a17-87c6-5d505706a921', + banner: { + format: [ + { + w: 400, + h: 500 + }, + { + w: 200, + h: 250 + }, + { + w: 300, + h: 350 + } + ], + w: 400, + h: 500, + ext: { + kobler: { + pos: 2 + } + } + }, + tagid: 'sdfgoi32y4', + bidfloor: 3.2543, + bidfloorcur: 'USD', + pmp: { + deals: [ + { + id: '92368234753283' + }, + { + id: '263845832942' + } + ] + } + }, + { + id: 'd0de713b-32e3-4191-a2df-a007f08ffe72', + banner: { + format: [ + { + w: 800, + h: 900 + } + ], + w: 800, + h: 900, + ext: { + kobler: { + pos: 3 + } + } + }, + tagid: 'gwms2738647', + bidfloor: 0, + bidfloorcur: 'USD', + pmp: {} + } + ], + device: { + devicetype: 2, + geo: { + zip: '0015' + } + }, + site: { + page: 'bid.kobler.no' + }, + test: 0 + }; + + expect(openRtbRequest).to.deep.equal(expectedOpenRtbRequest); + }); + }); + + describe('interpretResponse', function () { + it('should handle empty body', function () { + const responseWithEmptyBody = { + body: undefined + }; + const bids = spec.interpretResponse(responseWithEmptyBody) + + expect(bids.length).to.be.equal(0); + }); + + it('should generate bids from OpenRTB response', function () { + const responseWithTwoBids = { + body: { + seatbid: [ + { + bid: [ + { + impid: '6194ddef-89a4-404f-9efd-6b718fc23308', + price: 7.981, + nurl: 'https://atag.essrtb.com/serve/prebid_win_notification?payload=sdhfusdaobfadslf234324&sp=${AUCTION_PRICE}&asp=${AD_SERVER_PRICE}', + crid: 'edea9b03-3a57-41aa-9c00-abd673e22006', + dealid: '', + w: 320, + h: 250, + adm: '', + adomain: [ + 'https://kobler.no' + ] + }, + { + impid: '2ec0b40f-d3ca-4ba5-8ce3-48290565690f', + price: 6.71234, + nurl: 'https://atag.essrtb.com/serve/prebid_win_notification?payload=nbashgufvishdafjk23432&sp=${AUCTION_PRICE}&asp=${AD_SERVER_PRICE}', + crid: 'fa2d5af7-2678-4204-9023-44c526160742', + dealid: '2783483223432342', + w: 580, + h: 400, + adm: '', + adomain: [ + 'https://bid.kobler.no' + ] + } + ] + } + ], + cur: 'USD' + } + }; + const bids = spec.interpretResponse(responseWithTwoBids) + + const expectedBids = [ + { + requestId: '6194ddef-89a4-404f-9efd-6b718fc23308', + cpm: 7.981, + currency: 'USD', + width: 320, + height: 250, + creativeId: 'edea9b03-3a57-41aa-9c00-abd673e22006', + dealId: '', + netRevenue: true, + ttl: 600, + ad: '', + nurl: 'https://atag.essrtb.com/serve/prebid_win_notification?payload=sdhfusdaobfadslf234324&sp=${AUCTION_PRICE}&asp=${AD_SERVER_PRICE}', + meta: { + advertiserDomains: [ + 'https://kobler.no' + ] + } + }, + { + requestId: '2ec0b40f-d3ca-4ba5-8ce3-48290565690f', + cpm: 6.71234, + currency: 'USD', + width: 580, + height: 400, + creativeId: 'fa2d5af7-2678-4204-9023-44c526160742', + dealId: '2783483223432342', + netRevenue: true, + ttl: 600, + ad: '', + nurl: 'https://atag.essrtb.com/serve/prebid_win_notification?payload=nbashgufvishdafjk23432&sp=${AUCTION_PRICE}&asp=${AD_SERVER_PRICE}', + meta: { + advertiserDomains: [ + 'https://bid.kobler.no' + ] + } + } + ]; + expect(bids).to.deep.equal(expectedBids); + }); + }); + + describe('onBidWon', function () { + beforeEach(function () { + sinon.stub(utils, 'triggerPixel'); + }); + afterEach(function () { + utils.triggerPixel.restore(); + }); + + it('Should not trigger pixel if bid does not contain nurl', function () { + spec.onBidWon({}); + + expect(utils.triggerPixel.called).to.be.false; + }); + + it('Should not trigger pixel if nurl is empty', function () { + spec.onBidWon({ + nurl: '' + }); + + expect(utils.triggerPixel.called).to.be.false; + }); + + it('Should trigger pixel with replaced nurl if nurl is not empty', function () { + spec.onBidWon({ + cpm: 8.341, + nurl: 'https://atag.essrtb.com/serve/prebid_win_notification?payload=sdhfusdaobfadslf234324&sp=${AUCTION_PRICE}&asp=${AD_SERVER_PRICE}', + adserverTargeting: { + hb_pb: 8 + } + }); + + expect(utils.triggerPixel.callCount).to.be.equal(1); + expect(utils.triggerPixel.firstCall.args[0]).to.be.equal( + 'https://atag.essrtb.com/serve/prebid_win_notification?payload=sdhfusdaobfadslf234324&sp=8.341&asp=8' + ); + }); + }); + + describe('onTimeout', function () { + beforeEach(function () { + sinon.stub(utils, 'triggerPixel'); + }); + afterEach(function () { + utils.triggerPixel.restore(); + }); + + it('Should not trigger pixel if timeout data is not array', function () { + spec.onTimeout(null); + + expect(utils.triggerPixel.called).to.be.false; + }); + + it('Should not trigger pixel if timeout data is empty', function () { + spec.onTimeout([]); + + expect(utils.triggerPixel.called).to.be.false; + }); + + it('Should trigger pixel with query parameters if timeout data not empty', function () { + spec.onTimeout([ + { + adUnitCode: 'adunit-code', + auctionId: 'a1fba829-dd41-409f-acfb-b7b0ac5f30c6', + bidId: 'ef236c6c-e934-406b-a877-d7be8e8a839a', + timeout: 100, + params: [ + { + placementId: 'xrwg62731', + } + ], + }, + { + adUnitCode: 'adunit-code-2', + auctionId: 'a1fba829-dd41-409f-acfb-b7b0ac5f30c6', + bidId: 'ca4121c8-9a4a-46ba-a624-e9b64af206f2', + timeout: 100, + params: [ + { + placementId: 'bc482234', + } + ], + } + ]); + + expect(utils.triggerPixel.callCount).to.be.equal(2); + expect(utils.triggerPixel.getCall(0).args[0]).to.be.equal( + 'https://bid.essrtb.com/notify/prebid_timeout?ad_unit_code=adunit-code&' + + 'auction_id=a1fba829-dd41-409f-acfb-b7b0ac5f30c6&bid_id=ef236c6c-e934-406b-a877-d7be8e8a839a&timeout=100&' + + 'placement_id=xrwg62731&page_url=' + encodeURIComponent(getRefererInfo().referer) + ); + expect(utils.triggerPixel.getCall(1).args[0]).to.be.equal( + 'https://bid.essrtb.com/notify/prebid_timeout?ad_unit_code=adunit-code-2&' + + 'auction_id=a1fba829-dd41-409f-acfb-b7b0ac5f30c6&bid_id=ca4121c8-9a4a-46ba-a624-e9b64af206f2&timeout=100&' + + 'placement_id=bc482234&page_url=' + encodeURIComponent(getRefererInfo().referer) + ); + }); + }); +}); diff --git a/test/spec/modules/krushmediaBidAdapter_spec.js b/test/spec/modules/krushmediaBidAdapter_spec.js new file mode 100644 index 00000000000..3af9ed64c43 --- /dev/null +++ b/test/spec/modules/krushmediaBidAdapter_spec.js @@ -0,0 +1,329 @@ +import {expect} from 'chai'; +import {spec} from '../../../modules/krushmediaBidAdapter.js'; +import { BANNER, VIDEO, NATIVE } from '../../../src/mediaTypes.js'; + +describe('KrushmediabBidAdapter', function () { + const bid = { + bidId: '23fhj33i987f', + bidder: 'krushmedia', + mediaTypes: { + [BANNER]: { + sizes: [[300, 250]] + } + }, + params: { + key: 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.key; + 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://ads4.krushmedia.com/?c=rtb&m=hb'); + }); + 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('key', 'bidId', 'traffic', 'sizes', 'schain'); + expect(placement.key).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('key', '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('key', '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://cs.krushmedia.com/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://cs.krushmedia.com/html?src=pbjs&ccpa_consent=1NNN') + }); + }); +}); 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 a236ac17d71..a3a70a39731 100644 --- a/test/spec/modules/lemmaBidAdapter_spec.js +++ b/test/spec/modules/lemmaBidAdapter_spec.js @@ -23,6 +23,7 @@ describe('lemmaBidAdapter', function() { pubId: 1001, adunitId: 1, currency: 'AUD', + bidFloor: 1.3, geo: { lat: '12.3', lon: '23.7', @@ -46,6 +47,7 @@ describe('lemmaBidAdapter', function() { params: { pubId: 1001, adunitId: 1, + bidFloor: 1.3, video: { mimes: ['video/mp4', 'video/x-flv'], skippable: true, @@ -161,6 +163,7 @@ describe('lemmaBidAdapter', function() { expect(data.site.publisher.id).to.equal(bidRequests[0].params.pubId.toString()); // publisher Id expect(data.imp[0].tagid).to.equal('1'); // tagid expect(data.imp[0].bidfloorcur).to.equal(bidRequests[0].params.currency); + expect(data.imp[0].bidfloor).to.equal(bidRequests[0].params.bidFloor); }); it('Request params check without mediaTypes object', function() { var bidRequests = [{ @@ -192,6 +195,7 @@ describe('lemmaBidAdapter', function() { expect(data.site.publisher.id).to.equal(bidRequests[0].params.pubId.toString()); // publisher Id expect(data.imp[0].tagid).to.equal(undefined); // tagid expect(data.imp[0].bidfloorcur).to.equal(bidRequests[0].params.currency); + expect(data.imp[0].bidfloor).to.equal(bidRequests[0].params.bidFloor); }); it('Request params multi size format object check', function() { var bidRequests = [{ @@ -309,6 +313,77 @@ describe('lemmaBidAdapter', function() { expect(data.imp[0]['video']['w']).to.equal(videoBidRequests[0].mediaTypes.video.playerSize[0]); expect(data.imp[0]['video']['h']).to.equal(videoBidRequests[0].mediaTypes.video.playerSize[1]); }); + describe('setting imp.floor using floorModule', function() { + /* + Use the minimum value among floor from floorModule per mediaType + If params.bidFloor is set then take max(floor, min(floors from floorModule)) + set imp.bidfloor only if it is more than 0 + */ + + let newRequest; + let floorModuleTestData; + let getFloor = function(req) { + return floorModuleTestData[req.mediaType]; + }; + + beforeEach(() => { + floorModuleTestData = { + 'banner': { + 'currency': 'AUD', + 'floor': 1.50 + }, + 'video': { + 'currency': 'AUD', + 'floor': 2.00 + } + }; + newRequest = utils.deepClone(bidRequests); + newRequest[0].getFloor = getFloor; + }); + + it('bidfloor should be undefined if calculation is <= 0', function() { + floorModuleTestData.banner.floor = 0; // lowest of them all + newRequest[0].params.bidFloor = undefined; + let request = spec.buildRequests(newRequest); + let data = JSON.parse(request.data); + data = data.imp[0]; + expect(data.bidfloor).to.equal(undefined); + }); + + it('ignore floormodule o/p if floor is not number', function() { + floorModuleTestData.banner.floor = 'INR'; + newRequest[0].params.bidFloor = undefined; + let request = spec.buildRequests(newRequest); + let data = JSON.parse(request.data); + data = data.imp[0]; + expect(data.bidfloor).to.equal(undefined); // video will be lowest now + }); + + it('ignore floormodule o/p if currency is not matched', function() { + floorModuleTestData.banner.currency = 'INR'; + newRequest[0].params.bidFloor = undefined; + let request = spec.buildRequests(newRequest); + let data = JSON.parse(request.data); + data = data.imp[0]; + expect(data.bidfloor).to.equal(undefined); // video will be lowest now + }); + + it('bidFloor is not passed, use minimum from floorModule', function() { + newRequest[0].params.bidFloor = undefined; + let request = spec.buildRequests(newRequest); + let data = JSON.parse(request.data); + data = data.imp[0]; + expect(data.bidfloor).to.equal(1.5); + }); + + it('bidFloor is passed as 1, use min of floorModule as it is highest', function() { + newRequest[0].params.bidFloor = '1.0';// yes, we want it as a string + let request = spec.buildRequests(newRequest); + let data = JSON.parse(request.data); + data = data.imp[0]; + expect(data.bidfloor).to.equal(1.5); + }); + }); describe('Response checking', function() { it('should check for valid response values', function() { var request = spec.buildRequests(bidRequests); @@ -331,5 +406,21 @@ describe('lemmaBidAdapter', function() { }); }); }); + describe('getUserSyncs', function() { + const syncurl_iframe = 'https://sync.lemmatechnologies.com/js/usersync.html?pid=1001'; + 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: syncurl_iframe + }]); + }); + }); }); }); 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 b19d38d5859..f1de2f3bf93 100644 --- a/test/spec/modules/liveIntentIdSystem_spec.js +++ b/test/spec/modules/liveIntentIdSystem_spec.js @@ -1,62 +1,85 @@ -import {liveIntentIdSubmodule, reset as resetLiveIntentIdSubmodule, storage} from 'modules/liveIntentIdSystem.js'; +import { liveIntentIdSubmodule, reset as resetLiveIntentIdSubmodule, storage } from 'modules/liveIntentIdSystem.js'; import * as utils from 'src/utils.js'; -import {uspDataHandler} from '../../../src/adapterManager.js'; -import {server} from 'test/mocks/xhr.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 = {publisherId: PUBLISHER_ID}; +const defaultConfigParams = { params: {publisherId: PUBLISHER_ID} }; const responseHeader = {'Content-Type': 'application/json'} -describe('LiveIntentId', function () { - let pixel = {}; +describe('LiveIntentId', function() { let logErrorStub; - let consentDataStub; + let uspConsentDataStub; + let gdprConsentDataStub; let getCookieStub; let getDataFromLocalStorageStub; let imgStub; - beforeEach(function () { - imgStub = sinon.stub(window, 'Image').returns(pixel); + beforeEach(function() { + liveIntentIdSubmodule.setModuleMode('standard'); + imgStub = sinon.stub(utils, 'triggerPixel'); getCookieStub = sinon.stub(storage, 'getCookie'); getDataFromLocalStorageStub = sinon.stub(storage, 'getDataFromLocalStorage'); logErrorStub = sinon.stub(utils, 'logError'); - consentDataStub = sinon.stub(uspDataHandler, 'getConsentData'); + uspConsentDataStub = sinon.stub(uspDataHandler, 'getConsentData'); + gdprConsentDataStub = sinon.stub(gdprDataHandler, 'getConsentData'); }); - afterEach(function () { - pixel = {}; + afterEach(function() { imgStub.restore(); getCookieStub.restore(); getDataFromLocalStorageStub.restore(); logErrorStub.restore(); - consentDataStub.restore(); + uspConsentDataStub.restore(); + gdprConsentDataStub.restore(); resetLiveIntentIdSubmodule(); }); - it('should initialize LiveConnect with a us privacy string when getId, and include it in all requests', function () { - consentDataStub.returns('1YNY'); + it('should initialize LiveConnect with a privacy string when getId, and include it in the resolution request', function () { + uspConsentDataStub.returns('1YNY'); + gdprConsentDataStub.returns({ + gdprApplies: true, + consentString: 'consentDataString' + }) let callBackSpy = sinon.spy(); let submoduleCallback = liveIntentIdSubmodule.getId(defaultConfigParams).callback; - expect(pixel.src).to.match(/.*us_privacy=1YNY/); submoduleCallback(callBackSpy); - let request = server.requests[0]; - expect(request.url).to.match(/.*us_privacy=1YNY/); + let request = server.requests[1]; + expect(request.url).to.match(/.*us_privacy=1YNY.*&gdpr=1&gdpr_consent=consentDataString.*/); + const response = { + unifiedId: 'a_unified_id', + segments: [123, 234] + } request.respond( 200, responseHeader, - JSON.stringify({}) + JSON.stringify(response) ); - expect(callBackSpy.calledOnce).to.be.true; + expect(callBackSpy.calledOnceWith(response)).to.be.true; }); - it('should fire an event when getId', function () { + it('should fire an event when getId', function() { + uspConsentDataStub.returns('1YNY'); + gdprConsentDataStub.returns({ + gdprApplies: true, + consentString: 'consentDataString' + }) liveIntentIdSubmodule.getId(defaultConfigParams); - expect(pixel.src).to.match(/https:\/\/rp.liadm.com\/p\?wpn=prebid.*/) + expect(server.requests[0].url).to.match(/https:\/\/rp.liadm.com\/j\?wpn=prebid.*us_privacy=1YNY.*&gdpr=1&gdpr_consent=consentDataString.*/); }); - it('should initialize LiveConnect with the config params when decode and emit an event', function () { - liveIntentIdSubmodule.decode({}, { + it('should fire an event when getId and a hash is provided', function() { + liveIntentIdSubmodule.getId({ params: { ...defaultConfigParams, + emailHash: '58131bc547fb87af94cebdaf3102321f' + }}); + expect(server.requests[0].url).to.match(/https:\/\/rp.liadm.com\/j\?.*e=58131bc547fb87af94cebdaf3102321f.+/) + }); + + it('should initialize LiveConnect with the config params when decode and emit an event', function () { + liveIntentIdSubmodule.decode({}, { params: { + ...defaultConfigParams.params, ...{ url: 'https://dummy.liveintent.com', liCollectConfig: { @@ -64,61 +87,72 @@ describe('LiveIntentId', function () { collectorUrl: 'https://collector.liveintent.com' } } - }); - expect(pixel.src).to.match(/https:\/\/collector.liveintent.com\/p\?aid=a-0001&wpn=prebid.*/) + }}); + expect(server.requests[0].url).to.match(/https:\/\/collector.liveintent.com\/j\?aid=a-0001&wpn=prebid.*/); }); - it('should initialize LiveConnect and emit an event with a us privacy string when decode', function () { - consentDataStub.returns('1YNY'); + it('should initialize LiveConnect and emit an event with a privacy string when decode', function() { + uspConsentDataStub.returns('1YNY'); + gdprConsentDataStub.returns({ + gdprApplies: false, + consentString: 'consentDataString' + }) liveIntentIdSubmodule.decode({}, defaultConfigParams); - expect(pixel.src).to.match(/.*us_privacy=1YNY/); + expect(server.requests[0].url).to.match(/.*us_privacy=1YNY.*&gdpr=0&gdpr_consent=consentDataString.*/); }); - it('should not return a decoded identifier when the unifiedId is not present in the value', function () { - const result = liveIntentIdSubmodule.decode({additionalData: 'data'}); + it('should fire an event when decode and a hash is provided', function() { + liveIntentIdSubmodule.decode({}, { params: { + ...defaultConfigParams.params, + emailHash: '58131bc547fb87af94cebdaf3102321f' + }}); + expect(server.requests[0].url).to.match(/https:\/\/rp.liadm.com\/j\?.*e=58131bc547fb87af94cebdaf3102321f.+/); + }); + + 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 fire an event when decode', function () { + it('should fire an event when decode', function() { liveIntentIdSubmodule.decode({}, defaultConfigParams); - expect(pixel.src).to.be.not.null + expect(server.requests[0].url).to.be.not.null }); - it('should initialize LiveConnect and send data only once', function () { + it('should initialize LiveConnect and send data only once', function() { liveIntentIdSubmodule.getId(defaultConfigParams); liveIntentIdSubmodule.decode({}, defaultConfigParams); liveIntentIdSubmodule.getId(defaultConfigParams); liveIntentIdSubmodule.decode({}, defaultConfigParams); - expect(imgStub.calledOnce).to.be.true; + expect(server.requests.length).to.be.eq(1); }); - it('should call the Custom URL of the LiveIntent Identity Exchange endpoint', function () { + it('should call the Custom URL of the LiveIntent Identity Exchange endpoint', function() { getCookieStub.returns(null); let callBackSpy = sinon.spy(); - let submoduleCallback = liveIntentIdSubmodule.getId({...defaultConfigParams, ...{'url': 'https://dummy.liveintent.com/idex'}}).callback; + let submoduleCallback = liveIntentIdSubmodule.getId({ params: {...defaultConfigParams.params, ...{'url': 'https://dummy.liveintent.com/idex'}} }).callback; submoduleCallback(callBackSpy); - let request = server.requests[0]; + let request = server.requests[1]; expect(request.url).to.be.eq('https://dummy.liveintent.com/idex/prebid/89899'); request.respond( - 200, - responseHeader, - JSON.stringify({}) + 204, + responseHeader ); - expect(callBackSpy.calledOnce).to.be.true; + expect(callBackSpy.calledOnceWith({})).to.be.true; }); - it('should call the default url of the LiveIntent Identity Exchange endpoint, with a partner', function () { + 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({ - ...defaultConfigParams, + let submoduleCallback = liveIntentIdSubmodule.getId({ params: { + ...defaultConfigParams.params, ...{ 'url': 'https://dummy.liveintent.com/idex', 'partner': 'rubicon' } - }).callback; + } }).callback; submoduleCallback(callBackSpy); - let request = server.requests[0]; + let request = server.requests[1]; expect(request.url).to.be.eq('https://dummy.liveintent.com/idex/rubicon/89899'); request.respond( 200, @@ -128,12 +162,12 @@ describe('LiveIntentId', function () { expect(callBackSpy.calledOnce).to.be.true; }); - it('should call the LiveIntent Identity Exchange endpoint, with no additional query params', function () { + 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]; + let request = server.requests[1]; expect(request.url).to.be.eq('https://idx.liadm.com/idex/prebid/89899'); request.respond( 200, @@ -143,12 +177,12 @@ describe('LiveIntentId', function () { expect(callBackSpy.calledOnce).to.be.true; }); - it('should log an error and continue to callback if ajax request errors', function () { + 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]; + let request = server.requests[1]; expect(request.url).to.be.eq('https://idx.liadm.com/idex/prebid/89899'); request.respond( 503, @@ -159,13 +193,13 @@ describe('LiveIntentId', function () { expect(callBackSpy.calledOnce).to.be.true; }); - it('should include the LiveConnect identifier when calling the LiveIntent Identity Exchange endpoint', function () { + it('should include the LiveConnect identifier when calling the LiveIntent Identity Exchange endpoint', function() { const oldCookie = 'a-xxxx--123e4567-e89b-12d3-a456-426655440000' getDataFromLocalStorageStub.withArgs('_li_duid').returns(oldCookie); let callBackSpy = sinon.spy(); let submoduleCallback = liveIntentIdSubmodule.getId(defaultConfigParams).callback; submoduleCallback(callBackSpy); - let request = server.requests[0]; + let request = server.requests[1]; expect(request.url).to.be.eq(`https://idx.liadm.com/idex/prebid/89899?duid=${oldCookie}`); request.respond( 200, @@ -175,20 +209,20 @@ describe('LiveIntentId', function () { expect(callBackSpy.calledOnce).to.be.true; }); - it('should include the LiveConnect identifier and additional Identifiers to resolve', function () { + it('should include the LiveConnect identifier and additional Identifiers to resolve', function() { const oldCookie = 'a-xxxx--123e4567-e89b-12d3-a456-426655440000' getDataFromLocalStorageStub.withArgs('_li_duid').returns(oldCookie); getDataFromLocalStorageStub.withArgs('_thirdPC').returns('third-pc'); - const configParams = { - ...defaultConfigParams, + const configParams = { params: { + ...defaultConfigParams.params, ...{ 'identifiersToResolve': ['_thirdPC'] } - }; + }}; let callBackSpy = sinon.spy(); let submoduleCallback = liveIntentIdSubmodule.getId(configParams).callback; submoduleCallback(callBackSpy); - let request = server.requests[0]; + let request = server.requests[1]; expect(request.url).to.be.eq(`https://idx.liadm.com/idex/prebid/89899?duid=${oldCookie}&_thirdPC=third-pc`); request.respond( 200, @@ -198,19 +232,19 @@ describe('LiveIntentId', function () { expect(callBackSpy.calledOnce).to.be.true; }); - it('should include an additional identifier value to resolve even if it is an object', function () { + 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 = { - ...defaultConfigParams, + const configParams = { params: { + ...defaultConfigParams.params, ...{ 'identifiersToResolve': ['_thirdPC'] } - }; + }}; let callBackSpy = sinon.spy(); let submoduleCallback = liveIntentIdSubmodule.getId(configParams).callback; submoduleCallback(callBackSpy); - let request = server.requests[0]; + let request = server.requests[1]; expect(request.url).to.be.eq('https://idx.liadm.com/idex/prebid/89899?_thirdPC=%7B%22key%22%3A%22value%22%7D'); request.respond( 200, @@ -219,4 +253,10 @@ describe('LiveIntentId', function () { ); expect(callBackSpy.calledOnce).to.be.true; }); + + it('should send an error when the cookie jar throws an unexpected error', function() { + getCookieStub.throws('CookieError', 'A message'); + liveIntentIdSubmodule.getId(defaultConfigParams); + expect(imgStub.getCall(0).args[0]).to.match(/.*ae=.+/); + }); }); diff --git a/test/spec/modules/livewrappedAnalyticsAdapter_spec.js b/test/spec/modules/livewrappedAnalyticsAdapter_spec.js index 4e05d1a31ff..ba9430e0b95 100644 --- a/test/spec/modules/livewrappedAnalyticsAdapter_spec.js +++ b/test/spec/modules/livewrappedAnalyticsAdapter_spec.js @@ -120,6 +120,8 @@ const MOCK = { const ANALYTICS_MESSAGE = { publisherId: 'CC411485-42BC-4F92-8389-42C503EE38D7', + gdpr: [{}], + auctionIds: ['25c6d7f5-699a-4bfc-87c9-996f915341fa'], bidAdUnits: [ { adUnit: 'panorama_d_1', @@ -134,17 +136,23 @@ const ANALYTICS_MESSAGE = { { adUnit: 'panorama_d_1', bidder: 'livewrapped', - timeStamp: 1519149562216 + timeStamp: 1519149562216, + gdpr: 0, + auctionId: 0 }, { adUnit: 'box_d_1', bidder: 'livewrapped', - timeStamp: 1519149562216 + timeStamp: 1519149562216, + gdpr: 0, + auctionId: 0 }, { adUnit: 'box_d_2', bidder: 'livewrapped', - timeStamp: 1519149562216 + timeStamp: 1519149562216, + gdpr: 0, + auctionId: 0 } ], responses: [ @@ -157,7 +165,9 @@ const ANALYTICS_MESSAGE = { cpm: 1.1, ttr: 200, IsBid: true, - mediaType: 1 + mediaType: 1, + gdpr: 0, + auctionId: 0 }, { timeStamp: 1519149562216, @@ -168,14 +178,18 @@ const ANALYTICS_MESSAGE = { cpm: 2.2, ttr: 300, IsBid: true, - mediaType: 1 + mediaType: 1, + gdpr: 0, + auctionId: 0 }, { timeStamp: 1519149562216, adUnit: 'box_d_2', bidder: 'livewrapped', ttr: 200, - IsBid: false + IsBid: false, + gdpr: 0, + auctionId: 0 } ], timeouts: [], @@ -187,7 +201,9 @@ const ANALYTICS_MESSAGE = { width: 980, height: 240, cpm: 1.1, - mediaType: 1 + mediaType: 1, + gdpr: 0, + auctionId: 0 }, { timeStamp: 1519149562216, @@ -196,7 +212,9 @@ const ANALYTICS_MESSAGE = { width: 300, height: 250, cpm: 2.2, - mediaType: 1 + mediaType: 1, + gdpr: 0, + auctionId: 0 } ] }; @@ -321,6 +339,148 @@ describe('Livewrapped analytics adapter', function () { expect(message.rcv).to.equal(true); }); + + it('should forward GDPR data', function () { + events.emit(AUCTION_INIT, MOCK.AUCTION_INIT); + events.emit(BID_REQUESTED, { + 'bidder': 'livewrapped', + 'auctionId': '25c6d7f5-699a-4bfc-87c9-996f915341fa', + 'bidderRequestId': '1be65d7958826a', + 'bids': [ + { + 'bidder': 'livewrapped', + 'adUnitCode': 'panorama_d_1', + 'bidId': '2ecff0db240757', + }, + { + 'bidder': 'livewrapped', + 'adUnitCode': 'box_d_1', + 'bidId': '3ecff0db240757', + } + ], + 'start': 1519149562216, + 'gdprConsent': { + 'gdprApplies': true, + 'consentString': 'consentstring' + } + }, + ); + + events.emit(BID_RESPONSE, MOCK.BID_RESPONSE[0]); + events.emit(BID_WON, MOCK.BID_WON[0]); + events.emit(AUCTION_END, MOCK.AUCTION_END); + + clock.tick(BID_WON_TIMEOUT + 1000); + + expect(server.requests.length).to.equal(1); + let request = server.requests[0]; + let message = JSON.parse(request.requestBody); + + expect(message.gdpr.length).to.equal(1); + expect(message.gdpr[0].gdprApplies).to.equal(true); + expect(message.gdpr[0].gdprConsent).to.equal('consentstring'); + expect(message.requests.length).to.equal(2); + expect(message.requests[0].gdpr).to.equal(0); + expect(message.requests[1].gdpr).to.equal(0); + + expect(message.responses.length).to.equal(1); + expect(message.responses[0].gdpr).to.equal(0); + + expect(message.wins.length).to.equal(1); + expect(message.wins[0].gdpr).to.equal(0); + }); + + it('should forward floor data', function () { + events.emit(AUCTION_INIT, MOCK.AUCTION_INIT); + events.emit(BID_REQUESTED, { + 'bidder': 'livewrapped', + 'auctionId': '25c6d7f5-699a-4bfc-87c9-996f915341fa', + 'bidderRequestId': '1be65d7958826a', + 'bids': [ + { + 'bidder': 'livewrapped', + 'adUnitCode': 'panorama_d_1', + 'bidId': '2ecff0db240757', + 'floorData': { + 'floorValue': 1.1, + 'floorCurrency': 'SEK' + } + } + ], + 'start': 1519149562216 + }); + + events.emit(BID_RESPONSE, MOCK.BID_RESPONSE[0]); + events.emit(BID_WON, MOCK.BID_WON[0]); + events.emit(AUCTION_END, MOCK.AUCTION_END); + + clock.tick(BID_WON_TIMEOUT + 1000); + + expect(server.requests.length).to.equal(1); + let request = server.requests[0]; + let message = JSON.parse(request.requestBody); + + expect(message.gdpr.length).to.equal(1); + + expect(message.responses.length).to.equal(1); + expect(message.responses[0].floor).to.equal(1.1); + expect(message.responses[0].floorCur).to.equal('SEK'); + + expect(message.wins.length).to.equal(1); + expect(message.wins[0].floor).to.equal(1.1); + expect(message.wins[0].floorCur).to.equal('SEK'); + }); + + it('should forward Livewrapped floor data', function () { + events.emit(AUCTION_INIT, MOCK.AUCTION_INIT); + events.emit(BID_REQUESTED, { + 'bidder': 'livewrapped', + 'auctionId': '25c6d7f5-699a-4bfc-87c9-996f915341fa', + 'bidderRequestId': '1be65d7958826a', + 'bids': [ + { + 'bidder': 'livewrapped', + 'adUnitCode': 'panorama_d_1', + 'bidId': '2ecff0db240757', + 'lwflr': { + 'flr': 1.1 + } + }, + { + 'bidder': 'livewrapped', + 'adUnitCode': 'box_d_1', + 'bidId': '3ecff0db240757', + 'lwflr': { + 'flr': 1.1, + 'bflrs': {'livewrapped': 2.2} + } + } + ], + 'start': 1519149562216 + }); + + events.emit(BID_RESPONSE, MOCK.BID_RESPONSE[0]); + events.emit(BID_RESPONSE, MOCK.BID_RESPONSE[1]); + events.emit(BID_WON, MOCK.BID_WON[0]); + events.emit(BID_WON, MOCK.BID_WON[1]); + events.emit(AUCTION_END, MOCK.AUCTION_END); + + clock.tick(BID_WON_TIMEOUT + 1000); + + expect(server.requests.length).to.equal(1); + let request = server.requests[0]; + let message = JSON.parse(request.requestBody); + + expect(message.gdpr.length).to.equal(1); + + expect(message.responses.length).to.equal(2); + expect(message.responses[0].floor).to.equal(1.1); + expect(message.responses[1].floor).to.equal(2.2); + + expect(message.wins.length).to.equal(2); + expect(message.wins[0].floor).to.equal(1.1); + expect(message.wins[1].floor).to.equal(2.2); + }); }); describe('when given other endpoint', function () { diff --git a/test/spec/modules/livewrappedBidAdapter_spec.js b/test/spec/modules/livewrappedBidAdapter_spec.js index fb676f75343..9435d9131fa 100644 --- a/test/spec/modules/livewrappedBidAdapter_spec.js +++ b/test/spec/modules/livewrappedBidAdapter_spec.js @@ -2,7 +2,7 @@ import {expect} from 'chai'; import {spec, storage} from 'modules/livewrappedBidAdapter.js'; import {config} from 'src/config.js'; import * as utils from 'src/utils.js'; -import { BANNER, NATIVE } from 'src/mediaTypes.js'; +import { BANNER, NATIVE, VIDEO } from 'src/mediaTypes.js'; describe('Livewrapped adapter tests', function () { let sandbox, @@ -102,7 +102,7 @@ describe('Livewrapped adapter tests', function () { userId: 'user id', url: 'https://www.domain.com', seats: {'dsp': ['seat 1']}, - version: '1.3', + version: '1.4', width: 100, height: 100, cookieSupport: true, @@ -139,7 +139,7 @@ describe('Livewrapped adapter tests', function () { userId: 'user id', url: 'https://www.domain.com', seats: {'dsp': ['seat 1']}, - version: '1.3', + version: '1.4', width: 100, height: 100, cookieSupport: true, @@ -177,7 +177,7 @@ describe('Livewrapped adapter tests', function () { userId: 'user id', url: 'https://www.domain.com', seats: {'dsp': ['seat 1']}, - version: '1.3', + version: '1.4', width: 100, height: 100, cookieSupport: true, @@ -208,7 +208,7 @@ describe('Livewrapped adapter tests', function () { auctionId: 'F7557995-65F5-4682-8782-7D5D34D82A8C', publisherId: '26947112-2289-405D-88C1-A7340C57E63E', url: 'https://www.domain.com', - version: '1.3', + version: '1.4', width: 100, height: 100, cookieSupport: true, @@ -238,7 +238,7 @@ describe('Livewrapped adapter tests', function () { let expectedQuery = { auctionId: 'F7557995-65F5-4682-8782-7D5D34D82A8C', url: 'https://www.domain.com', - version: '1.3', + version: '1.4', width: 100, height: 100, cookieSupport: true, @@ -270,7 +270,7 @@ describe('Livewrapped adapter tests', function () { auctionId: 'F7557995-65F5-4682-8782-7D5D34D82A8C', publisherId: '26947112-2289-405D-88C1-A7340C57E63E', url: 'https://www.domain.com', - version: '1.3', + version: '1.4', width: 100, height: 100, deviceId: 'deviceid', @@ -303,7 +303,7 @@ describe('Livewrapped adapter tests', function () { auctionId: 'F7557995-65F5-4682-8782-7D5D34D82A8C', publisherId: '26947112-2289-405D-88C1-A7340C57E63E', url: 'https://www.domain.com', - version: '1.3', + version: '1.4', width: 100, height: 100, tid: 'tracking id', @@ -335,7 +335,7 @@ describe('Livewrapped adapter tests', function () { auctionId: 'F7557995-65F5-4682-8782-7D5D34D82A8C', publisherId: '26947112-2289-405D-88C1-A7340C57E63E', url: 'https://www.domain.com', - version: '1.3', + version: '1.4', width: 100, height: 100, cookieSupport: true, @@ -366,7 +366,7 @@ describe('Livewrapped adapter tests', function () { auctionId: 'F7557995-65F5-4682-8782-7D5D34D82A8C', publisherId: '26947112-2289-405D-88C1-A7340C57E63E', url: 'https://www.domain.com', - version: '1.3', + version: '1.4', width: 100, height: 100, cookieSupport: true, @@ -397,7 +397,7 @@ describe('Livewrapped adapter tests', function () { auctionId: 'F7557995-65F5-4682-8782-7D5D34D82A8C', publisherId: '26947112-2289-405D-88C1-A7340C57E63E', url: 'https://www.domain.com', - version: '1.3', + version: '1.4', width: 100, height: 100, cookieSupport: true, @@ -428,7 +428,7 @@ describe('Livewrapped adapter tests', function () { auctionId: 'F7557995-65F5-4682-8782-7D5D34D82A8C', publisherId: '26947112-2289-405D-88C1-A7340C57E63E', url: 'https://www.domain.com', - version: '1.3', + version: '1.4', width: 100, height: 100, cookieSupport: true, @@ -445,6 +445,37 @@ describe('Livewrapped adapter tests', function () { expect(data).to.deep.equal(expectedQuery); }); + it('should make a well-formed single request object with video only parameters', function() { + sandbox.stub(utils, 'isSafariBrowser').callsFake(() => false); + sandbox.stub(storage, 'cookiesAreEnabled').callsFake(() => true); + let testbidRequest = clone(bidderRequest); + delete testbidRequest.bids[0].params.userId; + delete testbidRequest.bids[0].params.seats; + delete testbidRequest.bids[0].params.adUnitId; + testbidRequest.bids[0].mediaTypes = {'video': {'videodata': 'content parsed serverside only'}}; + let result = spec.buildRequests(testbidRequest.bids, testbidRequest); + let data = JSON.parse(result.data); + + let expectedQuery = { + auctionId: 'F7557995-65F5-4682-8782-7D5D34D82A8C', + publisherId: '26947112-2289-405D-88C1-A7340C57E63E', + url: 'https://www.domain.com', + version: '1.4', + width: 100, + height: 100, + cookieSupport: true, + adRequests: [{ + callerAdUnitId: 'panorama_d_1', + bidId: '2ffb201a808da7', + transactionId: '3D1C8CF7-D288-4D7F-8ADD-97C553056C3D', + formats: [{width: 980, height: 240}, {width: 980, height: 120}], + video: {'videodata': 'content parsed serverside only'} + }] + }; + + expect(data).to.deep.equal(expectedQuery); + }); + it('should use app objects', function() { sandbox.stub(utils, 'isSafariBrowser').callsFake(() => false); sandbox.stub(storage, 'cookiesAreEnabled').callsFake(() => true); @@ -474,7 +505,7 @@ describe('Livewrapped adapter tests', function () { userId: 'user id', url: 'https://appdomain.com', seats: {'dsp': ['seat 1']}, - version: '1.3', + version: '1.4', width: 300, height: 200, ifa: 'ifa', @@ -507,7 +538,7 @@ describe('Livewrapped adapter tests', function () { auctionId: 'F7557995-65F5-4682-8782-7D5D34D82A8C', publisherId: '26947112-2289-405D-88C1-A7340C57E63E', url: 'https://www.domain.com', - version: '1.3', + version: '1.4', width: 100, height: 100, cookieSupport: true, @@ -541,7 +572,7 @@ describe('Livewrapped adapter tests', function () { userId: 'user id', url: 'https://www.domain.com', seats: {'dsp': ['seat 1']}, - version: '1.3', + version: '1.4', width: 100, height: 100, cookieSupport: true, @@ -577,7 +608,7 @@ describe('Livewrapped adapter tests', function () { userId: 'user id', url: 'https://www.domain.com', seats: {'dsp': ['seat 1']}, - version: '1.3', + version: '1.4', width: 100, height: 100, cookieSupport: true, @@ -594,6 +625,79 @@ describe('Livewrapped adapter tests', function () { expect(data).to.deep.equal(expectedQuery); }); + it('should pass us privacy parameter', function() { + sandbox.stub(utils, 'isSafariBrowser').callsFake(() => false); + sandbox.stub(storage, 'cookiesAreEnabled').callsFake(() => true); + let testRequest = clone(bidderRequest); + testRequest.uspConsent = '1---'; + let result = spec.buildRequests(testRequest.bids, testRequest); + let data = JSON.parse(result.data); + + expect(result.url).to.equal('https://lwadm.com/ad'); + + let expectedQuery = { + auctionId: 'F7557995-65F5-4682-8782-7D5D34D82A8C', + publisherId: '26947112-2289-405D-88C1-A7340C57E63E', + userId: 'user id', + url: 'https://www.domain.com', + seats: {'dsp': ['seat 1']}, + version: '1.4', + width: 100, + height: 100, + cookieSupport: true, + usPrivacy: '1---', + adRequests: [{ + adUnitId: '9E153CED-61BC-479E-98DF-24DC0D01BA37', + callerAdUnitId: 'panorama_d_1', + bidId: '2ffb201a808da7', + transactionId: '3D1C8CF7-D288-4D7F-8ADD-97C553056C3D', + formats: [{width: 980, height: 240}, {width: 980, height: 120}] + }] + }; + + expect(data).to.deep.equal(expectedQuery); + }); + + it('should pass coppa parameter', function() { + sandbox.stub(utils, 'isSafariBrowser').callsFake(() => false); + sandbox.stub(storage, 'cookiesAreEnabled').callsFake(() => true); + + let origGetConfig = config.getConfig; + sandbox.stub(config, 'getConfig').callsFake(function (key) { + if (key === 'coppa') { + return true; + } + return origGetConfig.apply(config, arguments); + }); + + let result = spec.buildRequests(bidderRequest.bids, bidderRequest); + let data = JSON.parse(result.data); + + expect(result.url).to.equal('https://lwadm.com/ad'); + + let expectedQuery = { + auctionId: 'F7557995-65F5-4682-8782-7D5D34D82A8C', + publisherId: '26947112-2289-405D-88C1-A7340C57E63E', + userId: 'user id', + url: 'https://www.domain.com', + seats: {'dsp': ['seat 1']}, + version: '1.4', + width: 100, + height: 100, + cookieSupport: true, + coppa: true, + adRequests: [{ + adUnitId: '9E153CED-61BC-479E-98DF-24DC0D01BA37', + callerAdUnitId: 'panorama_d_1', + bidId: '2ffb201a808da7', + transactionId: '3D1C8CF7-D288-4D7F-8ADD-97C553056C3D', + formats: [{width: 980, height: 240}, {width: 980, height: 120}] + }] + }; + + expect(data).to.deep.equal(expectedQuery); + }); + it('should pass no cookie support', function() { sandbox.stub(storage, 'cookiesAreEnabled').callsFake(() => false); sandbox.stub(utils, 'isSafariBrowser').callsFake(() => false); @@ -608,7 +712,7 @@ describe('Livewrapped adapter tests', function () { userId: 'user id', url: 'https://www.domain.com', seats: {'dsp': ['seat 1']}, - version: '1.3', + version: '1.4', width: 100, height: 100, cookieSupport: false, @@ -638,7 +742,7 @@ describe('Livewrapped adapter tests', function () { userId: 'user id', url: 'https://www.domain.com', seats: {'dsp': ['seat 1']}, - version: '1.3', + version: '1.4', width: 100, height: 100, cookieSupport: false, @@ -701,7 +805,7 @@ describe('Livewrapped adapter tests', function () { userId: 'pubcid 123', url: 'https://www.domain.com', seats: {'dsp': ['seat 1']}, - version: '1.3', + version: '1.4', width: 100, height: 100, cookieSupport: true, @@ -733,7 +837,7 @@ describe('Livewrapped adapter tests', function () { userId: 'user id', url: 'https://www.domain.com', seats: {'dsp': ['seat 1']}, - version: '1.3', + version: '1.4', width: 100, height: 100, cookieSupport: true, @@ -750,42 +854,45 @@ describe('Livewrapped adapter tests', function () { }); }); - it('should make use of Id5-Id if available', function() { + it('should make use of user ids if available', function() { sandbox.stub(utils, 'isSafariBrowser').callsFake(() => false); sandbox.stub(storage, 'cookiesAreEnabled').callsFake(() => true); let testbidRequest = clone(bidderRequest); delete testbidRequest.bids[0].params.userId; - testbidRequest.bids[0].userId = {}; - testbidRequest.bids[0].userId.id5id = 'id5-user-id'; - let result = spec.buildRequests(testbidRequest.bids, testbidRequest); - let data = JSON.parse(result.data); - - expect(data.rtbData.user.ext.eids).to.deep.equal([{ - 'source': 'id5-sync.com', - 'uids': [{ - 'id': 'id5-user-id', - 'atype': 1 - }] - }]); - }); + testbidRequest.bids[0].userIdAsEids = [ + { + 'source': 'id5-sync.com', + 'uids': [{ + 'id': 'ID5-id', + 'atype': 1, + 'ext': { + 'linkType': 2 + } + }] + }, + { + 'source': 'pubcid.org', + 'uids': [{ + 'id': 'publisher-common-id', + 'atype': 1 + }] + }, + { + 'source': 'sharedid.org', + 'uids': [{ + 'id': 'sharedid', + 'atype': 1, + 'ext': { + 'third': 'sharedid' + } + }] + } + ]; - it('should make use of publisher common Id if available', function() { - sandbox.stub(utils, 'isSafariBrowser').callsFake(() => false); - sandbox.stub(storage, 'cookiesAreEnabled').callsFake(() => true); - let testbidRequest = clone(bidderRequest); - delete testbidRequest.bids[0].params.userId; - testbidRequest.bids[0].userId = {}; - testbidRequest.bids[0].userId.pubcid = 'publisher-common-id'; let result = spec.buildRequests(testbidRequest.bids, testbidRequest); let data = JSON.parse(result.data); - expect(data.rtbData.user.ext.eids).to.deep.equal([{ - 'source': 'pubcommon', - 'uids': [{ - 'id': 'publisher-common-id', - 'atype': 1 - }] - }]); + expect(data.rtbData.user.ext.eids).to.deep.equal(testbidRequest.bids[0].userIdAsEids); }); it('should send schain object if available', function() { @@ -895,6 +1002,48 @@ describe('Livewrapped adapter tests', function () { expect(bids).to.deep.equal(expectedResponse); }) + it('should handle single video success response', function() { + let lwResponse = { + ads: [ + { + id: '28e5ddf4-3c01-11e8-86a7-0a44794250d4', + callerId: 'site_outsider_0', + tag: 'VAST XML', + video: {}, + width: 300, + height: 250, + cpmBid: 2.565917, + bidId: '32e50fad901ae89', + auctionId: '13e674db-d4d8-4e19-9d28-ff38177db8bf', + creativeId: '52cbd598-2715-4c43-a06f-229fc170f945:427077', + ttl: 120, + meta: undefined + } + ], + currency: 'USD' + }; + + let expectedResponse = [{ + requestId: '32e50fad901ae89', + bidderCode: 'livewrapped', + cpm: 2.565917, + width: 300, + height: 250, + ad: 'VAST XML', + ttl: 120, + creativeId: '52cbd598-2715-4c43-a06f-229fc170f945:427077', + netRevenue: true, + currency: 'USD', + meta: undefined, + vastXml: 'VAST XML', + mediaType: VIDEO + }]; + + let bids = spec.interpretResponse({body: lwResponse}); + + expect(bids).to.deep.equal(expectedResponse); + }) + it('should handle multiple success response', function() { let lwResponse = { ads: [ diff --git a/test/spec/modules/lkqdBidAdapter_spec.js b/test/spec/modules/lkqdBidAdapter_spec.js index f10d936a28b..1cd33d9ec59 100644 --- a/test/spec/modules/lkqdBidAdapter_spec.js +++ b/test/spec/modules/lkqdBidAdapter_spec.js @@ -1,6 +1,7 @@ import { spec } from 'modules/lkqdBidAdapter.js'; import { newBidder } from 'src/adapters/bidderFactory.js'; -const { expect } = require('chai'); +import { config } from 'src/config.js'; +import { expect } from 'chai'; describe('LKQD Bid Adapter Test', () => { const adapter = newBidder(spec); @@ -47,7 +48,9 @@ describe('LKQD Bid Adapter Test', () => { 'bidder': 'lkqd', 'params': { 'siteId': '662921', - 'placementId': '263' + 'placementId': '263', + 'c1': 'newWindow', + 'c20': 'lkqdCustom' }, 'adUnitCode': 'lkqd', 'sizes': [[300, 250], [640, 480]], @@ -75,6 +78,10 @@ describe('LKQD Bid Adapter Test', () => { ]; it('should populate available parameters', () => { + sinon.stub(config, 'getConfig') + .withArgs('coppa') + .returns(true); + const requests = spec.buildRequests(bidRequests); expect(requests.length).to.equal(2); const r1 = requests[0].data; @@ -82,14 +89,26 @@ describe('LKQD Bid Adapter Test', () => { expect(r1).to.have.string('&sid=662921&'); expect(r1).to.have.string('&width=300&'); expect(r1).to.have.string('&height=250&'); + expect(r1).to.have.string('&coppa=1&'); + expect(r1).to.have.string('&c1=newWindow&'); + expect(r1).to.have.string('&c20=lkqdCustom'); const r2 = requests[1].data; expect(r2).to.have.string('pid=263&'); expect(r2).to.have.string('&sid=662921&'); expect(r2).to.have.string('&width=640&'); expect(r2).to.have.string('&height=480&'); + expect(r2).to.have.string('&coppa=1&'); + expect(r2).to.have.string('&c1=newWindow&'); + expect(r2).to.have.string('&c20=lkqdCustom'); + + config.getConfig.restore(); }); it('should not populate unspecified parameters', () => { + sinon.stub(config, 'getConfig') + .withArgs('coppa') + .returns(false); + const requests = spec.buildRequests(bidRequests); expect(requests.length).to.equal(2); const r1 = requests[0].data; @@ -99,6 +118,8 @@ describe('LKQD Bid Adapter Test', () => { expect(r1).to.not.have.string('&contentlength='); expect(r1).to.not.have.string('&contenturl='); expect(r1).to.not.have.string('&schain='); + expect(r1).to.not.have.string('&c10='); + expect(r1).to.not.have.string('coppa'); const r2 = requests[1].data; expect(r2).to.not.have.string('&dnt='); expect(r2).to.not.have.string('&contentid='); @@ -106,6 +127,10 @@ describe('LKQD Bid Adapter Test', () => { expect(r2).to.not.have.string('&contentlength='); expect(r2).to.not.have.string('&contenturl='); expect(r2).to.not.have.string('&schain='); + expect(r2).to.not.have.string('&c39='); + expect(r2).to.not.have.string('coppa'); + + config.getConfig.restore(); }); it('should handle single size request', () => { 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/logicadBidAdapter_spec.js b/test/spec/modules/logicadBidAdapter_spec.js index eddcaecac7b..6eb8dc1c043 100644 --- a/test/spec/modules/logicadBidAdapter_spec.js +++ b/test/spec/modules/logicadBidAdapter_spec.js @@ -19,7 +19,65 @@ describe('LogicadAdapter', function () { banner: { sizes: [[300, 250], [300, 600]] } - } + }, + userId: { + sharedid: { + id: 'fakesharedid', + third: 'fakesharedid' + } + }, + userIdAsEids: [{ + source: 'sharedid.org', + uids: [{ + id: 'fakesharedid', + atype: 1, + ext: { + third: 'fakesharedid' + } + }] + }] + }]; + const nativeBidRequests = [{ + bidder: 'logicad', + bidId: '51ef8751f9aead', + params: { + tid: 'bgjD1', + page: 'https://www.logicad.com/' + }, + adUnitCode: 'div-gpt-ad-1460505748561-1', + transactionId: 'd7b773de-ceaa-484d-89ca-d9f51b8d61ec', + sizes: [[1, 1]], + bidderRequestId: '418b37f85e772c', + auctionId: '18fd8b8b0bd757', + mediaTypes: { + native: { + title: { + required: true + }, + image: { + required: true + }, + sponsoredBy: { + required: true + } + } + }, + userId: { + sharedid: { + id: 'fakesharedid', + third: 'fakesharedid' + } + }, + userIdAsEids: [{ + source: 'sharedid.org', + uids: [{ + id: 'fakesharedid', + atype: 1, + ext: { + third: 'fakesharedid' + } + }] + }] }]; const bidderRequest = { refererInfo: { @@ -52,6 +110,40 @@ describe('LogicadAdapter', function () { } } }; + const nativeServerResponse = { + body: { + seatbid: + [{ + bid: { + requestId: '51ef8751f9aead', + cpm: 101.0234, + width: 1, + height: 1, + creativeId: '2019', + currency: 'JPY', + netRevenue: true, + ttl: 60, + native: { + clickUrl: 'https://www.logicad.com', + image: { + url: 'https://cd.ladsp.com/img.png', + width: '1200', + height: '628' + }, + impressionTrackers: [ + 'https://example.com' + ], + sponsoredBy: 'Logicad', + title: 'Native Creative', + } + } + }], + userSync: { + type: 'image', + url: 'https://cr-p31.ladsp.jp/cookiesender/31' + } + } + }; describe('isBidRequestValid', function () { it('should return true if the tid parameter is present', function () { @@ -69,6 +161,10 @@ describe('LogicadAdapter', function () { delete bidRequest[0].params; expect(spec.isBidRequestValid(bidRequest)).to.be.false; }); + + it('should return true if the tid parameter is present for native request', function () { + expect(spec.isBidRequestValid(nativeBidRequests[0])).to.be.true; + }); }); describe('buildRequests', function () { @@ -77,6 +173,11 @@ describe('LogicadAdapter', function () { expect(request.method).to.equal('POST'); expect(request.url).to.equal('https://pb.ladsp.com/adrequest/prebid'); expect(request.data).to.exist; + + const data = JSON.parse(request.data); + expect(data.auctionId).to.equal('18fd8b8b0bd757'); + expect(data.eids[0].source).to.equal('sharedid.org'); + expect(data.eids[0].uids[0].id).to.equal('fakesharedid'); }); }); @@ -101,6 +202,27 @@ describe('LogicadAdapter', function () { expect(interpretedResponse[0].netRevenue).to.equal(serverResponse.body.seatbid[0].bid.netRevenue); expect(interpretedResponse[0].ad).to.equal(serverResponse.body.seatbid[0].bid.ad); expect(interpretedResponse[0].ttl).to.equal(serverResponse.body.seatbid[0].bid.ttl); + + // native + const nativeRequest = spec.buildRequests(nativeBidRequests, bidderRequest)[0]; + const interpretedResponseForNative = spec.interpretResponse(nativeServerResponse, nativeRequest); + + expect(interpretedResponseForNative).to.have.lengthOf(1); + + expect(interpretedResponseForNative[0].requestId).to.equal(nativeServerResponse.body.seatbid[0].bid.requestId); + expect(interpretedResponseForNative[0].cpm).to.equal(nativeServerResponse.body.seatbid[0].bid.cpm); + expect(interpretedResponseForNative[0].width).to.equal(nativeServerResponse.body.seatbid[0].bid.width); + expect(interpretedResponseForNative[0].height).to.equal(nativeServerResponse.body.seatbid[0].bid.height); + expect(interpretedResponseForNative[0].creativeId).to.equal(nativeServerResponse.body.seatbid[0].bid.creativeId); + expect(interpretedResponseForNative[0].currency).to.equal(nativeServerResponse.body.seatbid[0].bid.currency); + expect(interpretedResponseForNative[0].netRevenue).to.equal(nativeServerResponse.body.seatbid[0].bid.netRevenue); + expect(interpretedResponseForNative[0].ttl).to.equal(nativeServerResponse.body.seatbid[0].bid.ttl); + expect(interpretedResponseForNative[0].native.clickUrl).to.equal(nativeServerResponse.body.seatbid[0].bid.native.clickUrl); + expect(interpretedResponseForNative[0].native.image.url).to.equal(nativeServerResponse.body.seatbid[0].bid.native.image.url); + expect(interpretedResponseForNative[0].native.image.width).to.equal(nativeServerResponse.body.seatbid[0].bid.native.image.width); + expect(interpretedResponseForNative[0].native.impressionTrackers).to.equal(nativeServerResponse.body.seatbid[0].bid.native.impressionTrackers); + expect(interpretedResponseForNative[0].native.sponsoredBy).to.equal(nativeServerResponse.body.seatbid[0].bid.native.sponsoredBy); + expect(interpretedResponseForNative[0].native.title).to.equal(nativeServerResponse.body.seatbid[0].bid.native.title); }); }); 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/lunamediahbBidAdapter_spec.js b/test/spec/modules/lunamediahbBidAdapter_spec.js new file mode 100644 index 00000000000..e9f88935ed5 --- /dev/null +++ b/test/spec/modules/lunamediahbBidAdapter_spec.js @@ -0,0 +1,304 @@ +import {expect} from 'chai'; +import {spec} from '../../../modules/lunamediahbBidAdapter.js'; +import { BANNER, VIDEO, NATIVE } from '../../../src/mediaTypes.js'; + +describe('LunamediaHBBidAdapter', function () { + const bid = { + bidId: '23fhj33i987f', + bidder: 'lunamediahb', + 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://balancer.lmgssp.com/?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; + }); + }); +}); diff --git a/test/spec/modules/luponmediaBidAdapter_spec.js b/test/spec/modules/luponmediaBidAdapter_spec.js index 39c915e38b8..8aeecc87c98 100644 --- a/test/spec/modules/luponmediaBidAdapter_spec.js +++ b/test/spec/modules/luponmediaBidAdapter_spec.js @@ -364,4 +364,49 @@ describe('luponmediaBidAdapter', function () { expect(checkSchain).to.equal(false); }); }); + + describe('onBidWon', function () { + const bidWonEvent = { + 'bidderCode': 'luponmedia', + 'width': 300, + 'height': 250, + 'statusMessage': 'Bid available', + 'adId': '105bbf8c54453ff', + 'requestId': '934b8752185955', + 'mediaType': 'banner', + 'source': 'client', + 'cpm': 0.364, + 'creativeId': '443801010', + 'currency': 'USD', + 'netRevenue': false, + 'ttl': 300, + 'referrer': '', + 'ad': '', + 'auctionId': '926a8ea3-3dd4-4bf2-95ab-c85c2ce7e99b', + 'responseTimestamp': 1598527728026, + 'requestTimestamp': 1598527727629, + 'bidder': 'luponmedia', + 'adUnitCode': 'div-gpt-ad-1533155193780-5', + 'timeToRespond': 397, + 'size': '300x250', + 'status': 'rendered' + }; + + let ajaxStub; + + beforeEach(() => { + ajaxStub = sinon.stub(spec, 'sendWinningsToServer') + }) + + afterEach(() => { + ajaxStub.restore() + }) + + it('calls luponmedia\'s callback endpoint', () => { + const result = spec.onBidWon(bidWonEvent); + expect(result).to.equal(undefined); + expect(ajaxStub.calledOnce).to.equal(true); + expect(ajaxStub.firstCall.args[0]).to.deep.equal(JSON.stringify(bidWonEvent)); + }); + }); }); diff --git a/test/spec/modules/malltvBidAdapter_spec.js b/test/spec/modules/malltvBidAdapter_spec.js new file mode 100644 index 00000000000..ffe08ad1a5e --- /dev/null +++ b/test/spec/modules/malltvBidAdapter_spec.js @@ -0,0 +1,168 @@ +import { expect } from 'chai'; +import { spec } from 'modules/malltvBidAdapter'; + +describe('malltvAdapterTest', () => { + describe('bidRequestValidity', () => { + it('bidRequest with propertyId and placementId', () => { + expect(spec.isBidRequestValid({ + bidder: 'malltv', + params: { + propertyId: '{propertyId}', + placementId: '{placementId}' + } + })).to.equal(true); + }); + + it('bidRequest without propertyId', () => { + expect(spec.isBidRequestValid({ + bidder: 'malltv', + params: { + placementId: '{placementId}' + } + })).to.equal(false); + }); + + it('bidRequest without placementId', () => { + expect(spec.isBidRequestValid({ + bidder: 'malltv', + params: { + propertyId: '{propertyId}', + } + })).to.equal(false); + }); + + it('bidRequest without propertyId or placementId', () => { + expect(spec.isBidRequestValid({ + bidder: 'malltv', + params: {} + })).to.equal(false); + }); + }); + + describe('bidRequest', () => { + const bidRequests = [{ + 'bidder': 'malltv', + 'params': { + 'propertyId': '{propertyId}', + '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', + 'sizes': [[300, 250]], + 'bidId': '10bdc36fe0b48c8', + 'bidderRequestId': '70deaff71c281d', + 'auctionId': 'f9012acc-b6b7-4748-9098-97252914f9dc' + }]; + + it('bidRequest HTTP method', () => { + const requests = spec.buildRequests(bidRequests); + requests.forEach(function (requestItem) { + expect(requestItem.method).to.equal('POST'); + }); + }); + + it('bidRequest url', () => { + const endpointUrl = 'https://central.mall.tv/bid'; + const requests = spec.buildRequests(bidRequests); + requests.forEach(function (requestItem) { + expect(requestItem.url).to.match(new RegExp(`${endpointUrl}`)); + }); + }); + + it('bidRequest data', () => { + const requests = spec.buildRequests(bidRequests); + requests.forEach(function (requestItem) { + expect(requestItem.data).to.exist; + }); + }); + + it('bidRequest sizes', () => { + const requests = spec.buildRequests(bidRequests); + requests.forEach(function (requestItem) { + expect(requestItem.data.placements).to.exist; + expect(requestItem.data.placements.length).to.equal(1); + 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', () => { + const bidRequest = { + 'method': 'POST', + 'url': 'https://central.mall.tv/bid', + 'data': { + 'sizes': '300x250', + 'adUnitId': 'rectangle', + 'placementId': '{placementId}', + 'propertyId': '{propertyId}', + 'pageViewGuid': '{pageViewGuid}', + 'url': 'http://localhost:9999/integrationExamples/gpt/hello_world.html?pbjs_debug=true', + 'requestid': '26ee8fe87940da7', + 'bidid': '2962dbedc4768bf' + } + }; + + const bidResponse = { + body: [{ + 'CPM': 1, + 'Width': 300, + 'Height': 250, + 'Referrer': 'http://localhost:9999/integrationExamples/gpt/hello_world.html?pbjs_debug=true', + 'Ad': '
Test ad
', + 'CreativeId': '123abc', + 'NetRevenue': false, + 'Currency': 'EUR', + 'TTL': 360 + }], + headers: {} + }; + + it('all keys present', () => { + const result = spec.interpretResponse(bidResponse, bidRequest); + + let keys = [ + 'requestId', + 'cpm', + 'width', + 'height', + 'creativeId', + 'currency', + 'netRevenue', + 'ttl', + 'referrer', + 'ad', + 'vastUrl', + 'mediaType' + ]; + + let resultKeys = Object.keys(result[0]); + resultKeys.forEach(function (key) { + expect(keys.indexOf(key) !== -1).to.equal(true); + }); + }) + }); +}); diff --git a/test/spec/modules/marsmediaBidAdapter_spec.js b/test/spec/modules/marsmediaBidAdapter_spec.js index b4c2fe68f34..cf074b0f3d6 100644 --- a/test/spec/modules/marsmediaBidAdapter_spec.js +++ b/test/spec/modules/marsmediaBidAdapter_spec.js @@ -1,11 +1,41 @@ -import {spec} from '../../../modules/marsmediaBidAdapter.js'; -import * as utils from '../../../src/utils.js'; -import * as sinon from 'sinon'; +import { spec } from 'modules/marsmediaBidAdapter.js'; +import * as utils from 'src/utils.js'; +import { config } from 'src/config.js'; -var r1adapter = spec; +var marsAdapter = spec; describe('marsmedia adapter tests', function () { + let element, win; + let sandbox; + beforeEach(function() { + element = { + x: 0, + y: 0, + + width: 0, + height: 0, + + getBoundingClientRect: () => { + return { + width: element.width, + height: element.height, + + left: element.x, + top: element.y, + right: element.x + element.width, + bottom: element.y + element.height + }; + } + }; + win = { + document: { + visibilityState: 'visible' + }, + + innerWidth: 800, + innerHeight: 600 + }; this.defaultBidderRequest = { 'refererInfo': { 'referer': 'Reference Page', @@ -15,28 +45,40 @@ describe('marsmedia adapter tests', function () { ] } }; + + this.defaultBidRequestList = [ + { + 'bidder': 'marsmedia', + 'params': { + 'zoneId': 9999 + }, + 'mediaTypes': { + 'banner': { + 'sizes': [[300, 250]] + } + }, + 'adUnitCode': 'Unit-Code', + 'transactionId': 'd7b773de-ceaa-484d-89ca-d9f51b8d61ec', + 'bidderRequestId': '418b37f85e772c', + 'auctionId': '18fd8b8b0bd757', + 'bidRequestsCount': 1, + 'bidId': '51ef8751f9aead' + } + ]; + + sandbox = sinon.sandbox.create(); + sandbox.stub(document, 'getElementById').withArgs('Unit-Code').returns(element); + sandbox.stub(utils, 'getWindowTop').returns(win); + sandbox.stub(utils, 'getWindowSelf').returns(win); + }); + + afterEach(function() { + sandbox.restore(); }); describe('Verify 1.0 POST Banner Bid Request', function () { it('buildRequests works', function () { - var bidRequestList = [ - { - 'bidder': 'marsmedia', - 'params': { - 'zoneId': 9999 - }, - 'mediaType': 'banner', - 'adUnitCode': 'div-gpt-ad-1438287399331-0', - 'sizes': [[300, 250]], - 'transactionId': 'd7b773de-ceaa-484d-89ca-d9f51b8d61ec', - 'bidderRequestId': '418b37f85e772c', - 'auctionId': '18fd8b8b0bd757', - 'bidRequestsCount': 1, - 'bidId': '51ef8751f9aead' - } - ]; - - var bidRequest = r1adapter.buildRequests(bidRequestList, this.defaultBidderRequest); + var bidRequest = marsAdapter.buildRequests(this.defaultBidRequestList, this.defaultBidderRequest); expect(bidRequest.url).to.have.string('https://hb.go2speed.media/bidder/?bid=3mhdom&zoneId=9999&hbv='); expect(bidRequest.method).to.equal('POST'); @@ -52,11 +94,11 @@ describe('marsmedia adapter tests', function () { expect(openrtbRequest.imp[0].ext.bidder.zoneId).to.equal(9999); }); - it('interpretResponse works', function() { + /* it('interpretResponse works', function() { var bidList = { 'body': [ { - 'impid': 'div-gpt-ad-1438287399331-0', + 'impid': 'Unit-Code', 'w': 300, 'h': 250, 'adm': '
My Compelling Ad
', @@ -67,7 +109,7 @@ describe('marsmedia adapter tests', function () { ] }; - var bannerBids = r1adapter.interpretResponse(bidList); + var bannerBids = marsAdapter.interpretResponse(bidList); expect(bannerBids.length).to.equal(1); const bid = bannerBids[0]; @@ -78,7 +120,7 @@ describe('marsmedia adapter tests', function () { expect(bid.netRevenue).to.equal(true); expect(bid.cpm).to.equal(1.0); expect(bid.ttl).to.equal(350); - }); + }); */ }); describe('Verify POST Video Bid Request', function() { @@ -95,7 +137,7 @@ describe('marsmedia adapter tests', function () { 'context': 'instream' } }, - 'adUnitCode': 'div-gpt-ad-1438287399331-1', + 'adUnitCode': 'Unit-Code', 'sizes': [ [300, 250] ], @@ -107,7 +149,7 @@ describe('marsmedia adapter tests', function () { } ]; - var bidRequest = r1adapter.buildRequests(bidRequestList, this.defaultBidderRequest); + var bidRequest = marsAdapter.buildRequests(bidRequestList, this.defaultBidderRequest); expect(bidRequest.url).to.have.string('https://hb.go2speed.media/bidder/?bid=3mhdom&zoneId=9999&hbv='); expect(bidRequest.method).to.equal('POST'); @@ -132,7 +174,7 @@ describe('marsmedia adapter tests', function () { var bidList = { 'body': [ { - 'impid': 'div-gpt-ad-1438287399331-1', + 'impid': 'Unit-Code', 'price': 1, 'adm': 'https://example.com/', 'adomain': [ @@ -147,7 +189,7 @@ describe('marsmedia adapter tests', function () { ] }; - var videoBids = r1adapter.interpretResponse(bidList); + var videoBids = marsAdapter.interpretResponse(bidList); expect(videoBids.length).to.equal(1); const bid = videoBids[0]; @@ -166,7 +208,7 @@ describe('marsmedia adapter tests', function () { var bidList = { 'body': [ { - 'impid': 'div-gpt-ad-1438287399331-1', + 'impid': 'Unit-Code', 'price': 1, 'adm': '', 'adomain': [ @@ -181,7 +223,7 @@ describe('marsmedia adapter tests', function () { ] }; - var videoBids = r1adapter.interpretResponse(bidList); + var videoBids = marsAdapter.interpretResponse(bidList); expect(videoBids.length).to.equal(1); const bid = videoBids[0]; @@ -199,26 +241,6 @@ describe('marsmedia adapter tests', function () { describe('misc buildRequests', function() { it('should send GDPR Consent data to Marsmedia tag', function () { - var bidRequestList = [ - { - 'bidder': 'marsmedia', - 'params': { - 'zoneId': 9999 - }, - 'mediaTypes': { - 'banner': { - 'sizes': [[300, 250]] - } - }, - 'adUnitCode': 'div-gpt-ad-1438287399331-3', - 'transactionId': 'd7b773de-ceaa-484d-89ca-d9f51b8d61ec', - 'bidderRequestId': '418b37f85e772c', - 'auctionId': '18fd8b8b0bd757', - 'bidRequestsCount': 1, - 'bidId': '51ef8751f9aead' - } - ]; - var consentString = 'testConsentString'; var gdprBidderRequest = this.defaultBidderRequest; gdprBidderRequest.gdprConsent = { @@ -226,13 +248,50 @@ describe('marsmedia adapter tests', function () { 'consentString': consentString }; - var bidRequest = r1adapter.buildRequests(bidRequestList, gdprBidderRequest); + var bidRequest = marsAdapter.buildRequests(this.defaultBidRequestList, gdprBidderRequest); const openrtbRequest = JSON.parse(bidRequest.data); expect(openrtbRequest.user.ext.consent).to.equal(consentString); expect(openrtbRequest.regs.ext.gdpr).to.equal(true); }); + it('should have CCPA Consent if defined', function () { + const ccpaBidderRequest = this.defaultBidderRequest; + ccpaBidderRequest.uspConsent = '1YYN'; + const bidRequest = marsAdapter.buildRequests(this.defaultBidRequestList, ccpaBidderRequest); + const openrtbRequest = JSON.parse(bidRequest.data); + + expect(openrtbRequest.regs.ext.us_privacy).to.equal('1YYN'); + }); + + it('should submit coppa if set in config', function () { + sinon.stub(config, 'getConfig') + .withArgs('coppa') + .returns(true); + const request = marsAdapter.buildRequests(this.defaultBidRequestList, this.defaultBidderRequest); + const requestparse = JSON.parse(request.data); + expect(requestparse.regs.coppa).to.equal(1); + config.getConfig.restore(); + }); + + it('should process floors module if available', function() { + const floorBidderRequest = this.defaultBidRequestList; + const floorInfo = { + currency: 'USD', + floor: 1.20 + }; + floorBidderRequest[0].getFloor = () => floorInfo; + const request = marsAdapter.buildRequests(floorBidderRequest, this.defaultBidderRequest); + const requestparse = JSON.parse(request.data); + expect(requestparse.imp[0].bidfloor).to.equal(1.20); + }); + + it('should have 0 bidfloor value', function() { + const request = marsAdapter.buildRequests(this.defaultBidRequestList, this.defaultBidderRequest); + const requestparse = JSON.parse(request.data); + expect(requestparse.imp[0].bidfloor).to.equal(0); + }); + it('prefer 2.0 sizes', function () { var bidRequestList = [ { @@ -245,7 +304,7 @@ describe('marsmedia adapter tests', function () { 'sizes': [[300, 600]] } }, - 'adUnitCode': 'div-gpt-ad-1438287399331-0', + 'adUnitCode': 'Unit-Code', 'sizes': [[300, 250]], 'transactionId': 'd7b773de-ceaa-484d-89ca-d9f51b8d61ec', 'bidderRequestId': '418b37f85e772c', @@ -255,7 +314,7 @@ describe('marsmedia adapter tests', function () { } ]; - var bidRequest = r1adapter.buildRequests(bidRequestList, this.defaultBidderRequest); + var bidRequest = marsAdapter.buildRequests(bidRequestList, this.defaultBidderRequest); const openrtbRequest = JSON.parse(bidRequest.data); expect(openrtbRequest.imp[0].banner.format[0].w).to.equal(300); @@ -274,7 +333,7 @@ describe('marsmedia adapter tests', function () { 'sizes': [[300]] } }, - 'adUnitCode': 'div-gpt-ad-1438287399331-0', + 'adUnitCode': 'Unit-Code', 'transactionId': 'd7b773de-ceaa-484d-89ca-d9f51b8d61ec', 'bidderRequestId': '418b37f85e772c', 'auctionId': '18fd8b8b0bd757', @@ -283,7 +342,7 @@ describe('marsmedia adapter tests', function () { } ]; - var bidRequest = r1adapter.buildRequests(bidRequestList, this.defaultBidderRequest); + var bidRequest = marsAdapter.buildRequests(bidRequestList, this.defaultBidderRequest); expect(bidRequest.method).to.be.undefined; }); @@ -297,7 +356,7 @@ describe('marsmedia adapter tests', function () { 'mediaTypes': { 'banner': {} }, - 'adUnitCode': 'div-gpt-ad-1438287399331-0', + 'adUnitCode': 'Unit-Code', 'transactionId': 'd7b773de-ceaa-484d-89ca-d9f51b8d61ec', 'bidderRequestId': '418b37f85e772c', 'auctionId': '18fd8b8b0bd757', @@ -306,7 +365,7 @@ describe('marsmedia adapter tests', function () { } ]; - var bidRequest = r1adapter.buildRequests(bidRequestList, this.defaultBidderRequest); + var bidRequest = marsAdapter.buildRequests(bidRequestList, this.defaultBidderRequest); expect(bidRequest.method).to.be.undefined; }); @@ -320,7 +379,7 @@ describe('marsmedia adapter tests', function () { 'mediaTypes': { 'banner': {'sizes': [['400', '500'], ['4n0', '5g0']]} }, - 'adUnitCode': 'div-gpt-ad-1438287399331-0', + 'adUnitCode': 'Unit-Code', 'transactionId': 'd7b773de-ceaa-484d-89ca-d9f51b8d61ec', 'bidderRequestId': '418b37f85e772c', 'auctionId': '18fd8b8b0bd757', @@ -329,35 +388,15 @@ describe('marsmedia adapter tests', function () { } ]; - var bidRequest = r1adapter.buildRequests(bidRequestList, this.defaultBidderRequest); + var bidRequest = marsAdapter.buildRequests(bidRequestList, this.defaultBidderRequest); const openrtbRequest = JSON.parse(bidRequest.data); expect(openrtbRequest.imp[0].banner.format.length).to.equal(1); }); it('dnt is correctly set to 1', function () { - var bidRequestList = [ - { - 'bidder': 'marsmedia', - 'params': { - 'zoneId': 9999 - }, - 'mediaTypes': { - 'banner': { - 'sizes': [[300, 600]] - } - }, - 'adUnitCode': 'div-gpt-ad-1438287399331-0', - 'transactionId': 'd7b773de-ceaa-484d-89ca-d9f51b8d61ec', - 'bidderRequestId': '418b37f85e772c', - 'auctionId': '18fd8b8b0bd757', - 'bidRequestsCount': 1, - 'bidId': '51ef8751f9aead' - } - ]; - var dntStub = sinon.stub(utils, 'getDNT').returns(1); - var bidRequest = r1adapter.buildRequests(bidRequestList, this.defaultBidderRequest); + var bidRequest = marsAdapter.buildRequests(this.defaultBidRequestList, this.defaultBidderRequest); dntStub.restore(); @@ -378,7 +417,7 @@ describe('marsmedia adapter tests', function () { 'playerSize': ['600', '300'] } }, - 'adUnitCode': 'div-gpt-ad-1438287399331-1', + 'adUnitCode': 'Unit-Code', 'transactionId': 'd7b773de-ceaa-484d-89ca-d9f51b8d61ec', 'bidderRequestId': '418b37f85e772c', 'auctionId': '18fd8b8b0bd757', @@ -387,7 +426,7 @@ describe('marsmedia adapter tests', function () { } ]; - var bidRequest = r1adapter.buildRequests(bidRequestList, this.defaultBidderRequest); + var bidRequest = marsAdapter.buildRequests(bidRequestList, this.defaultBidderRequest); const openrtbRequest = JSON.parse(bidRequest.data); expect(openrtbRequest.imp[0].video.w).to.equal(600); @@ -407,7 +446,7 @@ describe('marsmedia adapter tests', function () { 'playerSize': ['badWidth', 'badHeight'] } }, - 'adUnitCode': 'div-gpt-ad-1438287399331-1', + 'adUnitCode': 'Unit-Code', 'transactionId': 'd7b773de-ceaa-484d-89ca-d9f51b8d61ec', 'bidderRequestId': '418b37f85e772c', 'auctionId': '18fd8b8b0bd757', @@ -416,7 +455,7 @@ describe('marsmedia adapter tests', function () { } ]; - var bidRequest = r1adapter.buildRequests(bidRequestList, this.defaultBidderRequest); + var bidRequest = marsAdapter.buildRequests(bidRequestList, this.defaultBidderRequest); const openrtbRequest = JSON.parse(bidRequest.data); expect(openrtbRequest.imp[0].video.w).to.be.undefined; @@ -435,7 +474,7 @@ describe('marsmedia adapter tests', function () { 'context': 'instream' } }, - 'adUnitCode': 'div-gpt-ad-1438287399331-1', + 'adUnitCode': 'Unit-Code', 'transactionId': 'd7b773de-ceaa-484d-89ca-d9f51b8d61ec', 'bidderRequestId': '418b37f85e772c', 'auctionId': '18fd8b8b0bd757', @@ -444,7 +483,7 @@ describe('marsmedia adapter tests', function () { } ]; - var bidRequest = r1adapter.buildRequests(bidRequestList, this.defaultBidderRequest); + var bidRequest = marsAdapter.buildRequests(bidRequestList, this.defaultBidderRequest); const openrtbRequest = JSON.parse(bidRequest.data); expect(openrtbRequest.imp[0].video.w).to.be.undefined; @@ -453,52 +492,74 @@ describe('marsmedia adapter tests', function () { it('should return empty site data when refererInfo is missing', function() { delete this.defaultBidderRequest.refererInfo; - var bidRequestList = [ - { - 'bidder': 'marsmedia', - 'params': { - 'zoneId': 9999 - }, - 'mediaType': 'banner', - 'adUnitCode': 'div-gpt-ad-1438287399331-0', - 'sizes': [[300, 250]], - 'transactionId': 'd7b773de-ceaa-484d-89ca-d9f51b8d61ec', - 'bidderRequestId': '418b37f85e772c', - 'auctionId': '18fd8b8b0bd757', - 'bidRequestsCount': 1, - 'bidId': '51ef8751f9aead' - } - ]; - - var bidRequest = r1adapter.buildRequests(bidRequestList, this.defaultBidderRequest); + var bidRequest = marsAdapter.buildRequests(this.defaultBidRequestList, this.defaultBidderRequest); const openrtbRequest = JSON.parse(bidRequest.data); expect(openrtbRequest.site.domain).to.equal(''); expect(openrtbRequest.site.page).to.equal(''); expect(openrtbRequest.site.ref).to.equal(''); }); + + context('when element is fully in view', function() { + it('returns 100', function() { + Object.assign(element, { width: 600, height: 400 }); + const request = marsAdapter.buildRequests(this.defaultBidRequestList, this.defaultBidderRequest); + const openrtbRequest = JSON.parse(request.data); + expect(openrtbRequest.imp[0].ext.viewability).to.equal(100); + }); + }); + + context('when element is out of view', function() { + it('returns 0', function() { + Object.assign(element, { x: -300, y: 0, width: 207, height: 320 }); + const request = marsAdapter.buildRequests(this.defaultBidRequestList, this.defaultBidderRequest); + const openrtbRequest = JSON.parse(request.data); + expect(openrtbRequest.imp[0].ext.viewability).to.equal(0); + }); + }); + + context('when element is partially in view', function() { + it('returns percentage', function() { + Object.assign(element, { width: 800, height: 800 }); + const request = marsAdapter.buildRequests(this.defaultBidRequestList, this.defaultBidderRequest); + const openrtbRequest = JSON.parse(request.data); + expect(openrtbRequest.imp[0].ext.viewability).to.equal(75); + }); + }); + + context('when nested iframes', function() { + it('returns \'na\'', function() { + Object.assign(element, { width: 600, height: 400 }); + + utils.getWindowTop.restore(); + utils.getWindowSelf.restore(); + sandbox.stub(utils, 'getWindowTop').returns(win); + sandbox.stub(utils, 'getWindowSelf').returns({}); + + const request = marsAdapter.buildRequests(this.defaultBidRequestList, this.defaultBidderRequest); + const openrtbRequest = JSON.parse(request.data); + expect(openrtbRequest.imp[0].ext.viewability).to.equal('na'); + }); + }); + + context('when tab is inactive', function() { + it('returns 0', function() { + Object.assign(element, { width: 600, height: 400 }); + + utils.getWindowTop.restore(); + win.document.visibilityState = 'hidden'; + sandbox.stub(utils, 'getWindowTop').returns(win); + + const request = marsAdapter.buildRequests(this.defaultBidRequestList, this.defaultBidderRequest); + const openrtbRequest = JSON.parse(request.data); + expect(openrtbRequest.imp[0].ext.viewability).to.equal(0); + }); + }); }); it('should return empty site.domain and site.page when refererInfo.stack is empty', function() { this.defaultBidderRequest.refererInfo.stack = []; - var bidRequestList = [ - { - 'bidder': 'marsmedia', - 'params': { - 'zoneId': 9999 - }, - 'mediaType': 'banner', - 'adUnitCode': 'div-gpt-ad-1438287399331-0', - 'sizes': [[300, 250]], - 'transactionId': 'd7b773de-ceaa-484d-89ca-d9f51b8d61ec', - 'bidderRequestId': '418b37f85e772c', - 'auctionId': '18fd8b8b0bd757', - 'bidRequestsCount': 1, - 'bidId': '51ef8751f9aead' - } - ]; - - var bidRequest = r1adapter.buildRequests(bidRequestList, this.defaultBidderRequest); + var bidRequest = marsAdapter.buildRequests(this.defaultBidRequestList, this.defaultBidderRequest); const openrtbRequest = JSON.parse(bidRequest.data); expect(openrtbRequest.site.domain).to.equal(''); @@ -508,24 +569,7 @@ describe('marsmedia adapter tests', function () { it('should secure correctly', function() { this.defaultBidderRequest.refererInfo.stack[0] = ['https://securesite.dvl']; - var bidRequestList = [ - { - 'bidder': 'marsmedia', - 'params': { - 'zoneId': 9999 - }, - 'mediaType': 'banner', - 'adUnitCode': 'div-gpt-ad-1438287399331-0', - 'sizes': [[300, 250]], - 'transactionId': 'd7b773de-ceaa-484d-89ca-d9f51b8d61ec', - 'bidderRequestId': '418b37f85e772c', - 'auctionId': '18fd8b8b0bd757', - 'bidRequestsCount': 1, - 'bidId': '51ef8751f9aead' - } - ]; - - var bidRequest = r1adapter.buildRequests(bidRequestList, this.defaultBidderRequest); + var bidRequest = marsAdapter.buildRequests(this.defaultBidRequestList, this.defaultBidderRequest); const openrtbRequest = JSON.parse(bidRequest.data); expect(openrtbRequest.imp[0].secure).to.equal(1); @@ -551,9 +595,12 @@ describe('marsmedia adapter tests', function () { 'params': { 'zoneId': 9999 }, - 'mediaType': 'banner', - 'adUnitCode': 'div-gpt-ad-1438287399331-0', - 'sizes': [[300, 250]], + 'mediaTypes': { + 'banner': { + 'sizes': [[300, 250]] + } + }, + 'adUnitCode': 'Unit-Code', 'transactionId': 'd7b773de-ceaa-484d-89ca-d9f51b8d61ec', 'bidderRequestId': '418b37f85e772c', 'auctionId': '18fd8b8b0bd757', @@ -563,7 +610,7 @@ describe('marsmedia adapter tests', function () { } ]; - var bidRequest = r1adapter.buildRequests(bidRequestList, this.defaultBidderRequest); + var bidRequest = marsAdapter.buildRequests(bidRequestList, this.defaultBidderRequest); const openrtbRequest = JSON.parse(bidRequest.data); expect(openrtbRequest.source.ext.schain).to.deep.equal(schain); @@ -571,7 +618,7 @@ describe('marsmedia adapter tests', function () { describe('misc interpretResponse', function () { it('No bid response', function() { - var noBidResponse = r1adapter.interpretResponse({ + var noBidResponse = marsAdapter.interpretResponse({ 'body': '' }); expect(noBidResponse.length).to.equal(0); @@ -589,22 +636,22 @@ describe('marsmedia adapter tests', function () { 'sizes': [[300, 250]] } }, - 'adUnitCode': 'bannerDiv' + 'adUnitCode': 'Unit-Code' }; it('should return true when required params found', function () { - expect(r1adapter.isBidRequestValid(bid)).to.equal(true); + expect(marsAdapter.isBidRequestValid(bid)).to.equal(true); }); it('should return false when placementId missing', function () { delete bid.params.zoneId; - expect(r1adapter.isBidRequestValid(bid)).to.equal(false); + expect(marsAdapter.isBidRequestValid(bid)).to.equal(false); }); }); describe('getUserSyncs', function () { it('returns an empty string', function () { - expect(r1adapter.getUserSyncs()).to.deep.equal([]); + expect(marsAdapter.getUserSyncs()).to.deep.equal([]); }); }); diff --git a/test/spec/modules/mass_spec.js b/test/spec/modules/mass_spec.js new file mode 100644 index 00000000000..a4a87ce113f --- /dev/null +++ b/test/spec/modules/mass_spec.js @@ -0,0 +1,151 @@ +import { expect } from 'chai'; +import { + init, + addBidResponseHook, + addListenerOnce, + isMassBid, + useDefaultMatch, + useDefaultRender, + updateRenderers, + listenerAdded, + isEnabled +} from 'modules/mass'; +import { logInfo } from 'src/utils.js'; + +// mock a MASS bid: +const mockedMassBids = [ + { + bidder: 'ix', + bidId: 'mass-bid-1', + requestId: 'mass-bid-1', + bidderRequestId: 'bidder-request-id-1', + dealId: 'MASS1234', + ad: 'mass://provider/product/etc...', + meta: {} + }, + { + bidder: 'ix', + bidId: 'mass-bid-2', + requestId: 'mass-bid-2', + bidderRequestId: 'bidder-request-id-1', + dealId: '1234', + ad: 'mass://provider/product/etc...', + meta: { + mass: true + } + }, +]; + +// mock non-MASS bids: +const mockedNonMassBids = [ + { + bidder: 'ix', + bidId: 'non-mass-bid-1', + requstId: 'non-mass-bid-1', + bidderRequestId: 'bidder-request-id-1', + dealId: 'MASS1234', + ad: '', + meta: { + mass: true + } + }, + { + bidder: 'ix', + bidId: 'non-mass-bid-2', + requestId: 'non-mass-bid-2', + bidderRequestId: 'bidder-request-id-1', + dealId: '1234', + ad: 'mass://provider/product/etc...', + meta: {} + }, +]; + +// mock bidder request: +const mockedBidderRequest = { + bidderCode: 'ix', + bidderRequestId: 'bidder-request-id-1' +}; + +const noop = function() {}; + +describe('MASS Module', function() { + let bidderRequest = Object.assign({}, mockedBidderRequest); + + it('should be enabled by default', function() { + expect(isEnabled).to.equal(true); + }); + + it('can be disabled', function() { + init({enabled: false}); + expect(isEnabled).to.equal(false); + }); + + it('should only affect MASS bids', function() { + init({renderUrl: 'https://...'}); + mockedNonMassBids.forEach(function(mockedBid) { + const originalBid = Object.assign({}, mockedBid); + const bid = Object.assign({}, originalBid); + + bidderRequest.bids = [bid]; + + addBidResponseHook.call({bidderRequest}, noop, 'ad-code-id', bid); + + expect(bid).to.deep.equal(originalBid); + }); + }); + + it('should only update the ad markup field', function() { + init({renderUrl: 'https://...'}); + mockedMassBids.forEach(function(mockedBid) { + const originalBid = Object.assign({}, mockedBid); + const bid = Object.assign({}, originalBid); + + bidderRequest.bids = [bid]; + + addBidResponseHook.call({bidderRequest}, noop, 'ad-code-id', bid); + + expect(bid.ad).to.not.equal(originalBid.ad); + + delete bid.ad; + delete originalBid.ad; + + expect(bid).to.deep.equal(originalBid); + }); + }); + + it('should add a message listener', function() { + addListenerOnce(); + expect(listenerAdded).to.equal(true); + }); + + it('should support custom renderers', function() { + init({ + renderUrl: 'https://...', + custom: [ + { + dealIdPattern: /abc/, + render: function() {} + } + ] + }); + + const renderers = updateRenderers(); + + expect(renderers.length).to.equal(2); + }); + + it('should match bids by deal ID with the default matcher', function() { + const match = useDefaultMatch(/abc/); + + expect(match({dealId: 'abc'})).to.equal(true); + expect(match({dealId: 'xyz'})).to.equal(false); + }); + + it('should have a default renderer', function() { + const render = useDefaultRender('https://example.com/render.js', 'abc'); + render({}); + + expect(window.abc.loaded).to.equal(true); + expect(window.abc.queue.length).to.equal(1); + }); +}); diff --git a/test/spec/modules/mediaforceBidAdapter_spec.js b/test/spec/modules/mediaforceBidAdapter_spec.js index ee478acbc83..24326afe5c0 100644 --- a/test/spec/modules/mediaforceBidAdapter_spec.js +++ b/test/spec/modules/mediaforceBidAdapter_spec.js @@ -100,6 +100,37 @@ describe('mediaforce bid adapter', function () { transactionId: 'd45dd707-a418-42ec-b8a7-b70a6c6fab0b', }; + const multiBid = [ + { + publisher_id: 'pub123', + placement_id: '202', + }, + { + publisher_id: 'pub123', + placement_id: '203', + }, + { + publisher_id: 'pub124', + placement_id: '202', + }, + { + publisher_id: 'pub123', + placement_id: '203', + transactionId: '8df76688-1618-417a-87b1-60ad046841c9' + } + ].map(({publisher_id, placement_id, transactionId}) => { + return { + bidder: 'mediaforce', + params: {publisher_id, placement_id}, + mediaTypes: { + banner: { + sizes: [[300, 250], [600, 400]] + } + }, + transactionId: transactionId || 'd45dd707-a418-42ec-b8a7-b70a6c6fab0b' + } + }); + const refererInfo = { referer: 'https://www.prebid.org', reachedTop: true, @@ -111,7 +142,9 @@ describe('mediaforce bid adapter', function () { const requestUrl = `${baseUrl}/header_bid`; const dnt = utils.getDNT() ? 1 : 0; - const secure = 1; + const secure = window.location.protocol === 'https:' ? 1 : 0; + const pageUrl = window.location.href; + const timeout = 1500; it('should return undefined if no validBidRequests passed', function () { assert.equal(spec.buildRequests([]), undefined); @@ -127,18 +160,29 @@ describe('mediaforce bid adapter', function () { bid.params.bidfloor = 0.5; let bidRequests = [bid]; - let bidderRequest = {bids: bidRequests, refererInfo: refererInfo}; + let bidderRequest = { + bids: bidRequests, + refererInfo: refererInfo, + timeout: timeout, + auctionId: '210a474e-88f0-4646-837f-4253b7cf14fb' + }; let [request] = spec.buildRequests(bidRequests, bidderRequest); let data = JSON.parse(request.data); assert.deepEqual(data, { - id: bid.transactionId, + id: data.id, + tmax: timeout, + ext: { + mediaforce: { + hb_key: bidderRequest.auctionId + } + }, site: { id: bid.params.publisher_id, publisher: {id: bid.params.publisher_id}, ref: encodeURIComponent(refererInfo.referer), - page: encodeURIComponent(refererInfo.referer), + page: pageUrl, }, device: { ua: navigator.userAgent, @@ -150,6 +194,11 @@ describe('mediaforce bid adapter', function () { tagid: bid.params.placement_id, secure: secure, bidfloor: bid.params.bidfloor, + ext: { + mediaforce: { + transactionId: bid.transactionId + } + }, banner: {w: 300, h: 250}, native: { ver: '1.2', @@ -170,7 +219,7 @@ describe('mediaforce bid adapter', function () { assert.deepEqual(request, { method: 'POST', url: requestUrl, - data: '{"id":"d45dd707-a418-42ec-b8a7-b70a6c6fab0b","site":{"page":"https%3A%2F%2Fwww.prebid.org","ref":"https%3A%2F%2Fwww.prebid.org","id":"pub123","publisher":{"id":"pub123"}},"device":{"ua":"' + navigator.userAgent + '","js":1,"dnt":' + dnt + ',"language":"' + language + '"},"imp":[{"tagid":"202","secure":1,"bidfloor":0.5,"banner":{"w":300,"h":250},"native":{"ver":"1.2","request":{"assets":[{"required":1,"id":1,"title":{"len":800}},{"required":1,"id":3,"img":{"type":3,"w":300,"h":250}},{"required":1,"id":5,"data":{"type":1}}],"context":1,"plcmttype":1,"ver":"1.2"}}}]}', + data: '{"id":"' + data.id + '","site":{"page":"' + pageUrl + '","ref":"https%3A%2F%2Fwww.prebid.org","id":"pub123","publisher":{"id":"pub123"}},"device":{"ua":"' + navigator.userAgent + '","js":1,"dnt":' + dnt + ',"language":"' + language + '"},"ext":{"mediaforce":{"hb_key":"210a474e-88f0-4646-837f-4253b7cf14fb"}},"tmax":1500,"imp":[{"tagid":"202","secure":' + secure + ',"bidfloor":0.5,"ext":{"mediaforce":{"transactionId":"d45dd707-a418-42ec-b8a7-b70a6c6fab0b"}},"banner":{"w":300,"h":250},"native":{"ver":"1.2","request":{"assets":[{"required":1,"id":1,"title":{"len":800}},{"required":1,"id":3,"img":{"type":3,"w":300,"h":250}},{"required":1,"id":5,"data":{"type":1}}],"context":1,"plcmttype":1,"ver":"1.2"}}}]}', }); }); @@ -186,6 +235,116 @@ describe('mediaforce bid adapter', function () { let data = JSON.parse(request.data); assert.deepEqual(data.imp[0].banner, {w: 300, h: 600, format: [{w: 300, h: 250}]}); }); + + it('should return proper requests for multiple imps', function () { + let bidderRequest = { + bids: multiBid, + refererInfo: refererInfo, + timeout: timeout, + auctionId: '210a474e-88f0-4646-837f-4253b7cf14fb' + }; + + let requests = spec.buildRequests(multiBid, bidderRequest); + assert.equal(requests.length, 2); + requests.forEach((req) => { + req.data = JSON.parse(req.data); + }); + + assert.deepEqual(requests, [ + { + method: 'POST', + url: requestUrl, + data: { + id: requests[0].data.id, + tmax: timeout, + ext: { + mediaforce: { + hb_key: bidderRequest.auctionId + } + }, + site: { + id: 'pub123', + publisher: {id: 'pub123'}, + ref: encodeURIComponent(refererInfo.referer), + page: pageUrl, + }, + device: { + ua: navigator.userAgent, + dnt: dnt, + js: 1, + language: language, + }, + imp: [{ + tagid: '202', + secure: secure, + bidfloor: 0, + ext: { + mediaforce: { + transactionId: 'd45dd707-a418-42ec-b8a7-b70a6c6fab0b' + } + }, + banner: {w: 300, h: 250, format: [{w: 600, h: 400}]}, + }, { + tagid: '203', + secure: secure, + bidfloor: 0, + ext: { + mediaforce: { + transactionId: 'd45dd707-a418-42ec-b8a7-b70a6c6fab0b' + } + }, + banner: {w: 300, h: 250, format: [{w: 600, h: 400}]}, + }, { + tagid: '203', + secure: secure, + bidfloor: 0, + ext: { + mediaforce: { + transactionId: '8df76688-1618-417a-87b1-60ad046841c9' + } + }, + banner: {w: 300, h: 250, format: [{w: 600, h: 400}]}, + }] + } + }, + { + method: 'POST', + url: requestUrl, + data: { + id: requests[1].data.id, + tmax: timeout, + ext: { + mediaforce: { + hb_key: bidderRequest.auctionId + } + }, + site: { + id: 'pub124', + publisher: {id: 'pub124'}, + ref: encodeURIComponent(refererInfo.referer), + page: pageUrl, + }, + device: { + ua: navigator.userAgent, + dnt: dnt, + js: 1, + language: language, + }, + imp: [{ + tagid: '202', + secure: secure, + bidfloor: 0, + ext: { + mediaforce: { + transactionId: 'd45dd707-a418-42ec-b8a7-b70a6c6fab0b' + } + }, + banner: {w: 300, h: 250, format: [{w: 600, h: 400}]}, + }] + } + } + ]); + }); }); describe('interpretResponse() banner', function () { diff --git a/test/spec/modules/mediagoBidAdapter_spec.js b/test/spec/modules/mediagoBidAdapter_spec.js new file mode 100644 index 00000000000..25cfc72672b --- /dev/null +++ b/test/spec/modules/mediagoBidAdapter_spec.js @@ -0,0 +1,101 @@ +import { + expect +} from 'chai'; +import { + spec +} from 'modules/mediagoBidAdapter.js'; + +describe('mediago:BidAdapterTests', function() { + let bidRequestData = { + 'bidderCode': 'mediago', + 'auctionId': '7fae02a9-0195-472f-ba94-708d3bc2c0d9', + 'bidderRequestId': '4fec04e87ad785', + 'bids': [{ + 'bidder': 'mediago', + 'params': { + 'token': '85a6b01e41ac36d49744fad726e3655d' + }, + 'mediaTypes': { + 'banner': { + 'sizes': [ + [ + 300, + 250 + ] + ] + } + }, + 'adUnitCode': 'div-gpt-ad-1460505748561-0', + 'transactionId': '5e24a2ce-db03-4565-a8a3-75dbddca9377', + 'sizes': [ + [ + 300, + 250 + ] + ], + 'bidId': '54d73f19c9d47a', + 'bidderRequestId': '4fec04e87ad785', + 'auctionId': '7fae02a9-0195-472f-ba94-708d3bc2c0d9', + 'src': 'client', + 'bidRequestsCount': 1, + 'bidderRequestsCount': 1, + 'bidderWinsCount': 0 + }] + }; + let request = []; + + it('mediago:validate_pub_params', function() { + expect( + spec.isBidRequestValid({ + bidder: 'mediago', + params: { + token: ['85a6b01e41ac36d49744fad726e3655d'] + } + }) + ).to.equal(true); + }); + + it('mediago:validate_generated_params', function() { + request = spec.buildRequests(bidRequestData.bids, bidRequestData); + // console.log(request); + let req_data = JSON.parse(request.data); + expect(req_data.imp).to.have.lengthOf(1); + }); + + it('mediago:validate_response_params', function() { + let serverResponse = { + body: { + 'id': '7244645c-a81a-4760-8fd6-9d908d2c4a44', + 'seatbid': [{ + 'bid': [{ + 'id': 'aa86796a857ebedda9a2d7128a87dab1', + 'impid': '1', + 'price': 0.05, + 'nurl': 'http://d21uzv52i0cqie.cloudfront.net/api/winnotice?tn=341443089c0cb829164455a42d216ee3\u0026winloss=1\u0026id=aa86796a857ebedda9a2d7128a87dab1\u0026seat_id=${AUCTION_SEAT_ID}\u0026currency=${AUCTION_CURRENCY}\u0026bid_id=${AUCTION_BID_ID}\u0026ad_id=${AUCTION_AD_ID}\u0026loss=${AUCTION_LOSS}\u0026imp_id=1\u0026price=${AUCTION_PRICE}\u0026test=0\u0026time=1597714943\u0026adp=Dtnz0O4U8sdAU-XGGijCAgMbjDIeMGyCLXeSg1laXxM\u0026dsp_id=22\u0026url=-BFDu2NYtc4PYTplFW_2JcnDSRVLOOfaERbwJABjFyG6NUB4ywA3dUaXt5zPlyCUpBCOxjH9gk4E6yWTshzuSfQSx7g_TxvcXYUgh7YtY9NQZxx14InmNCTsezqID5UztV7llz8SXWHQ-ZsutH1nJIZzl1jH3i2uCPi91shqIZLN1bLJ5guAr5O4WyxVeOqIKyD_GiVcY9Olm51iI_3wgwFyDEN_dIDv-ObgNxpbPD0L11-62bjhGw3__7RuEo6XLdox-g46Fcqk6i0zayfsPM4QeMAhWJ4lsg-xswSI0YAfzyoOIeTWB78mdpt_GmN5PKZZPqyO7VkbwHEasn-mTyYTddbz5v2fzEkRO0AQZtAZx96PANGrNvcOHnRVmCdkzN96b5Ur1_8ipdyzHOFRtJ-z_KmKaxig6himvMCePozZvrvihiGhigP4RGiFT7ytVYKHyUGAV2PF5SwtgnB0uGCltd7o1CLhZyZEQNgE7LSESyGztZ5kM9N_VZV9gPZVhvlJDfYFNRW9i6D2pZxV0Gd63rA9gpeUJ3mhbkj-B27VRKrNTBSrwIAU7P0RPD5_opl3G8nPD1Ce2vKuQK8qynHWQblfeA61nDok-fRezSKbzwepqi8oxXadFrCmN7KxP_mPqA794xYzIw5-mS64NA', + 'burl': 'http://d21uzv52i0cqie.cloudfront.net/api/winnotice?tn=341443089c0cb829164455a42d216ee3\u0026winloss=2\u0026id=aa86796a857ebedda9a2d7128a87dab1\u0026seat_id=${AUCTION_SEAT_ID}\u0026currency=${AUCTION_CURRENCY}\u0026bid_id=${AUCTION_BID_ID}\u0026ad_id=${AUCTION_AD_ID}\u0026loss=${AUCTION_LOSS}\u0026imp_id=1\u0026price=${AUCTION_PRICE}\u0026test=0\u0026time=1597714943\u0026adp=Dtnz0O4U8sdAU-XGGijCAgMbjDIeMGyCLXeSg1laXxM\u0026dsp_id=22\u0026url=dXerAvyp4zYQzsQ56eGB4JtiA4yFaYlTqcHffccrvCg', + 'lurl': 'http://d21uzv52i0cqie.cloudfront.net/api/winnotice?tn=341443089c0cb829164455a42d216ee3\u0026winloss=0\u0026id=aa86796a857ebedda9a2d7128a87dab1\u0026seat_id=${AUCTION_SEAT_ID}\u0026currency=${AUCTION_CURRENCY}\u0026bid_id=${AUCTION_BID_ID}\u0026ad_id=${AUCTION_AD_ID}\u0026loss=${AUCTION_LOSS}\u0026imp_id=1\u0026price=${AUCTION_PRICE}\u0026test=0\u0026time=1597714943\u0026adp=Dtnz0O4U8sdAU-XGGijCAgMbjDIeMGyCLXeSg1laXxM\u0026dsp_id=22\u0026url=ptSxg_vR7-fdx-WAkkUADJb__BntE5a6-RSeYdUewvk', + 'adm': '\u003clink rel=\"stylesheet\" href=\"//cdn.mediago.io/js/style/style_banner_300*250.css\"\u003e\u003cdiv id=\"mgcontainer-583ce3286b442001205b2fb9a5488efc\" class=\"mediago-placement imgTopTitleBottom\" style=\"position:relative;width:298px;height:248px;overflow:hidden\"\u003e\u003ca href=\"http://trace.mediago.io/api/bidder/track?tn=341443089c0cb829164455a42d216ee3\u0026price=PRMC8pCHtH55ipgXubUs8jlsKTBxWRSEweH8Mee_aUQ\u0026evt=102\u0026rid=aa86796a857ebedda9a2d7128a87dab1\u0026campaignid=1003540\u0026impid=25-300x175-1\u0026offerid=1107782\u0026test=0\u0026time=1597714943\u0026cp=GJA03gjm-ugPTmN7prOpvhfu1aA04IgpTcW4oRX22Lg\u0026clickid=25_aa86796a857ebedda9a2d7128a87dab1_25-300x175-1\u0026acid=164\u0026trackingid=583ce3286b442001205b2fb9a5488efc\u0026uid=6dda6c6b70eb4e2d9ab3469d921f2c74\u0026jt=2\u0026url=PQFFci337KgCVkx7KTgRItClLaWH0lgTcIlgBRTpfKngVJES4uKLfxXz9mjLcDWIbWQcEVVk_gfTcIaK8oKO2YyVG5lc3hjZeZr0VaIDHbWggPJaqtfDK9T0HZIKvrpe\" target=\"_blank\"\u003e\u003cimg alt=\"Robert Vowed To Keep Silent, But Decided To Put The People First And Speak\" src=\"https://d2cli4kgl5uxre.cloudfront.net/ML/b5e361889beef5eaf69987384b7a56e8__300x175.png\" style=\"height:70%;width:100%;border-width:0;border:none;\"\u003e\u003ch3 class=\"title\" style=\"font-size:16px;\"\u003eRobert Vowed To Keep Silent, But Decided To Put The People First And Speak\u003c/h3\u003e\u003c/a\u003e\u003cspan class=\"source\"\u003e\u003ca class=\"sourcename\" href=\"//www.mediago.io\" target=\"_blank\"\u003e\u003cspan\u003eAd\u003c/span\u003e \u003c/a\u003e\u003ca class=\"srcnameadslabelurl\" href=\"//www.mediago.io/privacy\" target=\"_blank\"\u003e\u003cspan\u003eViral Net News\u003c/span\u003e\u003c/a\u003e\u003c/span\u003e\u003c/div\u003e\u003cscript\u003e!function(e){var t={};function n(o){if(t[o])return t[o].exports;var r=t[o]={i:o,l:!1,exports:{}};return e[o].call(r.exports,r,r.exports,n),r.l=!0,r.exports}n.m=e,n.c=t,n.d=function(e,t,o){n.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:o})},n.r=function(e){\"undefined\"!=typeof Symbol\u0026\u0026Symbol.toStringTag\u0026\u0026Object.defineProperty(e,Symbol.toStringTag,{value:\"Module\"}),Object.defineProperty(e,\"__esModule\",{value:!0})},n.t=function(e,t){if(1\u0026t\u0026\u0026(e=n(e)),8\u0026t)return e;if(4\u0026t\u0026\u0026\"object\"==typeof e\u0026\u0026e\u0026\u0026e.__esModule)return e;var o=Object.create(null);if(n.r(o),Object.defineProperty(o,\"default\",{enumerable:!0,value:e}),2\u0026t\u0026\u0026\"string\"!=typeof e)for(var r in e)n.d(o,r,function(t){return e[t]}.bind(null,r));return o},n.n=function(e){var t=e\u0026\u0026e.__esModule?function(){return e.default}:function(){return e};return n.d(t,\"a\",t),t},n.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},n.p=\"\",n(n.s=24)}({24:function(e,t,n){\"use strict\";function o(e){var t=new Image;t.src=e,t.style=\"display:none;visibility:hidden\",t.width=0,t.height=0,document.body.appendChild(t)}o(\"http://d21uzv52i0cqie.cloudfront.net/api/bidder/track?tn=341443089c0cb829164455a42d216ee3\u0026price=PRMC8pCHtH55ipgXubUs8jlsKTBxWRSEweH8Mee_aUQ\u0026evt=101\u0026rid=aa86796a857ebedda9a2d7128a87dab1\u0026campaignid=1003540\u0026impid=25-300x175-1\u0026offerid=1107782\u0026test=0\u0026time=1597714943\u0026cp=GJA03gjm-ugPTmN7prOpvhfu1aA04IgpTcW4oRX22Lg\u0026acid=164\u0026trackingid=583ce3286b442001205b2fb9a5488efc\u0026uid=6dda6c6b70eb4e2d9ab3469d921f2c74\");var r=document.getElementById(\"mgcontainer-583ce3286b442001205b2fb9a5488efc\"),i=!1;!function e(){setTimeout((function(){var t,n;!i\u0026\u0026(t=r,n=window.innerHeight||document.documentElement.clientHeight||document.body.clientHeight,(t.getBoundingClientRect()\u0026\u0026t.getBoundingClientRect().top)\u003c=n-.75*(t.offsetHeight||t.clientHeight))?(i=!0,o(\"http://d21uzv52i0cqie.cloudfront.net/api/bidder/track?tn=341443089c0cb829164455a42d216ee3\u0026price=PRMC8pCHtH55ipgXubUs8jlsKTBxWRSEweH8Mee_aUQ\u0026evt=104\u0026rid=aa86796a857ebedda9a2d7128a87dab1\u0026campaignid=1003540\u0026impid=25-300x175-1\u0026offerid=1107782\u0026test=0\u0026time=1597714943\u0026cp=GJA03gjm-ugPTmN7prOpvhfu1aA04IgpTcW4oRX22Lg\u0026acid=164\u0026trackingid=583ce3286b442001205b2fb9a5488efc\u0026uid=6dda6c6b70eb4e2d9ab3469d921f2c74\u0026sid=16__11__13\u0026format=\u0026crid=b5e361889beef5eaf69987384b7a56e8\")):e()}),500)}()}});\u003c/script\u003e\u003cscript type=\"text/javascript\" src=\"http://d21uzv52i0cqie.cloudfront.net/api/track?tn=341443089c0cb829164455a42d216ee3\u0026price=${AUCTION_PRICE}\u0026evt=5\u0026rid=aa86796a857ebedda9a2d7128a87dab1\u0026impid=1\u0026offerid=\u0026tagid=\u0026test=0\u0026time=1597714943\u0026adp=5zrCZ2rC-WLafYkNpeTnzA72tDqVZUlOA3Js6_eJjYU\u0026dsp_id=22\u0026cp=${cp}\u0026url=\u0026type=script\"\u003e\u003c/script\u003e\u003cscript\u003edocument.addEventListener\u0026\u0026document.addEventListener(\"click\",function(){var a=document.createElement(\"script\");a.src=\"http://d21uzv52i0cqie.cloudfront.net/api/track?tn=341443089c0cb829164455a42d216ee3\u0026price=${AUCTION_PRICE}\u0026evt=6\u0026rid=aa86796a857ebedda9a2d7128a87dab1\u0026impid=1\u0026offerid=\u0026tagid=\u0026test=0\u0026time=1597714943\u0026adp=5zrCZ2rC-WLafYkNpeTnzA72tDqVZUlOA3Js6_eJjYU\u0026dsp_id=22\u0026cp=${cp}\u0026url=\u0026clickid=25_aa86796a857ebedda9a2d7128a87dab1_1\";document.body.appendChild(a)});\u003c/script\u003e', + 'cid': '1003540', + 'crid': 'b5e361889beef5eaf69987384b7a56e8', + 'w': 300, + 'h': 250 + }] + }], + 'cur': 'USD' + } + }; + + let bids = spec.interpretResponse(serverResponse); + // console.log({ + // bids + // }); + expect(bids).to.have.lengthOf(1); + + let bid = bids[0]; + + expect(bid.creativeId).to.equal('b5e361889beef5eaf69987384b7a56e8'); + expect(bid.width).to.equal(300); + expect(bid.height).to.equal(250); + expect(bid.currency).to.equal('USD'); + }); +}); diff --git a/test/spec/modules/medianetAnalyticsAdapter_spec.js b/test/spec/modules/medianetAnalyticsAdapter_spec.js index 97b45cef00c..41a6338225e 100644 --- a/test/spec/modules/medianetAnalyticsAdapter_spec.js +++ b/test/spec/modules/medianetAnalyticsAdapter_spec.js @@ -9,20 +9,23 @@ const { } = CONSTANTS; const MOCK = { - AUCTION_INIT: {'auctionId': '8e0d5245-deb3-406c-96ca-9b609e077ff7', 'timestamp': 1584563605739}, - AUCTION_INIT_WITH_FLOOR: {'auctionId': '8e0d5245-deb3-406c-96ca-9b609e077ff7', 'timestamp': 1584563605739, 'bidderRequests': [{'bids': [{ 'floorData': {'enforcements': {'enforceJS': true}} }]}]}, + Ad_Units: [{'code': 'div-gpt-ad-1460505748561-0', 'mediaTypes': {'banner': {'sizes': [[300, 250]]}}, 'bids': [], 'ext': {'prop1': 'value1'}}], + MULTI_FORMAT_TWIN_AD_UNITS: [{'code': 'div-gpt-ad-1460505748561-0', 'mediaTypes': {'banner': {'sizes': [[300, 250]]}, 'native': {'image': {'required': true, 'sizes': [150, 50]}}}, 'bids': [], 'ext': {'prop1': 'value1'}}, {'code': 'div-gpt-ad-1460505748561-0', 'mediaTypes': {'video': {'playerSize': [640, 480], 'context': 'instream'}}, 'bids': [], 'ext': {'prop1': 'value1'}}], + AUCTION_INIT: {'auctionId': '8e0d5245-deb3-406c-96ca-9b609e077ff7', 'timestamp': 1584563605739, 'timeout': 6000}, + AUCTION_INIT_WITH_FLOOR: {'auctionId': '8e0d5245-deb3-406c-96ca-9b609e077ff7', 'timestamp': 1584563605739, 'timeout': 6000, 'bidderRequests': [{'bids': [{ 'floorData': {'enforcements': {'enforceJS': true}} }]}]}, BID_REQUESTED: {'bidderCode': 'medianet', 'auctionId': '8e0d5245-deb3-406c-96ca-9b609e077ff7', 'bids': [{'bidder': 'medianet', 'params': {'cid': 'TEST_CID', 'crid': '451466393'}, 'mediaTypes': {'banner': {'sizes': [[300, 250]], 'ext': ['asdads']}}, 'adUnitCode': 'div-gpt-ad-1460505748561-0', 'sizes': [[300, 250]], 'bidId': '28248b0e6aece2', 'auctionId': '8e0d5245-deb3-406c-96ca-9b609e077ff7', 'src': 'client'}], 'auctionStart': 1584563605739, 'timeout': 6000, 'uspConsent': '1YY', 'start': 1584563605743}, - MULTI_FORMAT_BID_REQUESTED: {'bidderCode': 'medianet', 'auctionId': '8e0d5245-deb3-406c-96ca-9b609e077ff7', 'bids': [{'bidder': 'medianet', 'params': {'cid': 'TEST_CID', 'crid': '451466393'}, 'mediaTypes': {'banner': {'sizes': [[300, 250]]}, 'video': {'playerSize': [640, 480], 'context': 'instream'}, 'native': {'image': {'required': true, 'sizes': [150, 50]}, 'title': {'required': true, 'len': 80}}, 'ext': ['asdads']}, 'adUnitCode': 'div-gpt-ad-1460505748561-0', 'sizes': [[300, 250]], 'bidId': '28248b0e6aece2', 'auctionId': '8e0d5245-deb3-406c-96ca-9b609e077ff7', 'src': 'client'}], 'auctionStart': 1584563605739, 'timeout': 6000, 'uspConsent': '1YY', 'start': 1584563605743}, + MULTI_FORMAT_BID_REQUESTED: {'bidderCode': 'medianet', 'auctionId': '8e0d5245-deb3-406c-96ca-9b609e077ff7', 'bids': [{'bidder': 'medianet', 'params': {'cid': 'TEST_CID', 'crid': '451466393'}, 'mediaTypes': {'banner': {'sizes': [[300, 250]]}, 'video': {'playerSize': [640, 480], 'context': 'instream'}, 'native': {'image': {'required': true, 'sizes': [150, 50]}, 'title': {'required': true, 'len': 80}}}, 'adUnitCode': 'div-gpt-ad-1460505748561-0', 'sizes': [[300, 250]], 'bidId': '28248b0e6aece2', 'auctionId': '8e0d5245-deb3-406c-96ca-9b609e077ff7', 'src': 'client'}], 'auctionStart': 1584563605739, 'timeout': 6000, 'uspConsent': '1YY', 'start': 1584563605743}, BID_RESPONSE: {'bidderCode': 'medianet', 'width': 300, 'height': 250, 'adId': '3e6e4bce5c8fb3', 'requestId': '28248b0e6aece2', 'mediaType': 'banner', 'source': 'client', 'ext': {'pvid': 123, 'crid': '321'}, 'no_bid': false, 'cpm': 2.299, 'ad': 'AD_CODE', 'ttl': 180, 'creativeId': 'Test1', 'netRevenue': true, 'currency': 'USD', 'dfp_id': 'div-gpt-ad-1460505748561-0', 'originalCpm': 1.1495, 'originalCurrency': 'USD', 'floorData': {'floorValue': 1.10, 'floorRule': 'banner'}, 'auctionId': '8e0d5245-deb3-406c-96ca-9b609e077ff7', 'responseTimestamp': 1584563606009, 'requestTimestamp': 1584563605743, 'bidder': 'medianet', 'adUnitCode': 'div-gpt-ad-1460505748561-0', 'timeToRespond': 266, 'pbLg': '2.00', 'pbMg': '2.20', 'pbHg': '2.29', 'pbAg': '2.25', 'pbDg': '2.29', 'pbCg': '2.00', 'size': '300x250', 'adserverTargeting': {'hb_bidder': 'medianet', 'hb_adid': '3e6e4bce5c8fb3', 'hb_pb': '2.00', 'hb_size': '300x250', 'hb_source': 'client', 'hb_format': 'banner', 'prebid_test': 1}, 'status': 'rendered', 'params': [{'cid': 'test123', 'crid': '451466393'}]}, AUCTION_END: {'auctionId': '8e0d5245-deb3-406c-96ca-9b609e077ff7', 'auctionEnd': 1584563605739}, SET_TARGETING: {'div-gpt-ad-1460505748561-0': {'prebid_test': '1', 'hb_format': 'banner', 'hb_source': 'client', 'hb_size': '300x250', 'hb_pb': '2.00', 'hb_adid': '3e6e4bce5c8fb3', 'hb_bidder': 'medianet', 'hb_format_medianet': 'banner', 'hb_source_medianet': 'client', 'hb_size_medianet': '300x250', 'hb_pb_medianet': '2.00', 'hb_adid_medianet': '3e6e4bce5c8fb3', 'hb_bidder_medianet': 'medianet'}}, + NO_BID_SET_TARGETING: {'div-gpt-ad-1460505748561-0': {}}, BID_WON: {'bidderCode': 'medianet', 'width': 300, 'height': 250, 'statusMessage': 'Bid available', 'adId': '3e6e4bce5c8fb3', 'requestId': '28248b0e6aece2', 'mediaType': 'banner', 'source': 'client', 'no_bid': false, 'cpm': 2.299, 'ad': 'AD_CODE', 'ttl': 180, 'creativeId': 'Test1', 'netRevenue': true, 'currency': 'USD', 'dfp_id': 'div-gpt-ad-1460505748561-0', 'originalCpm': 1.1495, 'originalCurrency': 'USD', 'auctionId': '8e0d5245-deb3-406c-96ca-9b609e077ff7', 'responseTimestamp': 1584563606009, 'requestTimestamp': 1584563605743, 'bidder': 'medianet', 'adUnitCode': 'div-gpt-ad-1460505748561-0', 'timeToRespond': 266, 'pbLg': '2.00', 'pbMg': '2.20', 'pbHg': '2.29', 'pbAg': '2.25', 'pbDg': '2.29', 'pbCg': '2.00', 'size': '300x250', 'adserverTargeting': {'hb_bidder': 'medianet', 'hb_adid': '3e6e4bce5c8fb3', 'hb_pb': '2.00', 'hb_size': '300x250', 'hb_source': 'client', 'hb_format': 'banner', 'prebid_test': 1}, 'status': 'rendered', 'params': [{'cid': 'test123', 'crid': '451466393'}]}, NO_BID: {'bidder': 'medianet', 'params': {'cid': 'test123', 'crid': '451466393', 'site': {}}, 'mediaTypes': {'banner': {'sizes': [[300, 250]], 'ext': ['asdads']}}, 'adUnitCode': 'div-gpt-ad-1460505748561-0', 'transactionId': '303fa0c6-682f-4aea-8e4a-dc68f0d5c7d5', 'sizes': [[300, 250], [300, 600]], 'bidId': '28248b0e6aece2', 'bidderRequestId': '13fccf3809fe43', 'auctionId': '8e0d5245-deb3-406c-96ca-9b609e077ff7', 'src': 'client'}, BID_TIMEOUT: [{'bidId': '28248b0e6aece2', 'bidder': 'medianet', 'adUnitCode': 'div-gpt-ad-1460505748561-0', 'auctionId': '8e0d5245-deb3-406c-96ca-9b609e077ff7', 'params': [{'cid': 'test123', 'crid': '451466393', 'site': {}}, {'cid': '8CUX0H51P', 'crid': '451466393', 'site': {}}], 'timeout': 6}] } function performAuctionWithFloorConfig() { - events.emit(AUCTION_INIT, MOCK.AUCTION_INIT_WITH_FLOOR); + events.emit(AUCTION_INIT, {...MOCK.AUCTION_INIT_WITH_FLOOR, adUnits: MOCK.Ad_Units}); events.emit(BID_REQUESTED, MOCK.BID_REQUESTED); events.emit(BID_RESPONSE, MOCK.BID_RESPONSE); events.emit(AUCTION_END, MOCK.AUCTION_END); @@ -31,7 +34,7 @@ function performAuctionWithFloorConfig() { } function performStandardAuctionWithWinner() { - events.emit(AUCTION_INIT, MOCK.AUCTION_INIT); + events.emit(AUCTION_INIT, {...MOCK.AUCTION_INIT, adUnits: MOCK.Ad_Units}); events.emit(BID_REQUESTED, MOCK.BID_REQUESTED); events.emit(BID_RESPONSE, MOCK.BID_RESPONSE); events.emit(AUCTION_END, MOCK.AUCTION_END); @@ -40,27 +43,27 @@ function performStandardAuctionWithWinner() { } function performMultiFormatAuctionWithNoBid() { - events.emit(AUCTION_INIT, MOCK.AUCTION_INIT); + events.emit(AUCTION_INIT, {...MOCK.AUCTION_INIT, adUnits: MOCK.MULTI_FORMAT_TWIN_AD_UNITS}); events.emit(BID_REQUESTED, MOCK.MULTI_FORMAT_BID_REQUESTED); - events.emit(BID_RESPONSE, MOCK.BID_RESPONSE); + events.emit(NO_BID, MOCK.NO_BID); events.emit(AUCTION_END, MOCK.AUCTION_END); - events.emit(SET_TARGETING, MOCK.SET_TARGETING); + events.emit(SET_TARGETING, MOCK.NO_BID_SET_TARGETING); } function performStandardAuctionWithNoBid() { - events.emit(AUCTION_INIT, MOCK.AUCTION_INIT); + events.emit(AUCTION_INIT, {...MOCK.AUCTION_INIT, adUnits: MOCK.Ad_Units}); events.emit(BID_REQUESTED, MOCK.BID_REQUESTED); events.emit(NO_BID, MOCK.NO_BID); events.emit(AUCTION_END, MOCK.AUCTION_END); - events.emit(SET_TARGETING, MOCK.SET_TARGETING); + events.emit(SET_TARGETING, MOCK.NO_BID_SET_TARGETING); } function performStandardAuctionWithTimeout() { - events.emit(AUCTION_INIT, MOCK.AUCTION_INIT); + events.emit(AUCTION_INIT, {...MOCK.AUCTION_INIT, adUnits: MOCK.Ad_Units}); events.emit(BID_REQUESTED, MOCK.BID_REQUESTED); events.emit(BID_TIMEOUT, MOCK.BID_TIMEOUT); events.emit(AUCTION_END, MOCK.AUCTION_END); - events.emit(SET_TARGETING, MOCK.SET_TARGETING); + events.emit(SET_TARGETING, MOCK.NO_BID_SET_TARGETING); } function getQueryData(url) { @@ -123,8 +126,10 @@ describe('Media.net Analytics Adapter', function() { cid: 'test123' } }); + medianetAnalytics.clearlogsQueue(); }); afterEach(function () { + medianetAnalytics.clearlogsQueue(); medianetAnalytics.disableAnalytics(); }); @@ -139,10 +144,9 @@ describe('Media.net Analytics Adapter', function() { performMultiFormatAuctionWithNoBid(); const noBidLog = medianetAnalytics.getlogsQueue().map((log) => getQueryData(log))[0]; medianetAnalytics.clearlogsQueue(); - - expect(noBidLog.szs).to.equal(encodeURIComponent('300x250|1x1|640x480')); + expect(noBidLog.mtype).to.have.ordered.members([encodeURIComponent('banner|native|video'), encodeURIComponent('banner|video|native')]); + expect(noBidLog.szs).to.have.ordered.members([encodeURIComponent('300x250|1x1|640x480'), encodeURIComponent('300x250|1x1|640x480')]); expect(noBidLog.vplcmtt).to.equal('instream'); - expect(noBidLog.sz2).to.equal(encodeURIComponent('300x250')); }); it('should have winner log in standard auction', function() { @@ -167,6 +171,7 @@ describe('Media.net Analytics Adapter', function() { src: 'client', size: '300x250', mtype: 'banner', + gdpr: '0', cid: 'test123', lper: '1', ogbdp: '1.1495', @@ -205,7 +210,7 @@ describe('Media.net Analytics Adapter', function() { expect(noBidLog.status).to.have.ordered.members(['1', '2']); expect(noBidLog.src).to.have.ordered.members(['client', 'client']); expect(noBidLog.curr).to.have.ordered.members(['', '']); - expect(noBidLog.mtype).to.have.ordered.members(['', '']); + expect(noBidLog.mtype).to.have.ordered.members(['banner', 'banner']); expect(noBidLog.ogbdp).to.have.ordered.members(['', '']); expect(noBidLog.mpvid).to.have.ordered.members(['', '']); expect(noBidLog.crid).to.have.ordered.members(['', '451466393']); @@ -223,7 +228,7 @@ describe('Media.net Analytics Adapter', function() { expect(timeoutLog.status).to.have.ordered.members(['1', '3']); expect(timeoutLog.src).to.have.ordered.members(['client', 'client']); expect(timeoutLog.curr).to.have.ordered.members(['', '']); - expect(timeoutLog.mtype).to.have.ordered.members(['', '']); + expect(timeoutLog.mtype).to.have.ordered.members(['banner', 'banner']); expect(timeoutLog.ogbdp).to.have.ordered.members(['', '']); expect(timeoutLog.mpvid).to.have.ordered.members(['', '']); expect(timeoutLog.crid).to.have.ordered.members(['', '451466393']); diff --git a/test/spec/modules/medianetBidAdapter_spec.js b/test/spec/modules/medianetBidAdapter_spec.js index 649929056fa..adb9663ba7c 100644 --- a/test/spec/modules/medianetBidAdapter_spec.js +++ b/test/spec/modules/medianetBidAdapter_spec.js @@ -1,7 +1,9 @@ import {expect} from 'chai'; import {spec} from 'modules/medianetBidAdapter.js'; +import { makeSlot } from '../integration/faker/googletag.js'; import { config } from 'src/config.js'; +$$PREBID_GLOBAL$$.version = $$PREBID_GLOBAL$$.version || 'version'; let VALID_BID_REQUEST = [{ 'bidder': 'medianet', 'params': { @@ -950,6 +952,42 @@ let VALID_BID_REQUEST = [{ } } }, + SERVER_VIDEO_OUTSTREAM_RESPONSE_VALID_BID = { + body: { + 'id': 'd90ca32f-3877-424a-b2f2-6a68988df57a', + 'bidList': [{ + 'no_bid': false, + 'requestId': '27210feac00e96', + 'cpm': 12.00, + 'width': 640, + 'height': 480, + 'ttl': 180, + 'creativeId': '370637746', + 'netRevenue': true, + 'vastXml': '', + 'currency': 'USD', + 'dfp_id': 'video1', + 'mediaType': 'video', + 'vto': 5000, + 'mavtr': 10, + 'avp': true, + 'ap': true, + 'pl': true, + 'mt': true, + 'jslt': 3000, + 'context': 'outstream' + }], + 'ext': { + 'csUrl': [{ + 'type': 'image', + 'url': 'http://cs.media.net/cksync.php' + }, { + 'type': 'iframe', + 'url': 'http://contextual.media.net/checksync.php?&vsSync=1' + }] + } + } + }, SERVER_VALID_BIDS = [{ 'no_bid': false, 'requestId': '27210feac00e96', @@ -1306,6 +1344,30 @@ describe('Media.net bid adapter', function () { expect(data.imp[1].ext).to.not.have.ownPropertyDescriptor('viewability'); expect(data.imp[1].ext.visibility).to.equal(0); }); + it('slot visibility should be calculable even in case of adUnitPath', function () { + const code = '/19968336/header-bid-tag-0'; + const divId = 'div-gpt-ad-1460505748561-0'; + window.googletag.pubads().setSlots([makeSlot({ code, divId })]); + + let boundingRect = { + top: 1010, + left: 1010, + bottom: 1050, + right: 1050 + }; + documentStub.withArgs(divId).returns({ + getBoundingClientRect: () => boundingRect + }); + documentStub.withArgs('div-gpt-ad-1460505748561-123').returns({ + getBoundingClientRect: () => boundingRect + }); + + const bidRequest = [{...VALID_BID_REQUEST[0], adUnitCode: code}] + const bidReq = spec.buildRequests(bidRequest, VALID_AUCTIONDATA); + const data = JSON.parse(bidReq.data); + expect(data.imp[0].ext.visibility).to.equal(2); + expect(data.imp[0].ext.viewability).to.equal(0); + }); }); describe('getUserSyncs', function () { @@ -1379,4 +1441,50 @@ describe('Media.net bid adapter', function () { expect(response).to.deep.equal(undefined); }); }); + + it('context should be outstream', function () { + let bids = spec.interpretResponse(SERVER_VIDEO_OUTSTREAM_RESPONSE_VALID_BID, []); + expect(bids[0].context).to.equal('outstream'); + }); + describe('buildRequests floor tests', function () { + let floor; + let getFloor = function(req) { + return floor[req.mediaType]; + }; + beforeEach(function () { + floor = { + 'banner': { + 'currency': 'USD', + 'floor': 1 + } + }; + $$PREBID_GLOBAL$$.medianetGlobals = {}; + + let documentStub = sandbox.stub(document, 'getElementById'); + let boundingRect = { + top: 50, + left: 50, + bottom: 100, + right: 100 + }; + documentStub.withArgs('div-gpt-ad-1460505748561-123').returns({ + getBoundingClientRect: () => boundingRect + }); + documentStub.withArgs('div-gpt-ad-1460505748561-0').returns({ + getBoundingClientRect: () => boundingRect + }); + let windowSizeStub = sandbox.stub(spec, 'getWindowSize'); + windowSizeStub.returns({ + w: 1000, + h: 1000 + }); + VALID_BID_REQUEST[0].getFloor = getFloor; + }); + + it('should build valid payload with floor', function () { + let requestObj = spec.buildRequests(VALID_BID_REQUEST, VALID_AUCTIONDATA); + requestObj = JSON.parse(requestObj.data); + expect(requestObj.imp[0].hasOwnProperty('bidfloors')).to.equal(true); + }); + }); }); diff --git a/test/spec/modules/mediasquareBidAdapter_spec.js b/test/spec/modules/mediasquareBidAdapter_spec.js index 351fbb40228..796be3420e8 100644 --- a/test/spec/modules/mediasquareBidAdapter_spec.js +++ b/test/spec/modules/mediasquareBidAdapter_spec.js @@ -24,6 +24,43 @@ describe('MediaSquare bid adapter tests', function () { code: 'publishername_atf_desktop_rg_pave' }, }]; + var VIDEO_PARAMS = [{ + adUnitCode: 'banner-div', + bidId: 'aaaa1234', + auctionId: 'bbbb1234', + transactionId: 'cccc1234', + mediaTypes: { + video: { + context: 'instream', + playerSize: [640, 480], + mimes: ['video/mp4'], + } + }, + bidder: 'mediasquare', + params: { + owner: 'test', + code: 'publishername_atf_desktop_rg_pave' + }, + }]; + var NATIVE_PARAMS = [{ + adUnitCode: 'banner-div', + bidId: 'aaaa1234', + auctionId: 'bbbb1234', + transactionId: 'cccc1234', + mediaTypes: { + native: { + title: { + required: true, + len: 80 + }, + } + }, + bidder: 'mediasquare', + params: { + owner: 'test', + code: 'publishername_atf_desktop_rg_pave' + }, + }]; var BID_RESPONSE = {'body': { 'responses': [{ @@ -53,7 +90,7 @@ describe('MediaSquare bid adapter tests', function () { canonicalUrl: 'https://www.prebid.org/the/link/to/the/page' }, uspConsent: '111222333', - userId: {'id5id': '1111'}, + userId: { 'id5id': { uid: '1111' } }, schain: { 'ver': '1.0', 'complete': 1, @@ -128,4 +165,33 @@ describe('MediaSquare bid adapter tests', function () { expect(syncs[0]).to.have.property('type').and.to.equal('image'); expect(syncs[0]).to.have.property('url').and.to.equal('http://www.cookie.sync.org/'); }); + it('Verifies user sync with no bid response', function() { + var syncs = spec.getUserSyncs({}, null, DEFAULT_OPTIONS.gdprConsent, DEFAULT_OPTIONS.uspConsent); + expect(syncs).to.have.property('type').and.to.equal('iframe'); + }); + it('Verifies user sync with no bid body response', function() { + var syncs = spec.getUserSyncs({}, [], DEFAULT_OPTIONS.gdprConsent, DEFAULT_OPTIONS.uspConsent); + expect(syncs).to.have.property('type').and.to.equal('iframe'); + var syncs = spec.getUserSyncs({}, [{}], DEFAULT_OPTIONS.gdprConsent, DEFAULT_OPTIONS.uspConsent); + expect(syncs).to.have.property('type').and.to.equal('iframe'); + }); + it('Verifies native in bid response', function () { + const request = spec.buildRequests(NATIVE_PARAMS, DEFAULT_OPTIONS); + BID_RESPONSE.body.responses[0].native = {'title': 'native title'}; + const response = spec.interpretResponse(BID_RESPONSE, request); + expect(response).to.have.lengthOf(1); + const bid = response[0]; + expect(bid).to.have.property('native'); + delete BID_RESPONSE.body.responses[0].native; + }); + it('Verifies video in bid response', function () { + const request = spec.buildRequests(VIDEO_PARAMS, DEFAULT_OPTIONS); + BID_RESPONSE.body.responses[0].video = {'xml': 'my vast XML', 'url': 'my vast url'}; + const response = spec.interpretResponse(BID_RESPONSE, request); + expect(response).to.have.lengthOf(1); + const bid = response[0]; + expect(bid).to.have.property('vastXml'); + expect(bid).to.have.property('vastUrl'); + delete BID_RESPONSE.body.responses[0].video; + }); }); 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/mobfoxpbBidAdapter_spec.js b/test/spec/modules/mobfoxpbBidAdapter_spec.js new file mode 100644 index 00000000000..a02d580ab88 --- /dev/null +++ b/test/spec/modules/mobfoxpbBidAdapter_spec.js @@ -0,0 +1,304 @@ +import {expect} from 'chai'; +import {spec} from '../../../modules/mobfoxpbBidAdapter.js'; +import { BANNER, VIDEO, NATIVE } from '../../../src/mediaTypes.js'; + +describe('MobfoxHBBidAdapter', function () { + const bid = { + bidId: '23fhj33i987f', + bidder: 'mobfoxpb', + 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://bes.mobfox.com/?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; + }); + }); +}); diff --git a/test/spec/modules/multibid_spec.js b/test/spec/modules/multibid_spec.js new file mode 100644 index 00000000000..e849392ee4b --- /dev/null +++ b/test/spec/modules/multibid_spec.js @@ -0,0 +1,845 @@ +import {expect} from 'chai'; +import { + validateMultibid, + adjustBidderRequestsHook, + addBidResponseHook, + resetMultibidUnits, + sortByMultibid, + targetBidPoolHook, + resetMultiConfig +} from 'modules/multibid/index.js'; +import {parse as parseQuery} from 'querystring'; +import {config} from 'src/config.js'; +import * as utils from 'src/utils.js'; +import find from 'core-js-pure/features/array/find.js'; + +describe('multibid adapter', function () { + let bidArray = [{ + 'bidderCode': 'bidderA', + 'requestId': '1c5f0a05d3629a', + 'cpm': 75, + 'originalCpm': 75, + 'bidder': 'bidderA', + }, { + 'bidderCode': 'bidderA', + 'cpm': 52, + 'requestId': '2e6j8s05r4363h', + 'originalCpm': 52, + 'bidder': 'bidderA', + }]; + let bidCacheArray = [{ + 'bidderCode': 'bidderA', + 'requestId': '1c5f0a05d3629a', + 'cpm': 66, + 'originalCpm': 66, + 'bidder': 'bidderA', + 'originalBidder': 'bidderA', + 'multibidPrefix': 'bidA' + }, { + 'bidderCode': 'bidderA', + 'cpm': 38, + 'requestId': '2e6j8s05r4363h', + 'originalCpm': 38, + 'bidder': 'bidderA', + 'originalBidder': 'bidderA', + 'multibidPrefix': 'bidA' + }]; + let bidArrayAlt = [{ + 'bidderCode': 'bidderA', + 'requestId': '1c5f0a05d3629a', + 'cpm': 29, + 'originalCpm': 29, + 'bidder': 'bidderA' + }, { + 'bidderCode': 'bidderA', + 'cpm': 52, + 'requestId': '2e6j8s05r4363h', + 'originalCpm': 52, + 'bidder': 'bidderA' + }, { + 'bidderCode': 'bidderB', + 'cpm': 3, + 'requestId': '7g8h5j45l7654i', + 'originalCpm': 3, + 'bidder': 'bidderB' + }, { + 'bidderCode': 'bidderC', + 'cpm': 12, + 'requestId': '9d7f4h56t6483u', + 'originalCpm': 12, + 'bidder': 'bidderC' + }]; + let bidderRequests = [{ + 'bidderCode': 'bidderA', + 'auctionId': 'e6bd4400-28fc-459b-9905-ad64d044daaa', + 'bidderRequestId': '10e78266423c0e', + 'bids': [{ + 'bidder': 'bidderA', + 'params': {'placementId': 1234567}, + 'crumbs': {'pubcid': 'fb4cfc66-ff3d-4fda-bef8-3f2cb6fe9412'}, + 'mediaTypes': { + 'banner': { + 'sizes': [[300, 250]] + } + }, + 'adUnitCode': 'test-div', + 'transactionId': 'c153f3da-84f0-4be8-95cb-0647c458bc60', + 'sizes': [[300, 250]], + 'bidId': '2408ef83b84c9d', + 'bidderRequestId': '10e78266423c0e', + 'auctionId': 'e6bd4400-28fc-459b-9905-ad64d044daaa', + 'src': 'client', + 'bidRequestsCount': 1, + 'bidderRequestsCount': 1, + 'bidderWinsCount': 0 + }] + }, { + 'bidderCode': 'bidderB', + 'auctionId': 'e6bd4400-28fc-459b-9905-ad64d044daaa', + 'bidderRequestId': '10e78266423c0e', + 'bids': [{ + 'bidder': 'bidderB', + 'params': {'placementId': 1234567}, + 'crumbs': {'pubcid': 'fb4cfc66-ff3d-4fda-bef8-3f2cb6fe9412'}, + 'mediaTypes': { + 'banner': { + 'sizes': [[300, 250]] + } + }, + 'adUnitCode': 'test-div', + 'transactionId': 'c153f3da-84f0-4be8-95cb-0647c458bc60', + 'sizes': [[300, 250]], + 'bidId': '2408ef83b84c9d', + 'bidderRequestId': '10e78266423c0e', + 'auctionId': 'e6bd4400-28fc-459b-9905-ad64d044daaa', + 'src': 'client', + 'bidRequestsCount': 1, + 'bidderRequestsCount': 1, + 'bidderWinsCount': 0 + }] + }]; + + afterEach(function () { + config.resetConfig(); + resetMultiConfig(); + resetMultibidUnits(); + }); + + describe('adjustBidderRequestsHook', function () { + let result; + let callbackFn = function (bidderRequests) { + result = bidderRequests; + }; + + beforeEach(function() { + result = null; + }); + + it('does not modify bidderRequest when no multibid config exists', function () { + let bidRequests = [{...bidderRequests[0]}]; + + adjustBidderRequestsHook(callbackFn, bidRequests); + + expect(result).to.not.equal(null); + expect(result).to.deep.equal(bidRequests); + }); + + it('does modify bidderRequest when multibid config exists', function () { + let bidRequests = [{...bidderRequests[0]}]; + + config.setConfig({multibid: [{bidder: 'bidderA', maxBids: 2}]}); + + adjustBidderRequestsHook(callbackFn, [{...bidderRequests[0]}]); + + expect(result).to.not.equal(null); + expect(result).to.not.deep.equal(bidRequests); + expect(result[0].bidLimit).to.equal(2); + }); + + it('does modify bidderRequest when multibid config exists using bidders array', function () { + let bidRequests = [{...bidderRequests[0]}]; + + config.setConfig({multibid: [{bidders: ['bidderA'], maxBids: 2}]}); + + adjustBidderRequestsHook(callbackFn, [{...bidderRequests[0]}]); + + expect(result).to.not.equal(null); + expect(result).to.not.deep.equal(bidRequests); + expect(result[0].bidLimit).to.equal(2); + }); + + it('does only modifies bidderRequest when multibid config exists for bidder', function () { + let bidRequests = [{...bidderRequests[0]}, {...bidderRequests[1]}]; + + config.setConfig({multibid: [{bidder: 'bidderA', maxBids: 2}]}); + + adjustBidderRequestsHook(callbackFn, [{...bidderRequests[0]}, {...bidderRequests[1]}]); + + expect(result).to.not.equal(null); + expect(result[0]).to.not.deep.equal(bidRequests[0]); + expect(result[0].bidLimit).to.equal(2); + expect(result[1]).to.deep.equal(bidRequests[1]); + expect(result[1].bidLimit).to.equal(undefined); + }); + }); + + describe('addBidResponseHook', function () { + let result; + let callbackFn = function (adUnitCode, bid) { + result = { + 'adUnitCode': adUnitCode, + 'bid': bid + }; + }; + + beforeEach(function() { + result = null; + }); + + it('adds original bids and does not modify', function () { + let adUnitCode = 'test-div'; + let bids = [{...bidArray[0]}, {...bidArray[1]}]; + + addBidResponseHook(callbackFn, adUnitCode, {...bids[0]}); + + expect(result).to.not.equal(null); + expect(result.adUnitCode).to.not.equal(null); + expect(result.adUnitCode).to.equal('test-div'); + expect(result.bid).to.not.equal(null); + expect(result.bid).to.deep.equal(bids[0]); + + result = null; + + addBidResponseHook(callbackFn, adUnitCode, {...bids[1]}); + + expect(result).to.not.equal(null); + expect(result.adUnitCode).to.not.equal(null); + expect(result.adUnitCode).to.equal('test-div'); + expect(result.bid).to.not.equal(null); + expect(result.bid).to.deep.equal(bids[1]); + }); + + it('modifies and adds both bids based on multibid configuration', function () { + let adUnitCode = 'test-div'; + let bids = [{...bidArray[0]}, {...bidArray[1]}]; + + config.setConfig({multibid: [{bidder: 'bidderA', maxBids: 2, targetBiddercodePrefix: 'bidA'}]}); + + addBidResponseHook(callbackFn, adUnitCode, {...bids[0]}); + + bids[0].multibidPrefix = 'bidA'; + bids[0].originalBidder = 'bidderA'; + + expect(result).to.not.equal(null); + expect(result.adUnitCode).to.not.equal(null); + expect(result.adUnitCode).to.equal('test-div'); + expect(result.bid).to.not.equal(null); + expect(result.bid).to.deep.equal(bids[0]); + + result = null; + + addBidResponseHook(callbackFn, adUnitCode, {...bids[1]}); + + bids[1].multibidPrefix = 'bidA'; + bids[1].originalBidder = 'bidderA'; + bids[1].targetingBidder = 'bidA2'; + bids[1].originalRequestId = '2e6j8s05r4363h'; + + delete bids[1].requestId; + delete result.bid.requestId; + + expect(result).to.not.equal(null); + expect(result.adUnitCode).to.not.equal(null); + expect(result.adUnitCode).to.equal('test-div'); + expect(result.bid).to.not.equal(null); + expect(result.bid).to.deep.equal(bids[1]); + }); + + it('only modifies bids defined in the multibid configuration', function () { + let adUnitCode = 'test-div'; + let bids = [{...bidArray[0]}, {...bidArray[1]}]; + + bids.push({ + 'bidderCode': 'bidderB', + 'cpm': 33, + 'requestId': '1j8s5f89y2345l', + 'originalCpm': 33, + 'bidder': 'bidderB', + }); + + config.setConfig({multibid: [{bidder: 'bidderA', maxBids: 2, targetBiddercodePrefix: 'bidA'}]}); + + addBidResponseHook(callbackFn, adUnitCode, {...bids[0]}); + + bids[0].multibidPrefix = 'bidA'; + bids[0].originalBidder = 'bidderA'; + + expect(result).to.not.equal(null); + expect(result.adUnitCode).to.not.equal(null); + expect(result.adUnitCode).to.equal('test-div'); + expect(result.bid).to.not.equal(null); + expect(result.bid).to.deep.equal(bids[0]); + + result = null; + + addBidResponseHook(callbackFn, adUnitCode, {...bids[1]}); + + bids[1].multibidPrefix = 'bidA'; + bids[1].originalBidder = 'bidderA'; + bids[1].targetingBidder = 'bidA2'; + bids[1].originalRequestId = '2e6j8s05r4363h'; + bids[1].requestId = result.bid.requestId; + + expect(result).to.not.equal(null); + expect(result.adUnitCode).to.not.equal(null); + expect(result.adUnitCode).to.equal('test-div'); + expect(result.bid).to.not.equal(null); + expect(result.bid).to.deep.equal(bids[1]); + + result = null; + + addBidResponseHook(callbackFn, adUnitCode, {...bids[2]}); + + expect(result).to.not.equal(null); + expect(result.adUnitCode).to.not.equal(null); + expect(result.adUnitCode).to.equal('test-div'); + expect(result.bid).to.not.equal(null); + expect(result.bid).to.deep.equal(bids[2]); + }); + + it('only modifies and returns bids under limit for a specifc bidder in the multibid configuration', function () { + let adUnitCode = 'test-div'; + let bids = [{...bidArray[0]}, {...bidArray[1]}]; + + bids.push({ + 'bidderCode': 'bidderA', + 'cpm': 33, + 'requestId': '1j8s5f89y2345l', + 'originalCpm': 33, + 'bidder': 'bidderA', + }); + + config.setConfig({multibid: [{bidder: 'bidderA', maxBids: 2, targetBiddercodePrefix: 'bidA'}]}); + + addBidResponseHook(callbackFn, adUnitCode, {...bids[0]}); + + bids[0].multibidPrefix = 'bidA'; + bids[0].originalBidder = 'bidderA'; + + expect(result).to.not.equal(null); + expect(result.adUnitCode).to.not.equal(null); + expect(result.adUnitCode).to.equal('test-div'); + expect(result.bid).to.not.equal(null); + expect(result.bid).to.deep.equal(bids[0]); + + result = null; + + addBidResponseHook(callbackFn, adUnitCode, {...bids[1]}); + + bids[1].multibidPrefix = 'bidA'; + bids[1].originalBidder = 'bidderA'; + bids[1].targetingBidder = 'bidA2'; + bids[1].originalRequestId = '2e6j8s05r4363h'; + bids[1].requestId = result.bid.requestId; + + expect(result).to.not.equal(null); + expect(result.adUnitCode).to.not.equal(null); + expect(result.adUnitCode).to.equal('test-div'); + expect(result.bid).to.not.equal(null); + expect(result.bid).to.deep.equal(bids[1]); + + result = null; + + addBidResponseHook(callbackFn, adUnitCode, {...bids[2]}); + + expect(result).to.equal(null); + }); + + it('if no prefix in multibid configuration, modifies and returns bids under limit without preifx property', function () { + let adUnitCode = 'test-div'; + let bids = [{...bidArray[0]}, {...bidArray[1]}]; + + bids.push({ + 'bidderCode': 'bidderA', + 'cpm': 33, + 'requestId': '1j8s5f89y2345l', + 'originalCpm': 33, + 'bidder': 'bidderA', + }); + + config.setConfig({multibid: [{bidder: 'bidderA', maxBids: 2}]}); + + addBidResponseHook(callbackFn, adUnitCode, {...bids[0]}); + + bids[0].originalBidder = 'bidderA'; + + expect(result).to.not.equal(null); + expect(result.adUnitCode).to.not.equal(null); + expect(result.adUnitCode).to.equal('test-div'); + expect(result.bid).to.not.equal(null); + expect(result.bid).to.deep.equal(bids[0]); + + result = null; + + addBidResponseHook(callbackFn, adUnitCode, {...bids[1]}); + + bids[1].originalBidder = 'bidderA'; + bids[1].originalRequestId = '2e6j8s05r4363h'; + bids[1].requestId = result.bid.requestId; + + expect(result).to.not.equal(null); + expect(result.adUnitCode).to.not.equal(null); + expect(result.adUnitCode).to.equal('test-div'); + expect(result.bid).to.not.equal(null); + expect(result.bid).to.deep.equal(bids[1]); + + result = null; + + addBidResponseHook(callbackFn, adUnitCode, {...bids[2]}); + + expect(result).to.equal(null); + }); + + it('does not include extra bids if cpm is less than floor value', function () { + let adUnitCode = 'test-div'; + let bids = [{...bidArrayAlt[1]}, {...bidArrayAlt[0]}, {...bidArrayAlt[2]}, {...bidArrayAlt[3]}]; + + bids.map(bid => { + bid.floorData = { + cpmAfterAdjustments: bid.cpm, + enforcements: { + enforceJS: true, + enforcePBS: false, + floorDeals: false, + bidAdjustment: true + }, + floorCurrency: 'USD', + floorRule: '*|banner', + floorRuleValue: 65, + floorValue: 65, + matchedFields: { + gptSlot: 'test-div', + mediaType: 'banner' + } + } + + return bid; + }); + + config.setConfig({multibid: [{bidder: 'bidderA', maxBids: 2, targetBiddercodePrefix: 'bidA'}]}); + + addBidResponseHook(callbackFn, adUnitCode, {...bids[0]}); + + bids[0].multibidPrefix = 'bidA'; + bids[0].originalBidder = 'bidderA'; + + expect(result).to.not.equal(null); + expect(result.adUnitCode).to.not.equal(null); + expect(result.adUnitCode).to.equal('test-div'); + expect(result.bid).to.not.equal(null); + expect(result.bid.bidder).to.equal('bidderA'); + expect(result.bid.targetingBidder).to.equal(undefined); + + result = null; + + addBidResponseHook(callbackFn, adUnitCode, {...bids[1]}); + + expect(result).to.equal(null); + + result = null; + + addBidResponseHook(callbackFn, adUnitCode, {...bids[2]}); + + expect(result).to.not.equal(null); + expect(result.adUnitCode).to.not.equal(null); + expect(result.adUnitCode).to.equal('test-div'); + expect(result.bid).to.not.equal(null); + expect(result.bid.bidder).to.equal('bidderB'); + expect(result.bid.targetingBidder).to.equal(undefined); + + result = null; + + addBidResponseHook(callbackFn, adUnitCode, {...bids[3]}); + + expect(result).to.not.equal(null); + expect(result.adUnitCode).to.not.equal(null); + expect(result.adUnitCode).to.equal('test-div'); + expect(result.bid).to.not.equal(null); + expect(result.bid.bidder).to.equal('bidderC'); + expect(result.bid.targetingBidder).to.equal(undefined); + }); + + it('does include extra bids if cpm is not less than floor value', function () { + let adUnitCode = 'test-div'; + let bids = [{...bidArrayAlt[1]}, {...bidArrayAlt[0]}]; + + bids.map(bid => { + bid.floorData = { + cpmAfterAdjustments: bid.cpm, + enforcements: { + enforceJS: true, + enforcePBS: false, + floorDeals: false, + bidAdjustment: true + }, + floorCurrency: 'USD', + floorRule: '*|banner', + floorRuleValue: 25, + floorValue: 25, + matchedFields: { + gptSlot: 'test-div', + mediaType: 'banner' + } + } + + return bid; + }); + + config.setConfig({multibid: [{bidder: 'bidderA', maxBids: 2, targetBiddercodePrefix: 'bidA'}]}); + + addBidResponseHook(callbackFn, adUnitCode, {...bids[0]}); + + bids[0].multibidPrefix = 'bidA'; + bids[0].originalBidder = 'bidderA'; + + expect(result).to.not.equal(null); + expect(result.adUnitCode).to.not.equal(null); + expect(result.adUnitCode).to.equal('test-div'); + expect(result.bid).to.not.equal(null); + expect(result.bid.bidder).to.equal('bidderA'); + expect(result.bid.targetingBidder).to.equal(undefined); + + result = null; + + addBidResponseHook(callbackFn, adUnitCode, {...bids[0]}); + + bids[0].multibidPrefix = 'bidA'; + bids[0].originalBidder = 'bidderA'; + + expect(result).to.not.equal(null); + expect(result.adUnitCode).to.not.equal(null); + expect(result.adUnitCode).to.equal('test-div'); + expect(result.bid).to.not.equal(null); + expect(result.bid.bidder).to.equal('bidderA'); + expect(result.bid.targetingBidder).to.equal('bidA2'); + }); + }); + + describe('targetBidPoolHook', function () { + let result; + let bidResult; + let callbackFn = function (bidsReceived, highestCpmCallback, adUnitBidLimit = 0, hasModified = false) { + result = { + 'bidsReceived': bidsReceived, + 'adUnitBidLimit': adUnitBidLimit, + 'hasModified': hasModified + }; + }; + let bidResponseCallback = function (adUnitCode, bid) { + bidResult = bid; + }; + + beforeEach(function() { + result = null; + bidResult = null; + }); + + it('it does not run filter on bidsReceived if no multibid configuration found', function () { + let bids = [{...bidArray[0]}, {...bidArray[1]}]; + targetBidPoolHook(callbackFn, bids, utils.getHighestCpm); + + expect(result).to.not.equal(null); + expect(result.bidsReceived).to.not.equal(null); + expect(result.bidsReceived.length).to.equal(2); + expect(result.bidsReceived).to.deep.equal(bids); + expect(result.adUnitBidLimit).to.not.equal(null); + expect(result.adUnitBidLimit).to.equal(0); + expect(result.hasModified).to.not.equal(null); + expect(result.hasModified).to.equal(false); + }); + + it('it does filter on bidsReceived if multibid configuration found with no prefix', function () { + let bids = [{...bidArray[0]}, {...bidArray[1]}]; + + config.setConfig({multibid: [{bidder: 'bidderA', maxBids: 2}]}); + + targetBidPoolHook(callbackFn, bids, utils.getHighestCpm); + bids.pop(); + + expect(result).to.not.equal(null); + expect(result.bidsReceived).to.not.equal(null); + expect(result.bidsReceived.length).to.equal(1); + expect(result.bidsReceived).to.deep.equal(bids); + expect(result.adUnitBidLimit).to.not.equal(null); + expect(result.adUnitBidLimit).to.equal(0); + expect(result.hasModified).to.not.equal(null); + expect(result.hasModified).to.equal(true); + }); + + it('it sorts and creates dynamic alias on bidsReceived if multibid configuration found with prefix', function () { + let modifiedBids = [{...bidArray[1]}, {...bidArray[0]}].map(bid => { + addBidResponseHook(bidResponseCallback, 'test-div', {...bid}); + + return bidResult; + }); + + config.setConfig({multibid: [{bidder: 'bidderA', maxBids: 2, targetBiddercodePrefix: 'bidA'}]}); + + targetBidPoolHook(callbackFn, modifiedBids, utils.getHighestCpm); + + expect(result).to.not.equal(null); + expect(result.bidsReceived).to.not.equal(null); + expect(result.bidsReceived.length).to.equal(2); + expect(result.bidsReceived).to.deep.equal([modifiedBids[1], modifiedBids[0]]); + expect(result.bidsReceived[0].bidderCode).to.equal('bidderA'); + expect(result.bidsReceived[0].bidder).to.equal('bidderA'); + expect(result.bidsReceived[1].bidderCode).to.equal('bidA2'); + expect(result.bidsReceived[1].bidder).to.equal('bidderA'); + expect(result.adUnitBidLimit).to.not.equal(null); + expect(result.adUnitBidLimit).to.equal(0); + expect(result.hasModified).to.not.equal(null); + expect(result.hasModified).to.equal(true); + }); + + it('it sorts by cpm treating dynamic alias as unique bid when no bid limit defined', function () { + let modifiedBids = [{...bidArrayAlt[0]}, {...bidArrayAlt[2]}, {...bidArrayAlt[3]}, {...bidArrayAlt[1]}].map(bid => { + addBidResponseHook(bidResponseCallback, 'test-div', {...bid}); + + return bidResult; + }); + + config.setConfig({multibid: [{bidder: 'bidderA', maxBids: 2, targetBiddercodePrefix: 'bidA'}]}); + + targetBidPoolHook(callbackFn, modifiedBids, utils.getHighestCpm); + + expect(result).to.not.equal(null); + expect(result.bidsReceived).to.not.equal(null); + expect(result.bidsReceived.length).to.equal(4); + expect(result.bidsReceived).to.deep.equal([modifiedBids[3], modifiedBids[0], modifiedBids[2], modifiedBids[1]]); + expect(result.bidsReceived[0].bidderCode).to.equal('bidderA'); + expect(result.bidsReceived[0].bidder).to.equal('bidderA'); + expect(result.bidsReceived[0].cpm).to.equal(52); + expect(result.bidsReceived[1].bidderCode).to.equal('bidA2'); + expect(result.bidsReceived[1].bidder).to.equal('bidderA'); + expect(result.bidsReceived[1].cpm).to.equal(29); + expect(result.bidsReceived[2].bidderCode).to.equal('bidderC'); + expect(result.bidsReceived[2].bidder).to.equal('bidderC'); + expect(result.bidsReceived[2].cpm).to.equal(12); + expect(result.bidsReceived[3].bidderCode).to.equal('bidderB'); + expect(result.bidsReceived[3].bidder).to.equal('bidderB'); + expect(result.bidsReceived[3].cpm).to.equal(3); + expect(result.adUnitBidLimit).to.not.equal(null); + expect(result.adUnitBidLimit).to.equal(0); + expect(result.hasModified).to.not.equal(null); + expect(result.hasModified).to.equal(true); + }); + + it('it should filter out dynamic bid when bid limit is less than unique bid pool', function () { + let modifiedBids = [{...bidArrayAlt[0]}, {...bidArrayAlt[2]}, {...bidArrayAlt[3]}, {...bidArrayAlt[1]}].map(bid => { + addBidResponseHook(bidResponseCallback, 'test-div', {...bid}); + + return bidResult; + }); + + config.setConfig({ multibid: [{bidder: 'bidderA', maxBids: 2, targetBiddercodePrefix: 'bidA'}] }); + + targetBidPoolHook(callbackFn, modifiedBids, utils.getHighestCpm, 3); + + expect(result).to.not.equal(null); + expect(result.bidsReceived).to.not.equal(null); + expect(result.bidsReceived.length).to.equal(3); + expect(result.bidsReceived).to.deep.equal([modifiedBids[3], modifiedBids[2], modifiedBids[1]]); + expect(result.bidsReceived[0].bidderCode).to.equal('bidderA'); + expect(result.bidsReceived[1].bidderCode).to.equal('bidderC'); + expect(result.bidsReceived[2].bidderCode).to.equal('bidderB'); + expect(result.adUnitBidLimit).to.not.equal(null); + expect(result.adUnitBidLimit).to.equal(3); + expect(result.hasModified).to.not.equal(null); + expect(result.hasModified).to.equal(true); + }); + + it('it should collect all bids from auction and bid cache then sort and filter', function () { + config.setConfig({ multibid: [{bidder: 'bidderA', maxBids: 2, targetBiddercodePrefix: 'bidA'}] }); + + let modifiedBids = [{...bidArrayAlt[0]}, {...bidArrayAlt[2]}, {...bidArrayAlt[3]}, {...bidArrayAlt[1]}].map(bid => { + addBidResponseHook(bidResponseCallback, 'test-div', {...bid}); + + return bidResult; + }); + + let bidPool = [].concat.apply(modifiedBids, [{...bidCacheArray[0]}, {...bidCacheArray[1]}]); + + expect(bidPool.length).to.equal(6); + + targetBidPoolHook(callbackFn, bidPool, utils.getHighestCpm); + + expect(result).to.not.equal(null); + expect(result.bidsReceived).to.not.equal(null); + expect(result.bidsReceived.length).to.equal(4); + expect(result.bidsReceived).to.deep.equal([bidPool[4], bidPool[3], bidPool[2], bidPool[1]]); + expect(result.bidsReceived[0].bidderCode).to.equal('bidderA'); + expect(result.bidsReceived[1].bidderCode).to.equal('bidA2'); + expect(result.bidsReceived[2].bidderCode).to.equal('bidderC'); + expect(result.bidsReceived[3].bidderCode).to.equal('bidderB'); + expect(result.adUnitBidLimit).to.not.equal(null); + expect(result.adUnitBidLimit).to.equal(0); + expect(result.hasModified).to.not.equal(null); + expect(result.hasModified).to.equal(true); + }); + }); + + describe('validate multibid', function () { + it('should fail validation for missing bidder name in entry', function () { + let conf = [{maxBids: 1}]; + let result = validateMultibid(conf); + + expect(result).to.equal(false); + }); + + it('should pass validation on all multibid entries', function () { + let conf = [{bidder: 'bidderA', maxBids: 1}, {bidder: 'bidderB', maxBids: 2}]; + let result = validateMultibid(conf); + + expect(result).to.equal(true); + }); + + it('should fail validation for maxbids less than 1 in entry', function () { + let conf = [{bidder: 'bidderA', maxBids: 0}, {bidder: 'bidderB', maxBids: 2}]; + let result = validateMultibid(conf); + + expect(result).to.equal(false); + }); + + it('should fail validation for maxbids greater than 9 in entry', function () { + let conf = [{bidder: 'bidderA', maxBids: 10}, {bidder: 'bidderB', maxBids: 2}]; + let result = validateMultibid(conf); + + expect(result).to.equal(false); + }); + + it('should add multbid entries to global config', function () { + config.setConfig({multibid: [{bidder: 'bidderA', maxBids: 1}]}); + let conf = config.getConfig('multibid'); + + expect(conf).to.deep.equal([{bidder: 'bidderA', maxBids: 1}]); + }); + + it('should modify multbid entries and add to global config', function () { + config.setConfig({multibid: [{bidder: 'bidderA', maxBids: 0}, {bidder: 'bidderB', maxBids: 15}]}); + let conf = config.getConfig('multibid'); + + expect(conf).to.deep.equal([{bidder: 'bidderA', maxBids: 1}, {bidder: 'bidderB', maxBids: 9}]); + }); + + it('should filter multbid entry and add modified to global config', function () { + config.setConfig({multibid: [{bidder: 'bidderA', maxBids: 0}, {maxBids: 15}]}); + let conf = config.getConfig('multibid'); + + expect(conf.length).to.equal(1); + expect(conf).to.deep.equal([{bidder: 'bidderA', maxBids: 1}]); + }); + }); + + describe('sort multibid', function () { + it('should not alter order', function () { + let bids = [{ + 'bidderCode': 'bidderA', + 'cpm': 75, + 'originalCpm': 75, + 'multibidPrefix': 'bidA', + 'originalBidder': 'bidderA', + 'bidder': 'bidderA', + }, { + 'bidderCode': 'bidA2', + 'cpm': 75, + 'originalCpm': 75, + 'multibidPrefix': 'bidA', + 'originalBidder': 'bidderA', + 'bidder': 'bidderA', + }]; + + let expected = [{ + 'bidderCode': 'bidderA', + 'cpm': 75, + 'originalCpm': 75, + 'multibidPrefix': 'bidA', + 'originalBidder': 'bidderA', + 'bidder': 'bidderA', + }, { + 'bidderCode': 'bidA2', + 'cpm': 75, + 'originalCpm': 75, + 'multibidPrefix': 'bidA', + 'originalBidder': 'bidderA', + 'bidder': 'bidderA', + }]; + let result = bids.sort(sortByMultibid); + + expect(result).to.deep.equal(expected); + }); + + it('should sort dynamic alias bidders to end', function () { + let bids = [{ + 'bidderCode': 'bidA2', + 'cpm': 75, + 'originalCpm': 75, + 'multibidPrefix': 'bidA', + 'originalBidder': 'bidderA', + 'bidder': 'bidderA', + }, { + 'bidderCode': 'bidderA', + 'cpm': 22, + 'originalCpm': 22, + 'multibidPrefix': 'bidA', + 'originalBidder': 'bidderA', + 'bidder': 'bidderA', + }, { + 'bidderCode': 'bidderB', + 'cpm': 4, + 'originalCpm': 4, + 'multibidPrefix': 'bidB', + 'originalBidder': 'bidderB', + 'bidder': 'bidderB', + }, { + 'bidderCode': 'bidB', + 'cpm': 2, + 'originalCpm': 2, + 'multibidPrefix': 'bidB', + 'originalBidder': 'bidderB', + 'bidder': 'bidderB', + }]; + let expected = [{ + 'bidderCode': 'bidderA', + 'cpm': 22, + 'originalCpm': 22, + 'multibidPrefix': 'bidA', + 'originalBidder': 'bidderA', + 'bidder': 'bidderA', + }, { + 'bidderCode': 'bidderB', + 'cpm': 4, + 'originalCpm': 4, + 'multibidPrefix': 'bidB', + 'originalBidder': 'bidderB', + 'bidder': 'bidderB', + }, { + 'bidderCode': 'bidA2', + 'cpm': 75, + 'originalCpm': 75, + 'multibidPrefix': 'bidA', + 'originalBidder': 'bidderA', + 'bidder': 'bidderA', + }, { + 'bidderCode': 'bidB', + 'cpm': 2, + 'originalCpm': 2, + 'multibidPrefix': 'bidB', + 'originalBidder': 'bidderB', + 'bidder': 'bidderB', + }]; + let result = bids.sort(sortByMultibid); + + expect(result).to.deep.equal(expected); + }); + }); +}); diff --git a/test/spec/modules/mwOpenLinkIdSystem_spec.js b/test/spec/modules/mwOpenLinkIdSystem_spec.js new file mode 100644 index 00000000000..fb082b8cd16 --- /dev/null +++ b/test/spec/modules/mwOpenLinkIdSystem_spec.js @@ -0,0 +1,20 @@ +import { writeCookie, mwOpenLinkIdSubModule } from 'modules/mwOpenLinkIdSystem.js'; + +const P_CONFIG_MOCK = { + params: { + accountId: '123', + partnerId: '123' + } +}; + +describe('mwOpenLinkId module', function () { + beforeEach(function() { + writeCookie(''); + }); + + it('getId() should return a MediaWallah openLink Id when the MediaWallah openLink first party cookie exists', function () { + writeCookie({eid: 'XX-YY-ZZ-123'}); + const id = mwOpenLinkIdSubModule.getId(P_CONFIG_MOCK); + expect(id).to.be.deep.equal({id: {eid: 'XX-YY-ZZ-123'}}); + }); +}); diff --git a/test/spec/modules/nativoBidAdapter_spec.js b/test/spec/modules/nativoBidAdapter_spec.js new file mode 100644 index 00000000000..e1132bf1b74 --- /dev/null +++ b/test/spec/modules/nativoBidAdapter_spec.js @@ -0,0 +1,227 @@ +import { expect } from 'chai' +import { spec } from 'modules/nativoBidAdapter.js' +// import { newBidder } from 'src/adapters/bidderFactory.js' +// import * as bidderFactory from 'src/adapters/bidderFactory.js' +// import { deepClone } from 'src/utils.js' +// import { config } from 'src/config.js' + +describe('nativoBidAdapterTests', function () { + describe('isBidRequestValid', function () { + let bid = { + bidder: 'nativo', + params: { + placementId: '10433394', + }, + adUnitCode: 'adunit-code', + sizes: [ + [300, 250], + [300, 600], + ], + bidId: '27b02036ccfa6e', + bidderRequestId: '1372cd8bd8d6a8', + auctionId: 'cfc467e4-2707-48da-becb-bcaab0b2c114', + } + + it('should return true when required params found', function () { + expect(spec.isBidRequestValid(bid)).to.equal(true) + }) + + it('should return false when required params are not passed', function () { + let bid2 = Object.assign({}, bid) + delete bid2.params + bid2.params = {} + expect(spec.isBidRequestValid(bid2)).to.equal(false) + }) + }) + + describe('buildRequests', function () { + let bidRequests = [ + { + bidder: 'nativo', + params: { + placementId: '10433394', + }, + adUnitCode: 'adunit-code', + sizes: [ + [300, 250], + [300, 600], + ], + bidId: '27b02036ccfa6e', + bidderRequestId: '1372cd8bd8d6a8', + auctionId: 'cfc467e4-2707-48da-becb-bcaab0b2c114', + transactionId: '3b36e7e0-0c3e-4006-a279-a741239154ff', + }, + ] + + it('url should contain query string parameters', function () { + const request = spec.buildRequests(bidRequests, { + bidderRequestId: 123456, + refererInfo: { + referer: 'https://www.test.com', + }, + }) + + expect(request.url).to.exist + expect(request.url).to.be.a('string') + + expect(request.url).to.include('?') + expect(request.url).to.include('ntv_url') + expect(request.url).to.include('ntv_ptd') + }) + }) +}) + +describe('interpretResponse', function () { + let response = { + id: '126456', + seatbid: [ + { + seat: 'seat_0', + bid: [ + { + id: 'f70362ac-f3cf-4225-82a5-948b690927a6', + impid: '1', + price: 3.569, + adm: '', + h: 300, + w: 250, + cat: [], + adomain: ['test.com'], + crid: '1060_72_6760217', + }, + ], + }, + ], + cur: 'USD', + } + + it('should get correct bid response', function () { + let expectedResponse = [ + { + requestId: '1F254428-AB11-4D5E-9887-567B3F952CA5', + cpm: 3.569, + currency: 'USD', + width: 300, + height: 250, + creativeId: '1060_72_6760217', + dealId: 'f70362ac-f3cf-4225-82a5-948b690927a6', + netRevenue: true, + ttl: 360, + ad: '', + meta: { + advertiserDomains: ['test.com'], + }, + }, + ] + + let bidderRequest = { + id: 123456, + bids: [ + { + params: { + placementId: 1 + } + }, + ], + } + + // mock + spec.getRequestId = () => 123456 + + let result = spec.interpretResponse({ body: response }, { bidderRequest }) + expect(Object.keys(result[0])).to.have.deep.members( + Object.keys(expectedResponse[0]) + ) + }) + + it('handles nobid responses', function () { + let response = {} + let bidderRequest + + let result = spec.interpretResponse({ body: response }, { bidderRequest }) + expect(result.length).to.equal(0) + }) +}) + +describe('getUserSyncs', function () { + const response = [ + { + body: { + cur: 'USD', + id: 'a136dbd8-4387-48bf-b8e4-ff9c1d6056ee', + seatbid: [ + { + bid: [{}], + seat: 'seat_0', + syncUrls: [ + { + type: 'image', + url: 'pixel-tracker-test-url/?{GDPR_params}', + }, + { + type: 'iframe', + url: 'iframe-tracker-test-url/?{GDPR_params}', + }, + ], + }, + ], + }, + }, + ] + + const gdprConsent = { + gdprApplies: true, + consentString: '111111' + } + + const uspConsent = { + uspConsent: '1YYY' + } + + it('Returns empty array if no supported user syncs', function () { + let userSync = spec.getUserSyncs( + { + iframeEnabled: false, + pixelEnabled: false, + }, + response, + gdprConsent, + uspConsent + ) + expect(userSync).to.be.an('array').with.lengthOf(0) + }) + + it('Returns valid iframe user sync', function () { + let userSync = spec.getUserSyncs( + { + iframeEnabled: true, + pixelEnabled: false, + }, + response, + gdprConsent, + uspConsent + ) + 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('iframe') + expect(userSync[0].url).to.contain('gdpr=1&gdpr_consent=111111&us_privacy=1YYY') + }) + + it('Returns valid URL and type', function () { + let userSync = spec.getUserSyncs( + { + iframeEnabled: false, + pixelEnabled: true, + }, + response, + gdprConsent, + uspConsent + ) + 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.contain('gdpr=1&gdpr_consent=111111&us_privacy=1YYY') + }) +}) diff --git a/test/spec/modules/nextrollIdSystem_spec.js b/test/spec/modules/nextrollIdSystem_spec.js new file mode 100644 index 00000000000..d89c7fe3c98 --- /dev/null +++ b/test/spec/modules/nextrollIdSystem_spec.js @@ -0,0 +1,56 @@ +import { nextrollIdSubmodule, storage } from 'modules/nextrollIdSystem.js'; + +const LS_VALUE = `{ + "AdID":{"id":"adid","key":"AdID"}, + "AdID:1002": {"id":"adid","key":"AdID:1002","value":"id_value"}}`; + +describe('NextrollId module', function () { + let sandbox = sinon.sandbox.create(); + let hasLocalStorageStub; + let getLocalStorageStub; + + beforeEach(function() { + hasLocalStorageStub = sandbox.stub(storage, 'hasLocalStorage'); + getLocalStorageStub = sandbox.stub(storage, 'getDataFromLocalStorage'); + }); + + afterEach(function () { + sandbox.restore(); + }) + + const testCases = [ + { + expect: { + id: {nextrollId: 'id_value'}, + }, + params: {partnerId: '1002'}, + localStorage: LS_VALUE + }, + { + expect: {id: undefined}, + params: {partnerId: '1003'}, + localStorage: LS_VALUE + }, + { + expect: {id: undefined}, + params: {partnerId: ''}, + localStorage: LS_VALUE + }, + { + expect: {id: undefined}, + params: {partnerId: '102'}, + localStorage: undefined + }, + { + expect: {id: undefined}, + params: undefined, + localStorage: undefined + } + ] + testCases.forEach( + (testCase, i) => it(`getId() (TC #${i}) should return the nextroll id if it exists`, function () { + getLocalStorageStub.withArgs('dca0.com').returns(testCase.localStorage); + const id = nextrollIdSubmodule.getId({params: testCase.params}); + expect(id).to.be.deep.equal(testCase.expect); + })) +}); diff --git a/test/spec/modules/nobidBidAdapter_spec.js b/test/spec/modules/nobidBidAdapter_spec.js index 3b8fd32160b..8dc15fbc89e 100644 --- a/test/spec/modules/nobidBidAdapter_spec.js +++ b/test/spec/modules/nobidBidAdapter_spec.js @@ -118,7 +118,7 @@ describe('Nobid Adapter', function () { expect(payload.a).to.exist; expect(payload.t).to.exist; expect(payload.tz).to.exist; - expect(payload.r).to.exist; + expect(payload.r).to.exist.and.to.equal('100x100'); expect(payload.lang).to.exist; expect(payload.ref).to.exist; expect(payload.a[0].d).to.exist.and.to.equal('adunit-code'); @@ -228,6 +228,75 @@ describe('Nobid Adapter', function () { }); }); + describe('buildRequestsEIDs', function () { + const SITE_ID = 2; + const REFERER = 'https://www.examplereferer.com'; + let bidRequests = [ + { + 'bidder': 'nobid', + 'params': { + 'siteId': SITE_ID + }, + 'adUnitCode': 'adunit-code', + 'sizes': [[300, 250]], + 'bidId': '30b31c1838de1e', + 'bidderRequestId': '22edbae2733bf6', + 'auctionId': '1d1a030790a475', + 'userIdAsEids': [ + { + 'source': 'criteo.com', + 'uids': [ + { + 'id': 'CRITEO_ID', + 'atype': 1 + } + ] + }, + { + 'source': 'id5-sync.com', + 'uids': [ + { + 'id': 'ID5_ID', + 'atype': 1, + 'ext': { + 'linkType': 0 + } + } + ] + }, + { + 'source': 'adserver.org', + 'uids': [ + { + 'id': 'TD_ID', + 'atype': 1, + 'ext': { + 'rtiPartner': 'TDID' + } + } + ] + } + ] + } + ]; + + let bidderRequest = { + refererInfo: {referer: REFERER} + } + + 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); + expect(payload.eids[0].source).to.exist.and.to.equal('criteo.com'); + expect(payload.eids[0].uids[0].id).to.exist.and.to.equal('CRITEO_ID'); + expect(payload.eids[1].source).to.exist.and.to.equal('id5-sync.com'); + expect(payload.eids[1].uids[0].id).to.exist.and.to.equal('ID5_ID'); + expect(payload.eids[2].source).to.exist.and.to.equal('adserver.org'); + expect(payload.eids[2].uids[0].id).to.exist.and.to.equal('TD_ID'); + }); + }); + describe('buildRequests', function () { const SITE_ID = 2; const REFERER = 'https://www.examplereferer.com'; @@ -264,6 +333,38 @@ describe('Nobid Adapter', function () { expect(payload.gdpr).to.exist; }); + it('sends bid request to ad size', function () { + const request = spec.buildRequests(bidRequests); + const payload = JSON.parse(request.data); + expect(payload.a).to.exist; + expect(payload.a.length).to.exist.and.to.equal(1); + expect(payload.a[0].z[0][0]).to.equal(300); + expect(payload.a[0].z[0][1]).to.equal(250); + }); + + it('sends bid request to div id', function () { + const request = spec.buildRequests(bidRequests); + const payload = JSON.parse(request.data); + expect(payload.a).to.exist; + expect(payload.a[0].d).to.equal('adunit-code'); + }); + + it('sends bid request to site id', function () { + const request = spec.buildRequests(bidRequests); + const payload = JSON.parse(request.data); + expect(payload.a).to.exist; + expect(payload.a[0].sid).to.equal(2); + expect(payload.a[0].at).to.equal('banner'); + expect(payload.a[0].params.siteId).to.equal(2); + }); + + it('sends bid request to ad type', function () { + const request = spec.buildRequests(bidRequests); + const payload = JSON.parse(request.data); + expect(payload.a).to.exist; + expect(payload.a[0].at).to.equal('banner'); + }); + it('sends bid request to ENDPOINT via POST', function () { const request = spec.buildRequests(bidRequests); expect(request.url).to.contain('ads.servenobid.com/adreq'); @@ -291,6 +392,43 @@ describe('Nobid Adapter', function () { expect(payload.gdpr.consentString).to.exist.and.to.equal(consentString); expect(payload.gdpr.consentRequired).to.exist.and.to.be.true; }); + + it('should add gdpr consent information to the request', function () { + let bidderRequest = { + 'bidderCode': 'nobid', + 'auctionId': '1d1a030790a475', + 'bidderRequestId': '22edbae2733bf6', + 'timeout': 3000, + 'gdprConsent': { + gdprApplies: false + } + }; + bidderRequest.bids = bidRequests; + + const request = spec.buildRequests(bidRequests, bidderRequest); + const payload = JSON.parse(request.data); + + expect(payload.gdpr).to.exist; + expect(payload.gdpr.consentString).to.not.exist; + expect(payload.gdpr.consentRequired).to.exist.and.to.be.false; + }); + + it('should add usp consent information to the request', function () { + let bidderRequest = { + 'bidderCode': 'nobid', + 'auctionId': '1d1a030790a475', + 'bidderRequestId': '22edbae2733bf6', + 'timeout': 3000, + 'uspConsent': '1Y-N' + }; + bidderRequest.bids = bidRequests; + + const request = spec.buildRequests(bidRequests, bidderRequest); + const payload = JSON.parse(request.data); + + expect(payload.usp).to.exist; + expect(payload.usp).to.exist.and.to.equal('1Y-N'); + }); }); describe('buildRequestsRefreshCount', function () { diff --git a/test/spec/modules/novatiqIdSystem_spec.js b/test/spec/modules/novatiqIdSystem_spec.js new file mode 100644 index 00000000000..60c82626450 --- /dev/null +++ b/test/spec/modules/novatiqIdSystem_spec.js @@ -0,0 +1,70 @@ +import { novatiqIdSubmodule } from 'modules/novatiqIdSystem.js'; +import * as utils from 'src/utils.js'; +import { server } from 'test/mocks/xhr.js'; + +describe('novatiqIdSystem', function () { + describe('getSrcId', function() { + it('getSrcId should set srcId value to 000 due to undefined parameter in config section', function() { + const config = { params: { } }; + const configParams = config.params || {}; + const response = novatiqIdSubmodule.getSrcId(configParams); + expect(response).to.eq('000'); + }); + + it('getSrcId should set srcId value to 000 due to missing value in config section', function() { + const config = { params: { sourceid: '' } }; + const configParams = config.params || {}; + const response = novatiqIdSubmodule.getSrcId(configParams); + expect(response).to.eq('000'); + }); + + it('getSrcId should set value to 000 due to null value in config section', function() { + const config = { params: { sourceid: null } }; + const configParams = config.params || {}; + const response = novatiqIdSubmodule.getSrcId(configParams); + expect(response).to.eq('000'); + }); + + it('getSrcId should set value to 001 due to wrong length in config section max 3 chars', function() { + const config = { params: { sourceid: '1234' } }; + const configParams = config.params || {}; + const response = novatiqIdSubmodule.getSrcId(configParams); + expect(response).to.eq('001'); + }); + + it('getSrcId should set value to 002 due to wrong format in config section', function() { + const config = { params: { sourceid: '1xc' } }; + const configParams = config.params || {}; + const response = novatiqIdSubmodule.getSrcId(configParams); + expect(response).to.eq('002'); + }); + }); + + describe('getId', function() { + it('should log message if novatiqId has wrong format', function() { + const config = { params: { sourceid: '123' } }; + const response = novatiqIdSubmodule.getId(config); + expect(response.id).to.have.length(40); + }); + + it('should log message if novatiqId not provided', function() { + const config = { params: { sourceid: '123' } }; + const response = novatiqIdSubmodule.getId(config); + expect(response.id).should.be.not.empty; + }); + }); + + describe('decode', function() { + it('should log message if novatiqId has wrong format', function() { + const novatiqId = '81b001ec-8914-488c-a96e-8c220d4ee08895ef'; + const response = novatiqIdSubmodule.decode(novatiqId); + expect(response.novatiq.snowflake).to.have.length(40); + }); + + it('should log message if novatiqId has wrong format', function() { + const novatiqId = '81b001ec-8914-488c-a96e-8c220d4ee08895ef'; + const response = novatiqIdSubmodule.decode(novatiqId); + expect(response.novatiq.snowflake).should.be.not.empty; + }); + }); +}) diff --git a/test/spec/modules/oneVideoBidAdapter_spec.js b/test/spec/modules/oneVideoBidAdapter_spec.js index a91e9ac27e8..3f5304dce0a 100644 --- a/test/spec/modules/oneVideoBidAdapter_spec.js +++ b/test/spec/modules/oneVideoBidAdapter_spec.js @@ -1,6 +1,5 @@ import { expect } from 'chai'; import { spec } from 'modules/oneVideoBidAdapter.js'; -import * as utils from 'src/utils.js'; describe('OneVideoBidAdapter', function () { let bidRequest; @@ -200,24 +199,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.3'; + const VERSION = '3.1.0'; expect(data.imp[0].video.w).to.equal(width); expect(data.imp[0].video.h).to.equal(height); expect(data.imp[0].bidfloor).to.equal(bidRequest.params.bidfloor); @@ -231,8 +230,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 +241,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 +261,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 +282,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 +301,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,46 +311,356 @@ 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 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 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 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 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 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 data = requests[0].data; + expect(data.imp[0].ext.custom).to.be.a('object'); + }); + it('should accept key values pairs if custom is Object ', function () { + bidRequest.params.video.custom = { + key1: 'value1', + key2: 'value2', + key3: 4444444, + key4: false, + key5: { + nested: 'object' + }, + key6: ['string', 2, true, null], + key7: null, + key8: undefined + }; + 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'); + expect(custom['key3']).to.be.a('number'); + expect(custom['key4']).to.not.exist; + expect(custom['key5']).to.not.exist; + expect(custom['key6']).to.not.exist; + 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.site.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.site.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.site.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.site.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.site.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.site.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.site.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.site.content).to.be.a('object'); + expect(data.site.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.site.content).to.be.a('object'); + expect(data.site.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.site.content).to.be.a('object'); + expect(data.site.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.site.content).to.be.a('object'); + expect(data.site.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.site.content).to.deep.equal(bidRequest.params.video.content); + }); + }); }); + describe('price floor module validations', function () { + beforeEach(function () { + bidRequest.getFloor = (floorObj) => { + return { + floor: bidRequest.floors.values[floorObj.mediaType + '|640x480'], + currency: floorObj.currency, + mediaType: floorObj.mediaType + } + } + }); + + it('should get bidfloor from getFloor method', function () { + bidRequest.params.cur = 'EUR'; + bidRequest.floors = { + currency: 'EUR', + values: { + 'video|640x480': 5.55 + } + }; + const requests = spec.buildRequests([bidRequest], bidderRequest); + const data = requests[0].data; + expect(data.cur).is.a('string'); + expect(data.cur).to.equal('EUR'); + expect(data.imp[0].bidfloor).is.a('number'); + expect(data.imp[0].bidfloor).to.equal(5.55); + }); + + it('should use adUnit/module currency & floor instead of bid.params.bidfloor', function () { + bidRequest.params.cur = 'EUR'; + bidRequest.params.bidfloor = 3.33; + bidRequest.floors = { + currency: 'EUR', + values: { + 'video|640x480': 5.55 + } + }; + const requests = spec.buildRequests([bidRequest], bidderRequest); + const data = requests[0].data; + expect(data.cur).is.a('string'); + expect(data.cur).to.equal('EUR'); + expect(data.imp[0].bidfloor).is.a('number'); + expect(data.imp[0].bidfloor).to.equal(5.55); + }); + + it('should load banner instead of video floor when DAP is active bid.params.video.display = 1', function () { + bidRequest.params.video.display = 1; + bidRequest.params.cur = 'EUR'; + bidRequest.mediaTypes = { + banner: { + sizes: [ + [640, 480] + ] + } + }; + bidRequest.floors = { + currency: 'EUR', + values: { + 'banner|640x480': 2.22, + 'video|640x480': 9.99 + } + }; + const requests = spec.buildRequests([bidRequest], bidderRequest); + const data = requests[0].data; + expect(data.cur).is.a('string'); + expect(data.cur).to.equal('EUR'); + expect(data.imp[0].bidfloor).is.a('number'); + expect(data.imp[0].bidfloor).to.equal(2.22); + }) + + it('should load video floor when multi-format adUnit is present', function () { + bidRequest.params.cur = 'EUR'; + bidRequest.mediaTypes.banner = { + sizes: [ + [640, 480] + ] + }; + bidRequest.floors = { + currency: 'EUR', + values: { + 'banner|640x480': 2.22, + 'video|640x480': 9.99 + } + }; + const requests = spec.buildRequests([bidRequest], bidderRequest); + const data = requests[0].data; + expect(data.cur).is.a('string'); + expect(data.cur).to.equal('EUR'); + expect(data.imp[0].bidfloor).is.a('number'); + expect(data.imp[0].bidfloor).to.equal(9.99); + }) + }) + 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, cpm: serverResponse.seatbid[0].bid[0].price, - adId: serverResponse.seatbid[0].bid[0].adid, creativeId: serverResponse.seatbid[0].bid[0].crid, vastXml: serverResponse.seatbid[0].bid[0].adm, width: 640, 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, @@ -370,12 +681,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 () { @@ -418,22 +768,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); }); @@ -477,7 +827,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; @@ -527,7 +877,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; @@ -573,7 +923,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; @@ -592,15 +942,35 @@ 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}) - expect(pixel[2].type).to.equal('image'); - expect(pixel[2].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)); + 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); - expect(pixel[2].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='); + 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); + 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') + }); + + it('should be TTD sync pixel', function () { + expect(pixel[2].url).to.equal('https://match.adsrvr.org/track/cmf/generic?ttd_pid=adaptv&ttd_tpi=1') + }); + }) }); }); diff --git a/test/spec/modules/ooloAnalyticsAdapter_spec.js b/test/spec/modules/ooloAnalyticsAdapter_spec.js new file mode 100644 index 00000000000..f82f7856fb2 --- /dev/null +++ b/test/spec/modules/ooloAnalyticsAdapter_spec.js @@ -0,0 +1,793 @@ +import ooloAnalytics, { PAGEVIEW_ID } from 'modules/ooloAnalyticsAdapter.js'; +import {expect} from 'chai'; +import {server} from 'test/mocks/xhr.js'; +import constants from 'src/constants.json' +import events from 'src/events' +import { config } from 'src/config'; +import { buildAuctionData, generatePageViewId } from 'modules/ooloAnalyticsAdapter'; + +const auctionId = '0ea14159-2058-4b87-a966-9d7652176a56'; +const auctionStart = 1598513385415 +const timeout = 3000 +const adUnit1 = 'top_1'; +const adUnit2 = 'top_2'; +const bidId1 = '392b5a6b05d648' +const bidId2 = '392b5a6b05d649' +const bidId3 = '392b5a6b05d650' + +const auctionInit = { + timestamp: auctionStart, + auctionId: auctionId, + timeout: timeout, + auctionStart, + adUnits: [{ + code: adUnit1, + transactionId: 'abalksdkjfh-12sade' + }, { + code: adUnit2, + transactionId: 'abalksdkjfh-12sadf' + }] +}; + +const bidRequested = { + auctionId, + bidderCode: 'appnexus', + bidderRequestId: '2946b569352ef2', + start: 1598513405254, + bids: [ + { + auctionId, + bidId: bidId1, + bidderRequestId: '2946b569352ef2', + bidder: 'appnexus', + adUnitCode: adUnit1, + sizes: [[728, 90], [970, 90]], + mediaTypes: { + banner: { + sizes: [[728, 90], [970, 90]], + }, + }, + params: { + placementId: '4799418', + }, + schain: {} + }, + { + auctionId, + bidId: bidId2, + bidderRequestId: '2946b569352ef3', + bidder: 'rubicon', + adUnitCode: adUnit2, + sizes: [[728, 90], [970, 90]], + mediaTypes: { + banner: { + sizes: [[728, 90], [970, 90]], + }, + }, + params: { + placementId: '4799418', + }, + schain: {} + }, + { + auctionId, + bidId: bidId3, + bidderRequestId: '2946b569352ef4', + bidder: 'ix', + adUnitCode: adUnit2, + sizes: [[728, 90], [970, 90]], + mediaTypes: { + banner: { + sizes: [[728, 90], [970, 90]], + }, + }, + params: { + placementId: '4799418', + }, + schain: {} + }, + ], +} + +const noBid = { + auctionId, + adUnitCode: adUnit2, + bidId: bidId2, + requestId: bidId2 +} + +const bidResponse = { + auctionId, + bidderCode: 'appnexus', + mediaType: 'banner', + width: 0, + height: 0, + statusMessage: 'Bid available', + adId: '222bb26f9e8bd', + cpm: 0.112256, + responseTimestamp: 1598513485254, + requestTimestamp: 1462919238936, + bidder: 'appnexus', + adUnitCode: adUnit1, + timeToRespond: 401, + pbLg: '0.00', + pbMg: '0.10', + pbHg: '0.11', + pbAg: '0.10', + size: '0x0', + requestId: bidId1, + creativeId: '123456', + adserverTargeting: { + hb_bidder: 'appnexus', + hb_adid: '222bb26f9e8bd', + hb_pb: '10.00', + hb_size: '0x0', + foobar: '0x0', + }, + netRevenue: true, + currency: 'USD', + ttl: 300, +} + +const auctionEnd = { + auctionId: auctionId +}; + +const bidTimeout = [ + { + adUnitCode: adUnit2, + auctionId: auctionId, + bidId: bidId3, + bidder: 'ix', + timeout: timeout + } +]; + +const bidWon = { + auctionId, + adUnitCode: adUnit1, + bidId: bidId1, + cpm: 0.5 +} + +function simulateAuction () { + events.emit(constants.EVENTS.AUCTION_INIT, auctionInit); + events.emit(constants.EVENTS.BID_REQUESTED, bidRequested); + events.emit(constants.EVENTS.BID_RESPONSE, bidResponse); + events.emit(constants.EVENTS.NO_BID, noBid); + events.emit(constants.EVENTS.BID_TIMEOUT, bidTimeout); + events.emit(constants.EVENTS.AUCTION_END, auctionEnd); +} + +describe('oolo Prebid Analytic', () => { + let clock + + beforeEach(() => { + sinon.stub(events, 'getEvents').returns([]); + clock = sinon.useFakeTimers() + }); + + afterEach(() => { + ooloAnalytics.disableAnalytics(); + events.getEvents.restore() + clock.restore(); + }) + + describe('enableAnalytics init options', () => { + it('should not enable analytics if invalid config', () => { + ooloAnalytics.enableAnalytics({ + provider: 'oolo', + options: { + pid: undefined + } + }) + + expect(server.requests).to.have.length(0) + }) + + it('should send prebid config to the server', () => { + ooloAnalytics.enableAnalytics({ + provider: 'oolo', + options: { + pid: 123 + } + }) + + const conf = {} + const pbjsConfig = config.getConfig() + + Object.keys(pbjsConfig).forEach(key => { + if (key[0] !== '_') { + conf[key] = pbjsConfig[key] + } + }) + + expect(server.requests[1].url).to.contain('/hbconf') + expect(JSON.parse(server.requests[1].requestBody)).to.deep.equal(conf) + }) + + it('should request server config and send page data', () => { + ooloAnalytics.enableAnalytics({ + provider: 'oolo', + options: { + pid: 123 + } + }) + clock.next() + + expect(server.requests[0].url).to.contain('?pid=123') + + const pageData = JSON.parse(server.requests[2].requestBody) + + expect(pageData).to.have.property('timestamp') + expect(pageData).to.have.property('screenWidth') + expect(pageData).to.have.property('screenHeight') + expect(pageData).to.have.property('url') + expect(pageData).to.have.property('protocol') + expect(pageData).to.have.property('origin') + expect(pageData).to.have.property('referrer') + expect(pageData).to.have.property('pbVersion') + expect(pageData).to.have.property('pvid') + expect(pageData).to.have.property('pid') + expect(pageData).to.have.property('pbModuleVersion') + expect(pageData).to.have.property('domContentLoadTime') + expect(pageData).to.have.property('pageLoadTime') + }) + }) + + describe('data handling and sending events', () => { + it('should send an "auction" event to the server', () => { + ooloAnalytics.enableAnalytics({ + provider: 'oolo', + options: { + pid: 123 + } + }) + clock.next() + + server.requests[0].respond(500) + + simulateAuction() + + expect(server.requests.length).to.equal(3) + clock.tick(2000) + expect(server.requests.length).to.equal(4) + + const request = JSON.parse(server.requests[3].requestBody); + + expect(request).to.include({ + eventType: 'auction', + pid: 123, + auctionId, + auctionStart, + auctionEnd: 0, + timeout, + }) + expect(request.pvid).to.be.a('number') + expect(request.adUnits).to.have.length(2) + + const auctionAdUnit1 = request.adUnits.filter(adUnit => adUnit.adunid === adUnit1)[0] + const auctionAdUnit2 = request.adUnits.filter(adUnit => adUnit.adunid === adUnit2)[0] + + expect(auctionAdUnit1.auctionId).to.equal(auctionId) + expect(auctionAdUnit1.bids).to.be.an('array') + expect(auctionAdUnit1.bids).to.have.length(1) + + // bid response + expect(auctionAdUnit1.bids[0].bidId).to.equal(bidId1) + expect(auctionAdUnit1.bids[0].bst).to.equal('bidReceived') + expect(auctionAdUnit1.bids[0].bidder).to.equal('appnexus') + expect(auctionAdUnit1.bids[0].cpm).to.equal(0.112256) + expect(auctionAdUnit1.bids[0].cur).to.equal('USD') + expect(auctionAdUnit1.bids[0].s).to.equal(bidRequested.start) + expect(auctionAdUnit1.bids[0].e).to.equal(bidResponse.responseTimestamp) + expect(auctionAdUnit1.bids[0].rs).to.equal(bidRequested.start - auctionStart) + expect(auctionAdUnit1.bids[0].re).to.equal(bidResponse.responseTimestamp - auctionStart) + expect(auctionAdUnit1.bids[0].h).to.equal(0) + expect(auctionAdUnit1.bids[0].w).to.equal(0) + expect(auctionAdUnit1.bids[0].mt).to.equal('banner') + expect(auctionAdUnit1.bids[0].nrv).to.equal(true) + expect(auctionAdUnit1.bids[0].params).to.have.keys('placementId') + expect(auctionAdUnit1.bids[0].size).to.equal('0x0') + expect(auctionAdUnit1.bids[0].crId).to.equal('123456') + expect(auctionAdUnit1.bids[0].ttl).to.equal(bidResponse.ttl) + expect(auctionAdUnit1.bids[0].ttr).to.equal(bidResponse.timeToRespond) + + expect(auctionAdUnit2.auctionId).to.equal(auctionId) + expect(auctionAdUnit2.bids).to.be.an('array') + expect(auctionAdUnit2.bids).to.have.length(2) + + // no bid + expect(auctionAdUnit2.bids[0].bidId).to.equal(bidId2) + expect(auctionAdUnit2.bids[0].bst).to.equal('noBid') + expect(auctionAdUnit2.bids[0].bidder).to.equal('rubicon') + expect(auctionAdUnit2.bids[0].s).to.be.a('number') + expect(auctionAdUnit2.bids[0].e).to.be.a('number') + expect(auctionAdUnit2.bids[0].params).to.have.keys('placementId') + + // timeout + expect(auctionAdUnit2.bids[1].bidId).to.equal(bidId3) + expect(auctionAdUnit2.bids[1].bst).to.equal('bidTimedOut') + expect(auctionAdUnit2.bids[1].bidder).to.equal('ix') + expect(auctionAdUnit2.bids[1].s).to.be.a('number') + expect(auctionAdUnit2.bids[1].e).to.be.a('undefined') + }) + + it('should push events to a queue and process them once server configuration returns', () => { + ooloAnalytics.enableAnalytics({ + provider: 'oolo', + options: { + pid: 123 + } + }) + + events.emit(constants.EVENTS.AUCTION_INIT, auctionInit); + events.emit(constants.EVENTS.BID_REQUESTED, bidRequested); + events.emit(constants.EVENTS.BID_RESPONSE, bidResponse); + + // configuration returned in an arbitrary moment + server.requests[0].respond(500) + + events.emit(constants.EVENTS.NO_BID, noBid); + events.emit(constants.EVENTS.BID_TIMEOUT, bidTimeout); + events.emit(constants.EVENTS.AUCTION_END, auctionEnd); + + clock.tick(1500) + + const request = JSON.parse(server.requests[3].requestBody); + + expect(request).to.include({ + eventType: 'auction', + pid: 123, + auctionId, + auctionStart, + auctionEnd: 0, + timeout, + }) + expect(request.pvid).to.be.a('number') + expect(request.adUnits).to.have.length(2) + + const auctionAdUnit1 = request.adUnits.filter(adUnit => adUnit.adunid === adUnit1)[0] + const auctionAdUnit2 = request.adUnits.filter(adUnit => adUnit.adunid === adUnit2)[0] + + expect(auctionAdUnit1.auctionId).to.equal(auctionId) + expect(auctionAdUnit1.bids).to.be.an('array') + expect(auctionAdUnit1.bids).to.have.length(1) + + // bid response + expect(auctionAdUnit1.bids[0].bidId).to.equal(bidId1) + expect(auctionAdUnit1.bids[0].bst).to.equal('bidReceived') + expect(auctionAdUnit1.bids[0].bidder).to.equal('appnexus') + expect(auctionAdUnit1.bids[0].cpm).to.equal(0.112256) + expect(auctionAdUnit1.bids[0].cur).to.equal('USD') + expect(auctionAdUnit1.bids[0].s).to.equal(bidRequested.start) + expect(auctionAdUnit1.bids[0].e).to.equal(bidResponse.responseTimestamp) + expect(auctionAdUnit1.bids[0].rs).to.equal(bidRequested.start - auctionStart) + expect(auctionAdUnit1.bids[0].re).to.equal(bidResponse.responseTimestamp - auctionStart) + expect(auctionAdUnit1.bids[0].h).to.equal(0) + expect(auctionAdUnit1.bids[0].w).to.equal(0) + expect(auctionAdUnit1.bids[0].mt).to.equal('banner') + expect(auctionAdUnit1.bids[0].nrv).to.equal(true) + expect(auctionAdUnit1.bids[0].params).to.have.keys('placementId') + expect(auctionAdUnit1.bids[0].size).to.equal('0x0') + expect(auctionAdUnit1.bids[0].crId).to.equal('123456') + expect(auctionAdUnit1.bids[0].ttl).to.equal(bidResponse.ttl) + expect(auctionAdUnit1.bids[0].ttr).to.equal(bidResponse.timeToRespond) + + expect(auctionAdUnit2.auctionId).to.equal(auctionId) + expect(auctionAdUnit2.bids).to.be.an('array') + expect(auctionAdUnit2.bids).to.have.length(2) + + // no bid + expect(auctionAdUnit2.bids[0].bidId).to.equal(bidId2) + expect(auctionAdUnit2.bids[0].bst).to.equal('noBid') + expect(auctionAdUnit2.bids[0].bidder).to.equal('rubicon') + expect(auctionAdUnit2.bids[0].s).to.be.a('number') + expect(auctionAdUnit2.bids[0].e).to.be.a('number') + expect(auctionAdUnit2.bids[0].params).to.have.keys('placementId') + + // timeout + expect(auctionAdUnit2.bids[1].bidId).to.equal(bidId3) + expect(auctionAdUnit2.bids[1].bst).to.equal('bidTimedOut') + expect(auctionAdUnit2.bids[1].bidder).to.equal('ix') + expect(auctionAdUnit2.bids[1].s).to.be.a('number') + expect(auctionAdUnit2.bids[1].e).to.be.a('undefined') + }) + + it('should send "auction" event without all the fields that were set to undefined', () => { + ooloAnalytics.enableAnalytics({ + provider: 'oolo', + options: { + pid: 123 + } + }) + + server.requests[0].respond(500) + + simulateAuction() + clock.tick(1500) + + const request = JSON.parse(server.requests[3].requestBody); + + const auctionAdUnit1 = request.adUnits.filter(adUnit => adUnit.adunid === adUnit1)[0] + const auctionAdUnit2 = request.adUnits.filter(adUnit => adUnit.adunid === adUnit2)[0] + + expect(auctionAdUnit1.code).to.equal(undefined) + expect(auctionAdUnit1.transactionId).to.equal(undefined) + expect(auctionAdUnit1.adUnitCode).to.equal(undefined) + expect(auctionAdUnit1.bids[0].auctionStart).to.equal(undefined) + expect(auctionAdUnit1.bids[0].bids).to.equal(undefined) + expect(auctionAdUnit1.bids[0].refererInfo).to.equal(undefined) + expect(auctionAdUnit1.bids[0].bidRequestsCount).to.equal(undefined) + expect(auctionAdUnit1.bids[0].bidderRequestId).to.equal(undefined) + expect(auctionAdUnit1.bids[0].bidderRequestsCount).to.equal(undefined) + expect(auctionAdUnit1.bids[0].bidderWinsCount).to.equal(undefined) + expect(auctionAdUnit1.bids[0].schain).to.equal(undefined) + expect(auctionAdUnit1.bids[0].src).to.equal(undefined) + expect(auctionAdUnit1.bids[0].transactionId).to.equal(undefined) + + // no bid + expect(auctionAdUnit2.bids[0].schain).to.equal(undefined) + expect(auctionAdUnit2.bids[0].src).to.equal(undefined) + expect(auctionAdUnit2.bids[0].transactionId).to.equal(undefined) + }) + + it('should mark bid winner and send to the server along with the auction data', () => { + ooloAnalytics.enableAnalytics({ + provider: 'oolo', + options: { + pid: 123 + } + }) + + server.requests[0].respond(500) + simulateAuction() + events.emit(constants.EVENTS.BID_WON, bidWon); + clock.tick(1500) + + // no bidWon + expect(server.requests).to.have.length(4) + + const request = JSON.parse(server.requests[3].requestBody); + expect(request.adUnits[0].bids[0].isW).to.equal(true) + }) + + it('should take BID_WON_TIMEOUT from server config if exists', () => { + ooloAnalytics.enableAnalytics({ + provider: 'oolo', + options: { + pid: 123 + } + }) + + server.requests[0].respond(200, {}, JSON.stringify({ + 'events': {}, + 'BID_WON_TIMEOUT': 500 + })) + + simulateAuction() + events.emit(constants.EVENTS.BID_WON, bidWon); + clock.tick(499) + + // no auction data + expect(server.requests).to.have.length(3) + + clock.tick(1) + + // auction data + expect(server.requests).to.have.length(4) + const request = JSON.parse(server.requests[3].requestBody); + expect(request.adUnits[0].bids[0].isW).to.equal(true) + }) + + it('should send a "bidWon" event to the server', () => { + ooloAnalytics.enableAnalytics({ + provider: 'oolo', + options: { + pid: 123 + } + }) + + server.requests[0].respond(500) + simulateAuction() + clock.tick(1500) + events.emit(constants.EVENTS.BID_WON, bidWon); + + expect(server.requests).to.have.length(5) + + const request = JSON.parse(server.requests[4].requestBody); + + expect(request.eventType).to.equal('bidWon') + expect(request.auctionId).to.equal(auctionId) + expect(request.adunid).to.equal(adUnit1) + expect(request.bid.bst).to.equal('bidWon') + expect(request.bid.cur).to.equal('USD') + expect(request.bid.cpm).to.equal(0.5) + }) + + it('should sent adRenderFailed to the server', () => { + ooloAnalytics.enableAnalytics({ + provider: 'oolo', + options: { + pid: 123 + } + }) + + server.requests[0].respond(500) + simulateAuction() + clock.tick(1500) + events.emit(constants.EVENTS.AD_RENDER_FAILED, { bidId: 'abcdef', reason: 'exception' }); + + expect(server.requests).to.have.length(5) + + const request = JSON.parse(server.requests[4].requestBody); + + expect(request.eventType).to.equal('adRenderFailed') + expect(request.pvid).to.equal(PAGEVIEW_ID) + expect(request.bidId).to.equal('abcdef') + expect(request.reason).to.equal('exception') + }) + + it('should pick fields according to server configuration', () => { + ooloAnalytics.enableAnalytics({ + provider: 'oolo', + options: { + pid: 123 + } + }) + + server.requests[0].respond(200, {}, JSON.stringify({ + 'events': { + 'bidRequested': { + 'sendRaw': 0, + 'pickFields': ['transactionId'] + }, + 'noBid': { + 'sendRaw': 0, + 'pickFields': ['src'] + }, + 'bidResponse': { + 'sendRaw': 0, + 'pickFields': ['adUrl', 'statusMessage'] + }, + 'auctionEnd': { + 'sendRaw': 0, + 'pickFields': ['winningBids'] + }, + } + })) + + events.emit(constants.EVENTS.AUCTION_INIT, { ...auctionInit }); + events.emit(constants.EVENTS.BID_REQUESTED, { ...bidRequested, bids: bidRequested.bids.map(b => { b.transactionId = '123'; return b }) }); + events.emit(constants.EVENTS.NO_BID, { ...noBid, src: 'client' }); + events.emit(constants.EVENTS.BID_RESPONSE, { ...bidResponse, adUrl: '...' }); + events.emit(constants.EVENTS.AUCTION_END, { ...auctionEnd, winningBids: [] }); + events.emit(constants.EVENTS.BID_WON, { ...bidWon, statusMessage: 'msg2' }); + + clock.tick(1500) + + const request = JSON.parse(server.requests[3].requestBody) + + expect(request.adUnits[0].bids[0].transactionId).to.equal('123') + expect(request.adUnits[0].bids[0].adUrl).to.equal('...') + expect(request.adUnits[0].bids[0].statusMessage).to.equal('msg2') + expect(request.adUnits[1].bids[0].src).to.equal('client') + }) + + it('should omit fields according to server configuration', () => { + ooloAnalytics.enableAnalytics({ + provider: 'oolo', + options: { + pid: 123 + } + }) + clock.next() + + server.requests[0].respond(200, {}, JSON.stringify({ + 'events': { + 'auctionInit': { + 'sendRaw': 0, + 'omitFields': ['custom_1'] + }, + 'bidResponse': { + 'sendRaw': 0, + 'omitFields': ['custom_2', 'custom_4', 'custom_5'] + } + } + })) + + events.emit(constants.EVENTS.AUCTION_INIT, { ...auctionInit, custom_1: true }); + events.emit(constants.EVENTS.BID_REQUESTED, { ...bidRequested, bids: bidRequested.bids.map(b => { b.custom_2 = true; return b }) }); + events.emit(constants.EVENTS.NO_BID, { ...noBid, custom_3: true }); + events.emit(constants.EVENTS.BID_RESPONSE, { ...bidResponse, custom_4: true }); + events.emit(constants.EVENTS.AUCTION_END, { ...auctionEnd }); + events.emit(constants.EVENTS.BID_WON, { ...bidWon, custom_5: true }); + + clock.tick(1500) + + const request = JSON.parse(server.requests[3].requestBody) + + expect(request.custom_1).to.equal(undefined) + expect(request.custom_6).to.equal(undefined) + expect(request.adUnits[0].bids[0].custom_2).to.equal(undefined) + expect(request.adUnits[0].bids[0].custom_4).to.equal(undefined) + expect(request.adUnits[0].bids[0].custom_5).to.equal(undefined) + expect(request.adUnits[0].bids[0].custom_7).to.equal(undefined) + }) + + it('should omit fields from raw data according to server configuration', () => { + ooloAnalytics.enableAnalytics({ + provider: 'oolo', + options: { + pid: 123 + } + }) + clock.next() + + server.requests[0].respond(200, {}, JSON.stringify({ + 'events': { + 'auctionInit': { + 'sendRaw': 1, + 'omitRawFields': ['custom_1'] + }, + } + })) + + events.emit(constants.EVENTS.AUCTION_INIT, { ...auctionInit, custom_1: true }); + + clock.tick(1500) + + const request = JSON.parse(server.requests[3].requestBody) + + expect(request.eventType).to.equal('auctionInit') + expect(request.custom_1).to.equal(undefined) + }) + + it('should send raw data to custom endpoint if exists in server configuration', () => { + ooloAnalytics.enableAnalytics({ + provider: 'oolo', + options: { + pid: 123 + } + }) + clock.next() + + server.requests[0].respond(200, {}, JSON.stringify({ + 'events': { + 'auctionInit': { + 'sendRaw': 1, + 'endpoint': 'https://pbjs.com' + }, + } + })) + + events.emit(constants.EVENTS.AUCTION_INIT, { ...auctionInit }); + + expect(server.requests[3].url).to.equal('https://pbjs.com') + }) + + it('should send raw events based on server configuration', () => { + ooloAnalytics.enableAnalytics({ + provider: 'oolo', + options: { + pid: 123 + } + }) + clock.next() + + server.requests[0].respond(200, {}, JSON.stringify({ + 'events': { + 'auctionInit': { + 'sendRaw': 0, + }, + 'bidRequested': { + 'sendRaw': 1, + } + } + })) + + events.emit(constants.EVENTS.AUCTION_INIT, auctionInit) + events.emit(constants.EVENTS.BID_REQUESTED, bidRequested); + + const request = JSON.parse(server.requests[3].requestBody) + + expect(request).to.deep.equal({ + eventType: 'bidRequested', + pid: 123, + pvid: PAGEVIEW_ID, + pbModuleVersion: '1.0.0', + ...bidRequested + }) + }) + + it('should queue events and raw events until server configuration resolves', () => { + ooloAnalytics.enableAnalytics({ + provider: 'oolo', + options: { + pid: 123 + } + }) + + simulateAuction() + clock.tick(1500) + + expect(server.requests).to.have.length(3) + + server.requests[0].respond(200, {}, JSON.stringify({ + 'events': { + 'auctionInit': { + 'sendRaw': 1, + }, + 'bidRequested': { + 'sendRaw': 1, + } + } + })) + + expect(server.requests).to.have.length(5) + expect(JSON.parse(server.requests[3].requestBody).eventType).to.equal('auctionInit') + expect(JSON.parse(server.requests[4].requestBody).eventType).to.equal('bidRequested') + }) + }); + + describe('buildAuctionData', () => { + let auction = { + auctionId, + auctionStart, + auctionEnd, + adUnitCodes: ['mid_1'], + auctionStatus: 'running', + bidderRequests: [], + bidsReceived: [], + noBids: [], + winningBids: [], + timestamp: 1234567, + config: {}, + adUnits: { + mid_1: { + adUnitCode: 'mid_1', + code: 'mid_1', + transactionId: '123dsafasdf', + bids: { + [bidId1]: { + adUnitCode: 'mid_1', + cpm: 0.5 + } + } + } + } + } + + it('should turn adUnits and bids objects into arrays', () => { + const auctionData = buildAuctionData(auction, []) + + expect(auctionData.adUnits).to.be.an('array') + expect(auctionData.adUnits[0].bids).to.be.an('array') + }) + + it('should remove fields from the auction', () => { + const auctionData = buildAuctionData(auction, []) + const auctionFields = Object.keys(auctionData) + const adUnitFields = Object.keys(auctionData.adUnits[0]) + + expect(auctionFields).not.to.contain('adUnitCodes') + expect(auctionFields).not.to.contain('auctionStatus') + expect(auctionFields).not.to.contain('bidderRequests') + expect(auctionFields).not.to.contain('bidsReceived') + expect(auctionFields).not.to.contain('noBids') + expect(auctionFields).not.to.contain('winningBids') + expect(auctionFields).not.to.contain('timestamp') + expect(auctionFields).not.to.contain('config') + + expect(adUnitFields).not.to.contain('adUnitCoe') + expect(adUnitFields).not.to.contain('code') + expect(adUnitFields).not.to.contain('transactionId') + }) + }) + + describe('generatePageViewId', () => { + it('should generate a 19 digits number', () => { + expect(generatePageViewId()).length(19) + }) + }) +}); diff --git a/test/spec/modules/openxAnalyticsAdapter_spec.js b/test/spec/modules/openxAnalyticsAdapter_spec.js index 805435abf80..b946efe922d 100644 --- a/test/spec/modules/openxAnalyticsAdapter_spec.js +++ b/test/spec/modules/openxAnalyticsAdapter_spec.js @@ -1,114 +1,192 @@ import { expect } from 'chai'; -import openxAdapter from 'modules/openxAnalyticsAdapter.js'; -import { config } from 'src/config.js'; +import openxAdapter, {AUCTION_STATES} from 'modules/openxAnalyticsAdapter.js'; import events from 'src/events.js'; import CONSTANTS from 'src/constants.json'; import * as utils from 'src/utils.js'; import { server } from 'test/mocks/xhr.js'; +import find from 'core-js-pure/features/array/find.js'; const { - EVENTS: { AUCTION_INIT, BID_REQUESTED, BID_RESPONSE, BID_TIMEOUT, BID_WON } + EVENTS: { AUCTION_INIT, BID_REQUESTED, BID_RESPONSE, BID_TIMEOUT, BID_WON, AUCTION_END } } = CONSTANTS; - const SLOT_LOADED = 'slotOnload'; +const CURRENT_TIME = 1586000000000; describe('openx analytics adapter', function() { - it('should require publisher id', function() { - sinon.spy(utils, 'logError'); + describe('when validating the configuration', function () { + let spy; + beforeEach(function () { + spy = sinon.spy(utils, 'logError'); + }); + + afterEach(function() { + utils.logError.restore(); + }); + + it('should require organization id when no configuration is passed', function() { + openxAdapter.enableAnalytics(); + expect(spy.firstCall.args[0]).to.match(/publisherPlatformId/); + expect(spy.firstCall.args[0]).to.match(/to exist/); + }); - openxAdapter.enableAnalytics(); - expect( - utils.logError.calledWith( - 'OpenX analytics adapter: publisherId is required.' - ) - ).to.be.true; + it('should require publisher id when no orgId is passed', function() { + openxAdapter.enableAnalytics({ + provider: 'openx', + options: { + publisherAccountId: 12345 + } + }); + expect(spy.firstCall.args[0]).to.match(/publisherPlatformId/); + expect(spy.firstCall.args[0]).to.match(/to exist/); + }); - utils.logError.restore(); + it('should validate types', function() { + openxAdapter.enableAnalytics({ + provider: 'openx', + options: { + orgId: 'test platformId', + sampling: 'invalid-float' + } + }); + + expect(spy.firstCall.args[0]).to.match(/sampling/); + expect(spy.firstCall.args[0]).to.match(/type 'number'/); + }); }); - describe('sending analytics event', function() { - const auctionInit = { auctionId: 'add5eb0f-587d-441d-86ec-bbb722c70f79' }; + describe('when tracking analytic events', function () { + const AD_UNIT_CODE = 'test-div-1'; + const SLOT_LOAD_WAIT_TIME = 10; + + const DEFAULT_V2_ANALYTICS_CONFIG = { + orgId: 'test-org-id', + publisherAccountId: 123, + publisherPlatformId: 'test-platform-id', + sample: 1.0, + enableV2: true, + payloadWaitTime: SLOT_LOAD_WAIT_TIME, + payloadWaitTimePadding: SLOT_LOAD_WAIT_TIME + }; + + const auctionInit = { + auctionId: 'test-auction-id', + timestamp: CURRENT_TIME, + timeout: 3000, + adUnitCodes: [AD_UNIT_CODE], + }; const bidRequestedOpenX = { - auctionId: 'add5eb0f-587d-441d-86ec-bbb722c70f79', - auctionStart: 1540944528017, + auctionId: 'test-auction-id', + auctionStart: CURRENT_TIME, + timeout: 2000, bids: [ { - adUnitCode: 'div-1', - bidId: '2f0c647b904e25', + adUnitCode: AD_UNIT_CODE, + bidId: 'test-openx-request-id', bidder: 'openx', - params: { unit: '540249866' }, - transactionId: 'ac66c3e6-3118-4213-a3ae-8cdbe4f72873' + params: { unit: 'test-openx-ad-unit-id' }, + userId: { + tdid: 'test-tradedesk-id', + empty_id: '', + null_id: null, + bla_id: '', + digitrustid: { data: { id: '1' } }, + lipbid: { lipb: '2' } + } } ], - start: 1540944528021 + start: CURRENT_TIME + 10 }; const bidRequestedCloseX = { - auctionId: 'add5eb0f-587d-441d-86ec-bbb722c70f79', - auctionStart: 1540944528017, + auctionId: 'test-auction-id', + auctionStart: CURRENT_TIME, + timeout: 1000, bids: [ { - adUnitCode: 'div-1', - bidId: '43d454020e9409', + adUnitCode: AD_UNIT_CODE, + bidId: 'test-closex-request-id', bidder: 'closex', - params: { unit: '513144370' }, - transactionId: 'ac66c3e6-3118-4213-a3ae-8cdbe4f72873' + params: { unit: 'test-closex-ad-unit-id' }, + userId: { + bla_id: '2', + tdid: 'test-tradedesk-id' + } } ], - start: 1540944528026 + start: CURRENT_TIME + 20 }; const bidResponseOpenX = { - requestId: '2f0c647b904e25', - adId: '33dddbb61d359a', - adUnitCode: 'div-1', - auctionId: 'add5eb0f-587d-441d-86ec-bbb722c70f79', + adUnitCode: AD_UNIT_CODE, cpm: 0.5, + netRevenue: true, + requestId: 'test-openx-request-id', + mediaType: 'banner', + width: 300, + height: 250, + adId: 'test-openx-ad-id', + auctionId: 'test-auction-id', creativeId: 'openx-crid', - responseTimestamp: 1540944528184, - ts: '2DAABBgABAAECAAIBAAsAAgAAAJccGApKSGt6NUZxRXYyHBbinsLj' + currency: 'USD', + timeToRespond: 100, + responseTimestamp: CURRENT_TIME + 30, + ts: 'test-openx-ts' }; const bidResponseCloseX = { - requestId: '43d454020e9409', - adId: '43dddbb61d359a', - adUnitCode: 'div-1', - auctionId: 'add5eb0f-587d-441d-86ec-bbb722c70f79', + adUnitCode: AD_UNIT_CODE, cpm: 0.3, + netRevenue: true, + requestId: 'test-closex-request-id', + mediaType: 'video', + width: 300, + height: 250, + adId: 'test-closex-ad-id', + auctionId: 'test-auction-id', creativeId: 'closex-crid', - responseTimestamp: 1540944528196, - ts: 'hu1QWo6iD3MHs6NG_AQAcFtyNqsj9y4S0YRbX7Kb06IrGns0BABb' + currency: 'USD', + timeToRespond: 200, + dealId: 'test-closex-deal-id', + responseTimestamp: CURRENT_TIME + 40, + ts: 'test-closex-ts' }; const bidTimeoutOpenX = { 0: { - adUnitCode: 'div-1', - auctionId: 'add5eb0f-587d-441d-86ec-bbb722c70f79', - bidId: '2f0c647b904e25' - } - }; + adUnitCode: AD_UNIT_CODE, + auctionId: 'test-auction-id', + bidId: 'test-openx-request-id' + }}; const bidTimeoutCloseX = { 0: { - adUnitCode: 'div-1', - auctionId: 'add5eb0f-587d-441d-86ec-bbb722c70f79', - bidId: '43d454020e9409' + adUnitCode: AD_UNIT_CODE, + auctionId: 'test-auction-id', + bidId: 'test-closex-request-id' } }; const bidWonOpenX = { - requestId: '2f0c647b904e25', - adId: '33dddbb61d359a', - adUnitCode: 'div-1', - auctionId: 'add5eb0f-587d-441d-86ec-bbb722c70f79' + requestId: 'test-openx-request-id', + adId: 'test-openx-ad-id', + adUnitCode: AD_UNIT_CODE, + auctionId: 'test-auction-id' + }; + + const auctionEnd = { + auctionId: 'test-auction-id', + timestamp: CURRENT_TIME, + auctionEnd: CURRENT_TIME + 100, + timeout: 3000, + adUnitCodes: [AD_UNIT_CODE], }; const bidWonCloseX = { - requestId: '43d454020e9409', - adId: '43dddbb61d359a', - adUnitCode: 'div-1', - auctionId: 'add5eb0f-587d-441d-86ec-bbb722c70f79' + requestId: 'test-closex-request-id', + adId: 'test-closex-ad-id', + adUnitCode: AD_UNIT_CODE, + auctionId: 'test-auction-id' }; function simulateAuction(events) { @@ -116,328 +194,446 @@ describe('openx analytics adapter', function() { events.forEach(event => { const [eventType, args] = event; - openxAdapter.track({ eventType, args }); if (eventType === BID_RESPONSE) { highestBid = highestBid || args; if (highestBid.cpm < args.cpm) { highestBid = args; } } - }); - openxAdapter.track({ - eventType: SLOT_LOADED, - args: { - slot: { - getAdUnitPath: () => { - return '/90577858/test_ad_unit'; - }, - getTargetingKeys: () => { - return []; - }, - getTargeting: sinon - .stub() - .withArgs('hb_adid') - .returns(highestBid ? [highestBid.adId] : []) - } + if (eventType === SLOT_LOADED) { + const slotLoaded = { + slot: { + getAdUnitPath: () => { + return '/12345678/test_ad_unit'; + }, + getSlotElementId: () => { + return AD_UNIT_CODE; + }, + getTargeting: (key) => { + if (key === 'hb_adid') { + return highestBid ? [highestBid.adId] : []; + } else { + return []; + } + } + } + }; + openxAdapter.track({ eventType, args: slotLoaded }); + } else { + openxAdapter.track({ eventType, args }); } }); } - function getQueryData(url) { - const queryArgs = url.split('?')[1].split('&'); - return queryArgs.reduce((data, arg) => { - const [key, val] = arg.split('='); - data[key] = val; - return data; - }, {}); - } + let clock; - before(function() { + beforeEach(function() { sinon.stub(events, 'getEvents').returns([]); - openxAdapter.enableAnalytics({ - options: { - publisherId: 'test123' - } - }); + clock = sinon.useFakeTimers(CURRENT_TIME); }); - after(function() { + afterEach(function() { events.getEvents.restore(); - openxAdapter.disableAnalytics(); + clock.restore(); }); - beforeEach(function() { - openxAdapter.reset(); - }); + describe('when there is an auction', function () { + let auction; + let auction2; + beforeEach(function () { + openxAdapter.enableAnalytics({options: DEFAULT_V2_ANALYTICS_CONFIG}); - afterEach(function() {}); + simulateAuction([ + [AUCTION_INIT, auctionInit], + [SLOT_LOADED] + ]); - it('should not send request if no bid response', function() { - simulateAuction([ - [AUCTION_INIT, auctionInit], - [BID_REQUESTED, bidRequestedOpenX] - ]); + simulateAuction([ + [AUCTION_INIT, {...auctionInit, auctionId: 'second-auction-id'}], + [SLOT_LOADED] + ]); - expect(server.requests.length).to.equal(0); - }); + clock.tick(SLOT_LOAD_WAIT_TIME); + auction = JSON.parse(server.requests[0].requestBody)[0]; + auction2 = JSON.parse(server.requests[1].requestBody)[0]; + }); + + afterEach(function () { + openxAdapter.reset(); + openxAdapter.disableAnalytics(); + }); + + it('should track auction start time', function () { + expect(auction.startTime).to.equal(auctionInit.timestamp); + }); + + it('should track auction time limit', function () { + expect(auction.timeLimit).to.equal(auctionInit.timeout); + }); + + it('should track the \'default\' test code', function () { + expect(auction.testCode).to.equal('default'); + }); - it('should send 1 request to the right endpoint', function() { - simulateAuction([ - [AUCTION_INIT, auctionInit], - [BID_REQUESTED, bidRequestedOpenX], - [BID_RESPONSE, bidResponseOpenX] - ]); + it('should track auction count', function () { + expect(auction.auctionOrder).to.equal(1); + expect(auction2.auctionOrder).to.equal(2); + }); - expect(server.requests.length).to.equal(1); + it('should track the orgId', function () { + expect(auction.orgId).to.equal(DEFAULT_V2_ANALYTICS_CONFIG.orgId); + }); - const endpoint = server.requests[0].url.split('?')[0]; - // note IE11 returns the default secure port, so we look for this alternate value as well in these tests - expect(endpoint).to.be.oneOf(['https://ads.openx.net/w/1.0/pban', 'https://ads.openx.net:443/w/1.0/pban']); + it('should track the orgId', function () { + expect(auction.publisherPlatformId).to.equal(DEFAULT_V2_ANALYTICS_CONFIG.publisherPlatformId); + }); + + it('should track the orgId', function () { + expect(auction.publisherAccountId).to.equal(DEFAULT_V2_ANALYTICS_CONFIG.publisherAccountId); + }); }); - describe('hb.ct, hb.rid, dddid, hb.asiid, hb.pubid', function() { - it('should always be in the query string', function() { + describe('when there is a custom test code', function () { + let auction; + beforeEach(function () { + openxAdapter.enableAnalytics({ + options: { + ...DEFAULT_V2_ANALYTICS_CONFIG, + testCode: 'test-code' + } + }); + simulateAuction([ [AUCTION_INIT, auctionInit], - [BID_REQUESTED, bidRequestedOpenX], - [BID_RESPONSE, bidResponseOpenX] + [SLOT_LOADED], ]); + clock.tick(SLOT_LOAD_WAIT_TIME); + auction = JSON.parse(server.requests[0].requestBody)[0]; + }); - const queryData = getQueryData(server.requests[0].url); - expect(queryData).to.include({ - 'hb.ct': String(bidRequestedOpenX.auctionStart), - 'hb.rid': auctionInit.auctionId, - dddid: bidRequestedOpenX.bids[0].transactionId, - 'hb.asiid': '/90577858/test_ad_unit', - 'hb.pubid': 'test123' - }); + afterEach(function () { + openxAdapter.reset(); + openxAdapter.disableAnalytics(); + }); + + it('should track the custom test code', function () { + expect(auction.testCode).to.equal('test-code'); }); }); - describe('hb.cur', function() { - it('should be in the query string if currency is set', function() { - sinon - .stub(config, 'getConfig') - .withArgs('currency.adServerCurrency') - .returns('bitcoin'); + describe('when there is campaign (utm) data', function () { + let auction; + beforeEach(function () { + + }); + + afterEach(function () { + openxAdapter.reset(); + utils.getWindowLocation.restore(); + openxAdapter.disableAnalytics(); + }); + + it('should track values from query params when they exist', function () { + sinon.stub(utils, 'getWindowLocation').returns({search: '?' + + 'utm_campaign=test%20campaign-name&' + + 'utm_source=test-source&' + + 'utm_medium=test-medium&' + }); + + openxAdapter.enableAnalytics({options: DEFAULT_V2_ANALYTICS_CONFIG}); simulateAuction([ [AUCTION_INIT, auctionInit], - [BID_REQUESTED, bidRequestedOpenX], - [BID_RESPONSE, bidResponseOpenX] + [SLOT_LOADED], ]); + clock.tick(SLOT_LOAD_WAIT_TIME); + auction = JSON.parse(server.requests[0].requestBody)[0]; + + // ensure that value are URI decoded + expect(auction.campaign.name).to.equal('test campaign-name'); + expect(auction.campaign.source).to.equal('test-source'); + expect(auction.campaign.medium).to.equal('test-medium'); + expect(auction.campaign.content).to.be.undefined; + expect(auction.campaign.term).to.be.undefined; + }); - config.getConfig.restore(); + it('should override query params if configuration parameters exist', function () { + sinon.stub(utils, 'getWindowLocation').returns({search: '?' + + 'utm_campaign=test-campaign-name&' + + 'utm_source=test-source&' + + 'utm_medium=test-medium&' + + 'utm_content=test-content&' + + 'utm_term=test-term' + }); - const queryData = getQueryData(server.requests[0].url); - expect(queryData).to.include({ - 'hb.cur': 'bitcoin' + openxAdapter.enableAnalytics({ + options: { + ...DEFAULT_V2_ANALYTICS_CONFIG, + campaign: { + name: 'test-config-name', + source: 'test-config-source', + medium: 'test-config-medium' + } + } }); - }); - it('should not be in the query string if currency is not set', function() { simulateAuction([ [AUCTION_INIT, auctionInit], - [BID_REQUESTED, bidRequestedOpenX], - [BID_RESPONSE, bidResponseOpenX] + [SLOT_LOADED], ]); - - const queryData = getQueryData(server.requests[0].url); - expect(queryData).to.not.have.key('hb.cur'); + clock.tick(SLOT_LOAD_WAIT_TIME); + auction = JSON.parse(server.requests[0].requestBody)[0]; + + expect(auction.campaign.name).to.equal('test-config-name'); + expect(auction.campaign.source).to.equal('test-config-source'); + expect(auction.campaign.medium).to.equal('test-config-medium'); + expect(auction.campaign.content).to.equal('test-content'); + expect(auction.campaign.term).to.equal('test-term'); }); }); - describe('hb.dcl, hb.dl, hb.tta, hb.ttr', function() { - it('should be in the query string if browser supports performance API', function() { - const timing = { - fetchStart: 1540944528000, - domContentLoadedEventEnd: 1540944528010, - loadEventEnd: 1540944528110 - }; - const originalPerf = window.top.performance; - window.top.performance = { timing }; + describe('when there are bid requests', function () { + let auction; + let openxBidder; + let closexBidder; - const renderTime = 1540944528100; - sinon.stub(Date, 'now').returns(renderTime); + beforeEach(function () { + openxAdapter.enableAnalytics({options: DEFAULT_V2_ANALYTICS_CONFIG}); simulateAuction([ [AUCTION_INIT, auctionInit], + [BID_REQUESTED, bidRequestedCloseX], [BID_REQUESTED, bidRequestedOpenX], - [BID_RESPONSE, bidResponseOpenX] + [SLOT_LOADED], ]); + clock.tick(SLOT_LOAD_WAIT_TIME * 2); + auction = JSON.parse(server.requests[0].requestBody)[0]; + openxBidder = find(auction.adUnits[0].bidRequests, bidderRequest => bidderRequest.bidder === 'openx'); + closexBidder = find(auction.adUnits[0].bidRequests, bidderRequest => bidderRequest.bidder === 'closex'); + }); - window.top.performance = originalPerf; - Date.now.restore(); + afterEach(function () { + openxAdapter.reset(); + openxAdapter.disableAnalytics(); + }); - const queryData = getQueryData(server.requests[0].url); - expect(queryData).to.include({ - 'hb.dcl': String(timing.domContentLoadedEventEnd - timing.fetchStart), - 'hb.dl': String(timing.loadEventEnd - timing.fetchStart), - 'hb.tta': String(bidRequestedOpenX.auctionStart - timing.fetchStart), - 'hb.ttr': String(renderTime - timing.fetchStart) - }); + it('should track the bidder', function () { + expect(openxBidder.bidder).to.equal('openx'); + expect(closexBidder.bidder).to.equal('closex'); + }); + + it('should track the adunit code', function () { + expect(auction.adUnits[0].code).to.equal(AD_UNIT_CODE); + }); + + it('should track the user ids', function () { + expect(auction.userIdProviders).to.deep.equal(['bla_id', 'digitrustid', 'lipbid', 'tdid']); }); - it('should not be in the query string if browser does not support performance API', function() { - const originalPerf = window.top.performance; - window.top.performance = undefined; + it('should not have responded', function () { + expect(openxBidder.hasBidderResponded).to.equal(false); + expect(closexBidder.hasBidderResponded).to.equal(false); + }); + }); + + describe('when there are request timeouts', function () { + let auction; + let openxBidRequest; + let closexBidRequest; + + beforeEach(function () { + openxAdapter.enableAnalytics({options: DEFAULT_V2_ANALYTICS_CONFIG}); simulateAuction([ [AUCTION_INIT, auctionInit], + [BID_REQUESTED, bidRequestedCloseX], [BID_REQUESTED, bidRequestedOpenX], - [BID_RESPONSE, bidResponseOpenX] + [BID_TIMEOUT, bidTimeoutCloseX], + [BID_TIMEOUT, bidTimeoutOpenX], + [AUCTION_END, auctionEnd] ]); + clock.tick(SLOT_LOAD_WAIT_TIME * 2); + auction = JSON.parse(server.requests[0].requestBody)[0]; - window.top.performance = originalPerf; + openxBidRequest = find(auction.adUnits[0].bidRequests, bidderRequest => bidderRequest.bidder === 'openx'); + closexBidRequest = find(auction.adUnits[0].bidRequests, bidderRequest => bidderRequest.bidder === 'closex'); + }); + + afterEach(function () { + openxAdapter.reset(); + openxAdapter.disableAnalytics(); + }); + + it('should track the timeout', function () { + expect(openxBidRequest.timedOut).to.equal(true); + expect(closexBidRequest.timedOut).to.equal(true); + }); - const queryData = getQueryData(server.requests[0].url); - expect(queryData).to.not.have.keys( - 'hb.dcl', - 'hb.dl', - 'hb.tta', - 'hb.ttr' - ); + it('should track the timeout value ie timeLimit', function () { + expect(openxBidRequest.timeLimit).to.equal(2000); + expect(closexBidRequest.timeLimit).to.equal(1000); }); }); - describe('ts, auid', function() { - it('OpenX is in auction and has a bid response', function() { + describe('when there are bid responses', function () { + let auction; + let openxBidResponse; + let closexBidResponse; + + beforeEach(function () { + openxAdapter.enableAnalytics({options: DEFAULT_V2_ANALYTICS_CONFIG}); + simulateAuction([ [AUCTION_INIT, auctionInit], - [BID_REQUESTED, bidRequestedOpenX], [BID_REQUESTED, bidRequestedCloseX], + [BID_REQUESTED, bidRequestedOpenX], [BID_RESPONSE, bidResponseOpenX], - [BID_RESPONSE, bidResponseCloseX] + [BID_RESPONSE, bidResponseCloseX], + [AUCTION_END, auctionEnd] ]); - const queryData = getQueryData(server.requests[0].url); - expect(queryData).to.include({ - ts: bidResponseOpenX.ts, - auid: bidRequestedOpenX.bids[0].params.unit - }); + clock.tick(SLOT_LOAD_WAIT_TIME * 2); + auction = JSON.parse(server.requests[0].requestBody)[0]; + + openxBidResponse = find(auction.adUnits[0].bidRequests, bidderRequest => bidderRequest.bidder === 'openx').bidResponses[0]; + closexBidResponse = find(auction.adUnits[0].bidRequests, bidderRequest => bidderRequest.bidder === 'closex').bidResponses[0]; }); - it('OpenX is in auction but no bid response', function() { - simulateAuction([ - [AUCTION_INIT, auctionInit], - [BID_REQUESTED, bidRequestedOpenX], - [BID_REQUESTED, bidRequestedCloseX], - [BID_RESPONSE, bidResponseCloseX] - ]); + afterEach(function () { + openxAdapter.reset(); + openxAdapter.disableAnalytics(); + }); - const queryData = getQueryData(server.requests[0].url); - expect(queryData).to.include({ - auid: bidRequestedOpenX.bids[0].params.unit - }); - expect(queryData).to.not.have.key('ts'); + it('should track the cpm in microCPM', function () { + expect(openxBidResponse.microCpm).to.equal(bidResponseOpenX.cpm * 1000000); + expect(closexBidResponse.microCpm).to.equal(bidResponseCloseX.cpm * 1000000); }); - it('OpenX is not in auction', function() { - simulateAuction([ - [AUCTION_INIT, auctionInit], - [BID_REQUESTED, bidRequestedCloseX], - [BID_RESPONSE, bidResponseCloseX] - ]); + it('should track if the bid is in net revenue', function () { + expect(openxBidResponse.netRevenue).to.equal(bidResponseOpenX.netRevenue); + expect(closexBidResponse.netRevenue).to.equal(bidResponseCloseX.netRevenue); + }); + + it('should track the mediaType', function () { + expect(openxBidResponse.mediaType).to.equal(bidResponseOpenX.mediaType); + expect(closexBidResponse.mediaType).to.equal(bidResponseCloseX.mediaType); + }); + + it('should track the currency', function () { + expect(openxBidResponse.currency).to.equal(bidResponseOpenX.currency); + expect(closexBidResponse.currency).to.equal(bidResponseCloseX.currency); + }); + + it('should track the ad width and height', function () { + expect(openxBidResponse.width).to.equal(bidResponseOpenX.width); + expect(openxBidResponse.height).to.equal(bidResponseOpenX.height); + + expect(closexBidResponse.width).to.equal(bidResponseCloseX.width); + expect(closexBidResponse.height).to.equal(bidResponseCloseX.height); + }); + + it('should track the bid dealId', function () { + expect(openxBidResponse.dealId).to.equal(bidResponseOpenX.dealId); // no deal id defined + expect(closexBidResponse.dealId).to.equal(bidResponseCloseX.dealId); // deal id defined + }); + + it('should track the bid\'s latency', function () { + expect(openxBidResponse.latency).to.equal(bidResponseOpenX.timeToRespond); + expect(closexBidResponse.latency).to.equal(bidResponseCloseX.timeToRespond); + }); + + it('should not have any bid winners', function () { + expect(openxBidResponse.winner).to.equal(false); + expect(closexBidResponse.winner).to.equal(false); + }); + + it('should track the bid currency', function () { + expect(openxBidResponse.currency).to.equal(bidResponseOpenX.currency); + expect(closexBidResponse.currency).to.equal(bidResponseCloseX.currency); + }); + + it('should track the auction end time', function () { + expect(auction.endTime).to.equal(auctionEnd.auctionEnd); + }); - const queryData = getQueryData(server.requests[0].url); - expect(queryData).to.not.have.keys('auid', 'ts'); + it('should track that the auction ended', function () { + expect(auction.state).to.equal(AUCTION_STATES.ENDED); }); }); - describe('hb.exn, hb.sts, hb.ets, hb.bv, hb.crid, hb.to', function() { - it('2 bidders in auction', function() { + describe('when there are bidder wins', function () { + let auction; + beforeEach(function () { + openxAdapter.enableAnalytics({options: DEFAULT_V2_ANALYTICS_CONFIG}); + simulateAuction([ [AUCTION_INIT, auctionInit], [BID_REQUESTED, bidRequestedOpenX], [BID_REQUESTED, bidRequestedCloseX], [BID_RESPONSE, bidResponseOpenX], - [BID_RESPONSE, bidResponseCloseX] + [BID_RESPONSE, bidResponseCloseX], + [AUCTION_END, auctionEnd], + [BID_WON, bidWonOpenX] ]); - const queryData = getQueryData(server.requests[0].url); - const auctionStart = bidRequestedOpenX.auctionStart; - expect(queryData).to.include({ - 'hb.exn': [ - bidRequestedOpenX.bids[0].bidder, - bidRequestedCloseX.bids[0].bidder - ].join(','), - 'hb.sts': [ - bidRequestedOpenX.start - auctionStart, - bidRequestedCloseX.start - auctionStart - ].join(','), - 'hb.ets': [ - bidResponseOpenX.responseTimestamp - auctionStart, - bidResponseCloseX.responseTimestamp - auctionStart - ].join(','), - 'hb.bv': [bidResponseOpenX.cpm, bidResponseCloseX.cpm].join(','), - 'hb.crid': [ - bidResponseOpenX.creativeId, - bidResponseCloseX.creativeId - ].join(','), - 'hb.to': [false, false].join(',') - }); + clock.tick(SLOT_LOAD_WAIT_TIME * 2); + auction = JSON.parse(server.requests[0].requestBody)[0]; }); - it('OpenX timed out', function() { - simulateAuction([ - [AUCTION_INIT, auctionInit], - [BID_REQUESTED, bidRequestedOpenX], - [BID_REQUESTED, bidRequestedCloseX], - [BID_RESPONSE, bidResponseCloseX], - [BID_TIMEOUT, bidTimeoutOpenX] - ]); + afterEach(function () { + openxAdapter.reset(); + openxAdapter.disableAnalytics(); + }); - const queryData = getQueryData(server.requests[0].url); - const auctionStart = bidRequestedOpenX.auctionStart; - expect(queryData).to.include({ - 'hb.exn': [ - bidRequestedOpenX.bids[0].bidder, - bidRequestedCloseX.bids[0].bidder - ].join(','), - 'hb.sts': [ - bidRequestedOpenX.start - auctionStart, - bidRequestedCloseX.start - auctionStart - ].join(','), - 'hb.ets': [ - undefined, - bidResponseCloseX.responseTimestamp - auctionStart - ].join(','), - 'hb.bv': [0, bidResponseCloseX.cpm].join(','), - 'hb.crid': [undefined, bidResponseCloseX.creativeId].join(','), - 'hb.to': [true, false].join(',') - }); + it('should track that bidder as the winner', function () { + let openxBidder = find(auction.adUnits[0].bidRequests, bidderRequest => bidderRequest.bidder === 'openx'); + expect(openxBidder.bidResponses[0]).to.contain({winner: true}); + }); + + it('should track that bidder as the losers', function () { + let closexBidder = find(auction.adUnits[0].bidRequests, bidderRequest => bidderRequest.bidder === 'closex'); + expect(closexBidder.bidResponses[0]).to.contain({winner: false}); }); }); - describe('hb.we, hb.g1', function() { - it('OpenX won', function() { + describe('when a winning bid renders', function () { + let auction; + beforeEach(function () { + openxAdapter.enableAnalytics({options: DEFAULT_V2_ANALYTICS_CONFIG}); + simulateAuction([ [AUCTION_INIT, auctionInit], [BID_REQUESTED, bidRequestedOpenX], + [BID_REQUESTED, bidRequestedCloseX], [BID_RESPONSE, bidResponseOpenX], - [BID_WON, bidWonOpenX] + [BID_RESPONSE, bidResponseCloseX], + [AUCTION_END, auctionEnd], + [BID_WON, bidWonOpenX], + [SLOT_LOADED] ]); - const queryData = getQueryData(server.requests[0].url); - expect(queryData).to.include({ - 'hb.we': '0', - 'hb.g1': 'false' - }); + clock.tick(SLOT_LOAD_WAIT_TIME * 2); + auction = JSON.parse(server.requests[0].requestBody)[0]; }); - it('DFP won', function() { - simulateAuction([ - [AUCTION_INIT, auctionInit], - [BID_REQUESTED, bidRequestedOpenX], - [BID_RESPONSE, bidResponseOpenX] - ]); + afterEach(function () { + openxAdapter.reset(); + openxAdapter.disableAnalytics(); + }); - const queryData = getQueryData(server.requests[0].url); - expect(queryData).to.include({ - 'hb.we': '-1', - 'hb.g1': 'true' - }); + it('should track that winning bid rendered', function () { + let openxBidder = find(auction.adUnits[0].bidRequests, bidderRequest => bidderRequest.bidder === 'openx'); + expect(openxBidder.bidResponses[0]).to.contain({rendered: true}); + }); + + it('should track that winning bid render time', function () { + let openxBidder = find(auction.adUnits[0].bidRequests, bidderRequest => bidderRequest.bidder === 'openx'); + expect(openxBidder.bidResponses[0]).to.contain({renderTime: CURRENT_TIME}); + }); + + it('should track that the auction completed', function () { + expect(auction.state).to.equal(AUCTION_STATES.COMPLETED); }); }); }); diff --git a/test/spec/modules/openxBidAdapter_spec.js b/test/spec/modules/openxBidAdapter_spec.js index 0808d78c0aa..7391c8826bc 100644 --- a/test/spec/modules/openxBidAdapter_spec.js +++ b/test/spec/modules/openxBidAdapter_spec.js @@ -1,6 +1,7 @@ import {expect} from 'chai'; import {spec, USER_ID_CODE_TO_QUERY_ARG} from 'modules/openxBidAdapter.js'; import {newBidder} from 'src/adapters/bidderFactory.js'; +import {BANNER, VIDEO} from 'src/mediaTypes.js'; import {userSync} from 'src/userSync.js'; import {config} from 'src/config.js'; import * as utils from 'src/utils.js'; @@ -1042,14 +1043,24 @@ describe('OpenxAdapter', function () { const EXAMPLE_DATA_BY_ATTR = { britepoolid: '1111-britepoolid', criteoId: '1111-criteoId', - digitrustid: {data: {id: 'DTID', keyv: 4, privacy: {optout: false}, producer: 'ABC', version: 2}}, - id5id: '1111-id5id', + fabrickId: '1111-fabrickid', + haloId: '1111-haloid', + id5id: {uid: '1111-id5id'}, idl_env: '1111-idl_env', + IDP: '1111-zeotap-idplusid', + idxId: '1111-idxid', + intentIqId: '1111-intentiqid', lipb: {lipbid: '1111-lipb'}, + lotamePanoramaId: '1111-lotameid', + merkleId: '1111-merkleid', netId: 'fH5A3n2O8_CZZyPoJVD-eabc6ECb7jhxCicsds7qSg', parrableId: { eid: 'eidVersion.encryptionKeyReference.encryptedValue' }, pubcid: '1111-pubcid', + quantcastId: '1111-quantcastid', + sharedId: '1111-sharedid', + tapadId: '111-tapadid', tdid: '1111-tdid', + verizonMediaId: '1111-verizonmediaid', }; // generates the same set of tests for each id provider @@ -1087,15 +1098,15 @@ describe('OpenxAdapter', function () { let userIdValue; // handle cases where userId key refers to an object switch (userIdProviderKey) { - case 'digitrustid': - userIdValue = EXAMPLE_DATA_BY_ATTR.digitrustid.data.id; - break; case 'lipb': userIdValue = EXAMPLE_DATA_BY_ATTR.lipb.lipbid; break; case 'parrableId': userIdValue = EXAMPLE_DATA_BY_ATTR.parrableId.eid; break; + case 'id5id': + userIdValue = EXAMPLE_DATA_BY_ATTR.id5id.uid; + break; default: userIdValue = EXAMPLE_DATA_BY_ATTR[userIdProviderKey]; } @@ -1184,6 +1195,7 @@ describe('OpenxAdapter', function () { let getFloorSpy = sinon.spy(bidRequest1, 'getFloor'); spec.buildRequests([bidRequest1], mockBidderRequest); + expect(getFloorSpy.args[0][0].mediaType).to.equal(BANNER); expect(getFloorSpy.args[0][0].currency).to.equal('USD'); }); @@ -1202,6 +1214,7 @@ describe('OpenxAdapter', function () { let getFloorSpy = sinon.spy(bidRequest1, 'getFloor'); spec.buildRequests([bidRequest1], mockBidderRequest); + expect(getFloorSpy.args[0][0].mediaType).to.equal(BANNER); expect(getFloorSpy.args[0][0].currency).to.equal('bitcoin'); }); }) @@ -1342,6 +1355,87 @@ describe('OpenxAdapter', function () { }); }); }); + + describe('floors', function () { + it('should send out custom floors on bids that have customFloors specified', function () { + const bidRequest = Object.assign({}, + bidRequestsWithMediaTypes[0], + { + params: { + 'unit': '12345678', + 'delDomain': 'test-del-domain', + 'customFloor': 1.500001 + } + } + ); + + const request = spec.buildRequests([bidRequest], mockBidderRequest); + const dataParams = request[0].data; + + expect(dataParams.aumfs).to.exist; + expect(dataParams.aumfs).to.equal('1500'); + }); + + context('with floors module', function () { + let adServerCurrencyStub; + function makeBidWithFloorInfo(floorInfo) { + return Object.assign(utils.deepClone(bidRequestsWithMediaTypes[0]), + { + getFloor: () => { + return floorInfo; + } + }); + } + + beforeEach(function () { + adServerCurrencyStub = sinon + .stub(config, 'getConfig') + .withArgs('currency.adServerCurrency') + }); + + afterEach(function () { + config.getConfig.restore(); + }); + + it('should send out floors on bids', function () { + const floors = [9.99, 18.881]; + const bidRequests = floors.map(floor => { + return makeBidWithFloorInfo({ + currency: 'AUS', + floor: floor + }); + }); + const request = spec.buildRequests(bidRequests, mockBidderRequest); + + expect(request[0].data.aumfs).to.exist; + expect(request[0].data.aumfs).to.equal('9990'); + expect(request[1].data.aumfs).to.exist; + expect(request[1].data.aumfs).to.equal('18881'); + }); + + it('should send out floors on bids in the default currency', function () { + const bidRequest1 = makeBidWithFloorInfo({}); + + let getFloorSpy = sinon.spy(bidRequest1, 'getFloor'); + + spec.buildRequests([bidRequest1], mockBidderRequest); + expect(getFloorSpy.args[0][0].mediaType).to.equal(VIDEO); + expect(getFloorSpy.args[0][0].currency).to.equal('USD'); + }); + + it('should send out floors on bids in the ad server currency if defined', function () { + adServerCurrencyStub.returns('bitcoin'); + + const bidRequest1 = makeBidWithFloorInfo({}); + + let getFloorSpy = sinon.spy(bidRequest1, 'getFloor'); + + spec.buildRequests([bidRequest1], mockBidderRequest); + expect(getFloorSpy.args[0][0].mediaType).to.equal(VIDEO); + expect(getFloorSpy.args[0][0].currency).to.equal('bitcoin'); + }); + }) + }) }); describe('buildRequest for multi-format ad', function () { @@ -1476,7 +1570,11 @@ describe('OpenxAdapter', function () { expect(bid.meta.brandId).to.equal(DEFAULT_TEST_ARJ_AD_UNIT.brand_id); }); - it('should return a brand ID', function () { + it('should return an adomain', function () { + expect(bid.meta.advertiserDomains).to.deep.equal([]); + }); + + it('should return a dsp ID', function () { expect(bid.meta.dspid).to.equal(DEFAULT_TEST_ARJ_AD_UNIT.adv_id); }); }); @@ -1756,6 +1854,13 @@ describe('OpenxAdapter', function () { expect(JSON.stringify(Object.keys(result[0]).sort())).to.eql(JSON.stringify(Object.keys(expectedResponse[0]).sort())); }); + it('should return correct bid response with MediaType and deal_id', function () { + const bidResponseOverride = { 'deal_id': 'OX-mydeal' }; + const bidResponseWithDealId = Object.assign({}, bidResponse, bidResponseOverride); + const result = spec.interpretResponse({body: bidResponseWithDealId}, bidRequestsWithMediaType); + expect(result[0].dealId).to.equal(bidResponseOverride.deal_id); + }); + it('should handle nobid responses for bidRequests with MediaTypes', function () { const bidResponse = {'vastUrl': '', 'pub_rev': '', 'width': '', 'height': '', 'adid': '', 'pixels': ''}; const result = spec.interpretResponse({body: bidResponse}, bidRequestsWithMediaTypes); diff --git a/test/spec/modules/optimonAnalyticsAdapter_spec.js b/test/spec/modules/optimonAnalyticsAdapter_spec.js new file mode 100644 index 00000000000..b5b76ce3fde --- /dev/null +++ b/test/spec/modules/optimonAnalyticsAdapter_spec.js @@ -0,0 +1,40 @@ +import * as utils from 'src/utils.js'; +import { expect } from 'chai'; +import optimonAnalyticsAdapter from '../../../modules/optimonAnalyticsAdapter.js'; +import adapterManager from 'src/adapterManager'; +import events from 'src/events'; +import constants from 'src/constants.json' + +const AD_UNIT_CODE = 'demo-adunit-1'; +const PUBLISHER_CONFIG = { + pubId: 'optimon_test', + pubAdxAccount: 123456789, + pubTimezone: 'Asia/Jerusalem' +}; + +describe('Optimon Analytics Adapter', () => { + const optmn_currentWindow = utils.getWindowSelf(); + let optmn_queue = []; + + beforeEach(() => { + optmn_currentWindow.OptimonAnalyticsAdapter = (...optmn_args) => optmn_queue.push(optmn_args); + adapterManager.enableAnalytics({ + provider: 'optimon' + }); + optmn_queue = [] + }); + + afterEach(() => { + optimonAnalyticsAdapter.disableAnalytics(); + }); + + it('should forward all events to the queue', () => { + const optmn_arguments = [AD_UNIT_CODE, PUBLISHER_CONFIG]; + + events.emit(constants.EVENTS.AUCTION_END, optmn_arguments) + events.emit(constants.EVENTS.BID_TIMEOUT, optmn_arguments) + events.emit(constants.EVENTS.BID_WON, optmn_arguments) + + expect(optmn_queue.length).to.eql(3); + }); +}); diff --git a/test/spec/modules/outbrainBidAdapter_spec.js b/test/spec/modules/outbrainBidAdapter_spec.js new file mode 100644 index 00000000000..4bdd657f419 --- /dev/null +++ b/test/spec/modules/outbrainBidAdapter_spec.js @@ -0,0 +1,548 @@ +import {expect} from 'chai'; +import {spec} from 'modules/outbrainBidAdapter.js'; +import {config} from 'src/config.js'; +import {server} from 'test/mocks/xhr'; + +describe('Outbrain Adapter', function () { + describe('Bid request and response', function () { + const commonBidRequest = { + bidder: 'outbrain', + params: { + publisher: { + id: 'publisher-id' + }, + }, + bidId: '2d6815a92ba1ba', + auctionId: '12043683-3254-4f74-8934-f941b085579e', + } + const nativeBidRequestParams = { + nativeParams: { + image: { + required: true, + sizes: [ + 120, + 100 + ], + sendId: true + }, + title: { + required: true, + sendId: true + }, + sponsoredBy: { + required: false + } + }, + } + + const displayBidRequestParams = { + sizes: [ + [ + 300, + 250 + ] + ] + } + + describe('isBidRequestValid', function () { + before(() => { + config.setConfig({ + outbrain: { + bidderUrl: 'https://bidder-url.com', + } + } + ) + }) + after(() => { + config.resetConfig() + }) + + it('should fail when bid is invalid', function () { + const bid = { + bidder: 'outbrain', + params: { + publisher: { + id: 'publisher-id', + } + }, + } + expect(spec.isBidRequestValid(bid)).to.equal(false) + }) + it('should succeed when bid contains native params', function () { + const bid = { + bidder: 'outbrain', + params: { + publisher: { + id: 'publisher-id', + } + }, + ...nativeBidRequestParams, + } + expect(spec.isBidRequestValid(bid)).to.equal(true) + }) + it('should succeed when bid contains sizes', function () { + const bid = { + bidder: 'outbrain', + params: { + publisher: { + id: 'publisher-id', + } + }, + ...displayBidRequestParams, + } + expect(spec.isBidRequestValid(bid)).to.equal(true) + }) + it('should fail if publisher id is not set', function () { + const bid = { + bidder: 'outbrain', + ...nativeBidRequestParams, + } + expect(spec.isBidRequestValid(bid)).to.equal(false) + }) + it('should succeed with outbrain config', function () { + const bid = { + bidder: 'outbrain', + params: { + publisher: { + id: 'publisher-id', + } + }, + ...nativeBidRequestParams, + } + config.resetConfig() + config.setConfig({ + outbrain: { + bidderUrl: 'https://bidder-url.com', + } + }) + expect(spec.isBidRequestValid(bid)).to.equal(true) + }) + it('should fail if bidder url is not set', function () { + const bid = { + bidder: 'outbrain', + params: { + publisher: { + id: 'publisher-id', + } + }, + ...nativeBidRequestParams, + } + config.resetConfig() + expect(spec.isBidRequestValid(bid)).to.equal(false) + }) + }) + + describe('buildRequests', function () { + before(() => { + config.setConfig({ + outbrain: { + bidderUrl: 'https://bidder-url.com', + } + } + ) + }) + after(() => { + config.resetConfig() + }) + + const commonBidderRequest = { + refererInfo: { + referer: 'https://example.com/' + } + } + + it('should build native request', function () { + const bidRequest = { + ...commonBidRequest, + ...nativeBidRequestParams, + } + const expectedNativeAssets = { + assets: [ + { + required: 1, + id: 3, + img: { + type: 3, + w: 120, + h: 100 + } + }, + { + required: 1, + id: 0, + title: {} + }, + { + required: 0, + id: 5, + data: { + type: 1 + } + } + ] + } + const expectedData = { + site: { + page: 'https://example.com/', + publisher: { + id: 'publisher-id' + } + }, + device: { + ua: navigator.userAgent + }, + source: { + fd: 1 + }, + cur: [ + 'USD' + ], + imp: [ + { + id: '1', + native: { + request: JSON.stringify(expectedNativeAssets) + } + } + ] + } + const res = spec.buildRequests([bidRequest], commonBidderRequest) + expect(res.url).to.equal('https://bidder-url.com') + expect(res.data).to.deep.equal(JSON.stringify(expectedData)) + }); + + it('should build display request', function () { + const bidRequest = { + ...commonBidRequest, + ...displayBidRequestParams, + } + const expectedData = { + site: { + page: 'https://example.com/', + publisher: { + id: 'publisher-id' + } + }, + device: { + ua: navigator.userAgent + }, + source: { + fd: 1 + }, + cur: [ + 'USD' + ], + imp: [ + { + id: '1', + banner: { + format: [ + { + w: 300, + h: 250 + } + ] + } + } + ] + } + const res = spec.buildRequests([bidRequest], commonBidderRequest) + expect(res.url).to.equal('https://bidder-url.com') + expect(res.data).to.deep.equal(JSON.stringify(expectedData)) + }) + + it('should pass optional parameters in request', function () { + const bidRequest = { + ...commonBidRequest, + ...nativeBidRequestParams, + } + bidRequest.params.tagid = 'test-tag' + bidRequest.params.publisher.name = 'test-publisher' + bidRequest.params.publisher.domain = 'test-publisher.com' + bidRequest.params.bcat = ['bad-category'] + bidRequest.params.badv = ['bad-advertiser'] + + const res = spec.buildRequests([bidRequest], commonBidderRequest) + const resData = JSON.parse(res.data) + expect(resData.imp[0].tagid).to.equal('test-tag') + expect(resData.site.publisher.name).to.equal('test-publisher') + expect(resData.site.publisher.domain).to.equal('test-publisher.com') + expect(resData.bcat).to.deep.equal(['bad-category']) + expect(resData.badv).to.deep.equal(['bad-advertiser']) + }); + + it('should pass bidder timeout', function () { + const bidRequest = { + ...commonBidRequest, + ...nativeBidRequestParams, + } + const bidderRequest = { + ...commonBidderRequest, + timeout: 500 + } + const res = spec.buildRequests([bidRequest], bidderRequest) + const resData = JSON.parse(res.data) + expect(resData.tmax).to.equal(500) + }); + + it('should pass GDPR consent', function () { + const bidRequest = { + ...commonBidRequest, + ...nativeBidRequestParams, + } + const bidderRequest = { + ...commonBidderRequest, + gdprConsent: { + gdprApplies: true, + consentString: 'consentString', + } + } + const res = spec.buildRequests([bidRequest], bidderRequest) + const resData = JSON.parse(res.data) + expect(resData.user.ext.consent).to.equal('consentString') + expect(resData.regs.ext.gdpr).to.equal(1) + }); + + it('should pass us privacy consent', function () { + const bidRequest = { + ...commonBidRequest, + ...nativeBidRequestParams, + } + const bidderRequest = { + ...commonBidderRequest, + uspConsent: 'consentString' + } + const res = spec.buildRequests([bidRequest], bidderRequest) + const resData = JSON.parse(res.data) + expect(resData.regs.ext.us_privacy).to.equal('consentString') + }); + + it('should pass coppa consent', function () { + const bidRequest = { + ...commonBidRequest, + ...nativeBidRequestParams, + } + config.setConfig({coppa: true}) + + const res = spec.buildRequests([bidRequest], commonBidderRequest) + const resData = JSON.parse(res.data) + expect(resData.regs.coppa).to.equal(1) + + config.resetConfig() + }); + }) + + describe('interpretResponse', function () { + it('should return empty array if no valid bids', function () { + const res = spec.interpretResponse({}, []) + expect(res).to.be.an('array').that.is.empty + }); + + it('should interpret native response', function () { + const serverResponse = { + body: { + id: '0a73e68c-9967-4391-b01b-dda2d9fc54e4', + seatbid: [ + { + bid: [ + { + id: '82822cf5-259c-11eb-8a52-f29e5275aa57', + impid: '1', + price: 1.1, + nurl: 'http://example.com/win/${AUCTION_PRICE}', + adm: '{"ver":"1.2","assets":[{"id":3,"required":1,"img":{"url":"http://example.com/img/url","w":120,"h":100}},{"id":0,"required":1,"title":{"text":"Test title"}},{"id":5,"data":{"value":"Test sponsor"}}],"link":{"url":"http://example.com/click/url"},"eventtrackers":[{"event":1,"method":1,"url":"http://example.com/impression"}]}', + adomain: [ + 'example.com' + ], + cid: '3487171', + crid: '28023739', + cat: [ + 'IAB10-2' + ] + } + ], + seat: 'acc-5537' + } + ], + bidid: '82822cf5-259c-11eb-8a52-b48e7518c657', + cur: 'USD' + }, + } + const request = { + bids: [ + { + ...commonBidRequest, + ...nativeBidRequestParams, + } + ] + } + const expectedRes = [ + { + requestId: request.bids[0].bidId, + cpm: 1.1, + creativeId: '28023739', + ttl: 360, + netRevenue: false, + currency: 'USD', + mediaType: 'native', + nurl: 'http://example.com/win/${AUCTION_PRICE}', + meta: { + 'advertiserDomains': [ + 'example.com' + ] + }, + native: { + clickTrackers: undefined, + clickUrl: 'http://example.com/click/url', + image: { + url: 'http://example.com/img/url', + width: 120, + height: 100 + }, + title: 'Test title', + sponsoredBy: 'Test sponsor', + impressionTrackers: [ + 'http://example.com/impression', + ] + } + } + ] + + const res = spec.interpretResponse(serverResponse, request) + expect(res).to.deep.equal(expectedRes) + }); + + it('should interpret display response', function () { + const serverResponse = { + body: { + id: '6b2eedc8-8ff5-46ef-adcf-e701b508943e', + seatbid: [ + { + bid: [ + { + id: 'd90fe7fa-28d7-11eb-8ce4-462a842a7cf9', + impid: '1', + price: 1.1, + nurl: 'http://example.com/win/${AUCTION_PRICE}', + adm: '
ad
', + adomain: [ + 'example.com' + ], + cid: '3865084', + crid: '29998660', + cat: [ + 'IAB10-2' + ], + w: 300, + h: 250 + } + ], + seat: 'acc-6536' + } + ], + bidid: 'd90fe7fa-28d7-11eb-8ce4-13d94bfa26f9', + cur: 'USD' + } + } + const request = { + bids: [ + { + ...commonBidRequest, + ...displayBidRequestParams + } + ] + } + const expectedRes = [ + { + requestId: request.bids[0].bidId, + cpm: 1.1, + creativeId: '29998660', + ttl: 360, + netRevenue: false, + currency: 'USD', + mediaType: 'banner', + nurl: 'http://example.com/win/${AUCTION_PRICE}', + ad: '
ad
', + width: 300, + height: 250, + meta: { + 'advertiserDomains': [ + 'example.com' + ] + }, + } + ] + + const res = spec.interpretResponse(serverResponse, request) + expect(res).to.deep.equal(expectedRes) + }); + }) + }) + + describe('getUserSyncs', function () { + const usersyncUrl = 'https://usersync-url.com'; + beforeEach(() => { + config.setConfig({ + outbrain: { + usersyncUrl: usersyncUrl, + } + } + ) + }) + after(() => { + config.resetConfig() + }) + + it('should return user sync if pixel enabled with outbrain config', function () { + const ret = spec.getUserSyncs({pixelEnabled: true}) + expect(ret).to.deep.equal([{type: 'image', url: usersyncUrl}]) + }) + + it('should not return user sync if pixel disabled', function () { + const ret = spec.getUserSyncs({pixelEnabled: false}) + expect(ret).to.be.an('array').that.is.empty + }) + + it('should not return user sync if url is not set', function () { + config.resetConfig() + const ret = spec.getUserSyncs({pixelEnabled: true}) + expect(ret).to.be.an('array').that.is.empty + }) + + it('should pass GDPR consent', function() { + expect(spec.getUserSyncs({ pixelEnabled: true }, {}, {gdprApplies: true, consentString: 'foo'}, undefined)).to.deep.equal([{ + type: 'image', url: `${usersyncUrl}?gdpr=1&gdpr_consent=foo` + }]); + expect(spec.getUserSyncs({ pixelEnabled: true }, {}, {gdprApplies: false, consentString: 'foo'}, undefined)).to.deep.equal([{ + type: 'image', url: `${usersyncUrl}?gdpr=0&gdpr_consent=foo` + }]); + expect(spec.getUserSyncs({ pixelEnabled: true }, {}, {gdprApplies: true, consentString: undefined}, undefined)).to.deep.equal([{ + type: 'image', url: `${usersyncUrl}?gdpr=1&gdpr_consent=` + }]); + }); + + it('should pass US consent', function() { + expect(spec.getUserSyncs({ pixelEnabled: true }, {}, undefined, '1NYN')).to.deep.equal([{ + type: 'image', url: `${usersyncUrl}?us_privacy=1NYN` + }]); + }); + + it('should pass GDPR and US consent', function() { + expect(spec.getUserSyncs({ pixelEnabled: true }, {}, {gdprApplies: true, consentString: 'foo'}, '1NYN')).to.deep.equal([{ + type: 'image', url: `${usersyncUrl}?gdpr=1&gdpr_consent=foo&us_privacy=1NYN` + }]); + }); + }) + + describe('onBidWon', function () { + it('should make an ajax call with the original cpm', function () { + const bid = { + nurl: 'http://example.com/win/${AUCTION_PRICE}', + cpm: 2.1, + originalCpm: 1.1, + } + spec.onBidWon(bid) + expect(server.requests[0].url).to.equals('http://example.com/win/1.1') + }); + }) +}) diff --git a/test/spec/modules/ozoneBidAdapter_spec.js b/test/spec/modules/ozoneBidAdapter_spec.js index f20e75dfedd..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': '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,29 +327,7 @@ 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': 'ID5-ZHMOpSv9CkZNiNd1oR4zc62AzCgSS73fPjmQ6Od7OA', @@ -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; @@ -2121,10 +2019,11 @@ describe('ozone Adapter', function () { '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; @@ -2141,59 +2040,118 @@ describe('ozone Adapter', function () { '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 7db22af82ab..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,114 +74,173 @@ 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() callback', function() { - let logErrorStub; - let callbackSpy = sinon.spy(); + describe('parrableIdSystem.getId()', function() { + describe('response callback function', function() { + let logErrorStub; + let callbackSpy = sinon.spy(); + + beforeEach(function() { + logErrorStub = sinon.stub(utils, 'logError'); + callbackSpy.resetHistory(); + writeParrableCookie({ eid: P_COOKIE_EID }); + }); - beforeEach(function() { - logErrorStub = sinon.stub(utils, 'logError'); - callbackSpy.resetHistory(); - writeParrableCookie({ eid: P_COOKIE_EID }); - }); + afterEach(function() { + removeParrableCookie(); + logErrorStub.restore(); + }) - afterEach(function() { - removeParrableCookie(); - logErrorStub.restore(); - }) + it('creates xhr to Parrable that synchronizes the ID', function() { + let getIdResult = parrableIdSubmodule.getId(P_CONFIG_MOCK); - it('creates xhr to Parrable that synchronizes the ID', function() { - let getIdResult = parrableIdSubmodule.getId(P_CONFIG_MOCK.params); + getIdResult.callback(callbackSpy); - getIdResult.callback(callbackSpy); + let request = server.requests[0]; + let queryParams = utils.parseQS(request.url.split('?')[1]); + let data = JSON.parse(atob(decodeBase64UrlSafe(queryParams.data))); - let request = server.requests[0]; - let queryParams = utils.parseQS(request.url.split('?')[1]); - let data = JSON.parse(atob(queryParams.data)); + expect(getIdResult.callback).to.be.a('function'); + expect(request.url).to.contain('h.parrable.com'); - expect(getIdResult.callback).to.be.a('function'); - expect(request.url).to.contain('h.parrable.com'); + expect(queryParams).to.not.have.property('us_privacy'); + expect(data).to.deep.equal({ + eid: P_COOKIE_EID, + trackers: P_CONFIG_MOCK.params.partners.split(','), + url: getRefererInfo().referer, + prebidVersion: '$prebid.version$', + isIframe: true + }); + + server.requests[0].respond(200, + { 'Content-Type': 'text/plain' }, + JSON.stringify({ eid: P_XHR_EID }) + ); + + expect(callbackSpy.lastCall.lastArg).to.deep.equal({ + eid: P_XHR_EID + }); + + expect(storage.getCookie(P_COOKIE_NAME)).to.equal( + encodeURIComponent('eid:' + P_XHR_EID) + ); + }); - 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 + it('xhr passes the uspString to Parrable', function() { + let uspString = '1YNN'; + uspDataHandler.setConsentData(uspString); + parrableIdSubmodule.getId( + P_CONFIG_MOCK, + null, + null + ).callback(callbackSpy); + uspDataHandler.setConsentData(null); + expect(server.requests[0].url).to.contain('us_privacy=' + uspString); }); - server.requests[0].respond(200, - { 'Content-Type': 'text/plain' }, - JSON.stringify({ eid: P_XHR_EID }) - ); + 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); - expect(callbackSpy.lastCall.lastArg).to.deep.equal({ - eid: P_XHR_EID + getIdResult.callback(callbackSpy); + + let request = server.requests[0]; + let queryParams = utils.parseQS(request.url.split('?')[1]); + expect(queryParams.data).to.equal(urlSafeBase64EncodedData); + btoaStub.restore(); }); - expect(storage.getCookie(P_COOKIE_NAME)).to.equal( - encodeURIComponent('eid:' + P_XHR_EID) - ); + it('should log an error and continue to callback if ajax request errors', function () { + let callBackSpy = sinon.spy(); + let submoduleCallback = parrableIdSubmodule.getId({ params: {partners: 'prebid'} }).callback; + submoduleCallback(callBackSpy); + let request = server.requests[0]; + expect(request.url).to.contain('h.parrable.com'); + request.respond( + 503, + null, + 'Unavailable' + ); + expect(logErrorStub.calledOnce).to.be.true; + expect(callBackSpy.calledOnce).to.be.true; + }); }); - it('xhr passes the uspString to Parrable', function() { - let uspString = '1YNN'; - uspDataHandler.setConsentData(uspString); - parrableIdSubmodule.getId( - P_CONFIG_MOCK.params, - null, - null - ).callback(callbackSpy); - uspDataHandler.setConsentData(null); - expect(server.requests[0].url).to.contain('us_privacy=' + uspString); - }); + describe('response id', function() { + it('provides the stored Parrable values if a cookie exists', function() { + writeParrableCookie({ eid: P_COOKIE_EID }); + let getIdResult = parrableIdSubmodule.getId(P_CONFIG_MOCK); + removeParrableCookie(); - it('should log an error and continue to callback if ajax request errors', function () { - let callBackSpy = sinon.spy(); - let submoduleCallback = parrableIdSubmodule.getId({partner: 'prebid'}).callback; - submoduleCallback(callBackSpy); - let request = server.requests[0]; - expect(request.url).to.contain('h.parrable.com'); - request.respond( - 503, - null, - 'Unavailable' - ); - expect(logErrorStub.calledOnce).to.be.true; - expect(callBackSpy.calledOnce).to.be.true; - }); - }); + expect(getIdResult.id).to.deep.equal({ + eid: P_COOKIE_EID + }); + }); - describe('parrableIdSystem.getId() id', function() { - it('provides the stored Parrable values if a cookie exists', function() { - writeParrableCookie({ eid: P_COOKIE_EID }); - let getIdResult = parrableIdSubmodule.getId(P_CONFIG_MOCK.params); - removeParrableCookie(); + it('provides the stored legacy Parrable ID values if cookies exist', function() { + let oldEid = '01.111.old-eid'; + let oldEidCookieName = '_parrable_eid'; + let oldOptoutCookieName = '_parrable_optout'; + + storage.setCookie(oldEidCookieName, oldEid); + storage.setCookie(oldOptoutCookieName, 'true'); - expect(getIdResult.id).to.deep.equal({ - eid: P_COOKIE_EID + let getIdResult = parrableIdSubmodule.getId(P_CONFIG_MOCK); + expect(getIdResult.id).to.deep.equal({ + eid: oldEid, + ibaOptout: true + }); + + // The ID system is expected to migrate old cookies to the new format + expect(storage.getCookie(P_COOKIE_NAME)).to.equal( + encodeURIComponent('eid:' + oldEid + ',ibaOptout:1') + ); + expect(storage.getCookie(oldEidCookieName)).to.equal(null); + expect(storage.getCookie(oldOptoutCookieName)).to.equal(null); + removeParrableCookie(); }); }); - it('provides the stored legacy Parrable ID values if cookies exist', function() { - let oldEid = '01.111.old-eid'; - let oldEidCookieName = '_parrable_eid'; - let oldOptoutCookieName = '_parrable_optout'; + describe('GDPR consent', () => { + let callbackSpy = sinon.spy(); - storage.setCookie(oldEidCookieName, oldEid); - storage.setCookie(oldOptoutCookieName, 'true'); + const config = { + params: { + partner: 'partner' + } + }; - let getIdResult = parrableIdSubmodule.getId(P_CONFIG_MOCK.params); - expect(getIdResult.id).to.deep.equal({ - eid: oldEid, - ibaOptout: true + 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'); + } + }) }); - - // The ID system is expected to migrate old cookies to the new format - expect(storage.getCookie(P_COOKIE_NAME)).to.equal( - encodeURIComponent('eid:' + oldEid + ',ibaOptout:1') - ); - expect(storage.getCookie(oldEidCookieName)).to.equal(null); - expect(storage.getCookie(oldOptoutCookieName)).to.equal(null); }); }); @@ -199,6 +258,209 @@ describe('Parrable ID System', function() { }); }); + describe('timezone filtering', function() { + before(function() { + sinon.stub(Intl, 'DateTimeFormat'); + }); + + after(function() { + Intl.DateTimeFormat.restore(); + }); + + it('permits an impression when no timezoneFilter is configured', function() { + expect(parrableIdSubmodule.getId({ params: { + partners: 'prebid-test', + } })).to.have.property('callback'); + }); + + it('permits an impression from a blocked timezone when a cookie exists', function() { + const blockedZone = 'Antarctica/South_Pole'; + const resolvedOptions = sinon.stub().returns({ timeZone: blockedZone }); + Intl.DateTimeFormat.returns({ resolvedOptions }); + + writeParrableCookie({ eid: P_COOKIE_EID }); + + expect(parrableIdSubmodule.getId({ params: { + partners: 'prebid-test', + timezoneFilter: { + blockedZones: [ blockedZone ] + } + } })).to.have.property('callback'); + expect(resolvedOptions.called).to.equal(false); + + removeParrableCookie(); + }) + + it('permits an impression from an allowed timezone', function() { + const allowedZone = 'America/New_York'; + const resolvedOptions = sinon.stub().returns({ timeZone: allowedZone }); + Intl.DateTimeFormat.returns({ resolvedOptions }); + + expect(parrableIdSubmodule.getId({ params: { + partners: 'prebid-test', + timezoneFilter: { + allowedZones: [ allowedZone ] + } + } })).to.have.property('callback'); + 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: { + partners: 'prebid-test', + timezoneFilter: { + blockedZones: [ blockedZone ] + } + } })).to.have.property('callback'); + expect(resolvedOptions.called).to.equal(true); + }); + + it('does not permit an impression from a blocked timezone', function() { + const blockedZone = 'America/New_York'; + const resolvedOptions = sinon.stub().returns({ timeZone: blockedZone }); + Intl.DateTimeFormat.returns({ resolvedOptions }); + + expect(parrableIdSubmodule.getId({ params: { + partners: 'prebid-test', + timezoneFilter: { + blockedZones: [ blockedZone ] + } + } })).to.equal(null); + 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: { + partners: 'prebid-test', + timezoneFilter: { + allowedZones: [ timezone ], + blockedZones: [ timezone ] + } + } })).to.equal(null); + expect(resolvedOptions.called).to.equal(true); + }); + }); + + describe('timezone offset filtering', function() { + before(function() { + sinon.stub(Date.prototype, 'getTimezoneOffset'); + }); + + afterEach(function() { + Date.prototype.getTimezoneOffset.reset(); + }) + + after(function() { + Date.prototype.getTimezoneOffset.restore(); + }); + + it('permits an impression from a blocked offset when a cookie exists', function() { + const blockedOffset = -4; + Date.prototype.getTimezoneOffset.returns(blockedOffset * 60); + + writeParrableCookie({ eid: P_COOKIE_EID }); + + expect(parrableIdSubmodule.getId({ params: { + partners: 'prebid-test', + timezoneFilter: { + blockedOffsets: [ blockedOffset ] + } + } })).to.have.property('callback'); + + removeParrableCookie(); + }); + + it('permits an impression from an allowed offset', function() { + const allowedOffset = -5; + Date.prototype.getTimezoneOffset.returns(allowedOffset * 60); + + expect(parrableIdSubmodule.getId({ params: { + partners: 'prebid-test', + timezoneFilter: { + allowedOffsets: [ allowedOffset ] + } + } })).to.have.property('callback'); + expect(Date.prototype.getTimezoneOffset.called).to.equal(true); + }); + + it('permits an impression from an offset that is not blocked', function() { + const allowedOffset = -5; + const blockedOffset = 5; + Date.prototype.getTimezoneOffset.returns(allowedOffset * 60); + + expect(parrableIdSubmodule.getId({ params: { + partners: 'prebid-test', + timezoneFilter: { + blockedOffsets: [ blockedOffset ] + } + }})).to.have.property('callback'); + expect(Date.prototype.getTimezoneOffset.called).to.equal(true); + }); + + it('does not permit an impression from a blocked offset', function() { + const blockedOffset = -5; + Date.prototype.getTimezoneOffset.returns(blockedOffset * 60); + + expect(parrableIdSubmodule.getId({ params: { + partners: 'prebid-test', + timezoneFilter: { + blockedOffsets: [ blockedOffset ] + } + } })).to.equal(null); + expect(Date.prototype.getTimezoneOffset.called).to.equal(true); + }); + + it('does not permit an impression from a blocked offset even when also allowed', function() { + const offset = -5; + Date.prototype.getTimezoneOffset.returns(offset * 60); + + expect(parrableIdSubmodule.getId({ params: { + partners: 'prebid-test', + timezoneFilter: { + allowedOffset: [ offset ], + blockedOffsets: [ offset ] + } + } })).to.equal(null); + expect(Date.prototype.getTimezoneOffset.called).to.equal(true); + }); + }); + describe('userId requestBids hook', function() { let adUnits; @@ -266,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..cf1f3861eab --- /dev/null +++ b/test/spec/modules/permutiveRtdProvider_spec.js @@ -0,0 +1,397 @@ +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 Magnite', 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) + } + }) + }) + } + }) + it('sets segment targeting for TrustX', 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 === 'trustx') { + expect(deepAccess(params, 'keywords.p_standard')).to.eql(data.ac) + } + }) + }) + } + }) + }) + + describe('Custom segment targeting', function () { + it('sets custom segment targeting for Magnite', 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('Existing key-value targeting', function () { + it('doesn\'t overwrite existing key-values for Xandr', function () { + 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.test_kv')).to.eql(['true']) + } + }) + }) + } + }) + it('doesn\'t overwrite existing key-values for Magnite', function () { + 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.test_kv')).to.eql(['true']) + } + }) + }) + } + }) + it('doesn\'t overwrite existing key-values for Ozone', function () { + 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.test_kv')).to.eql(['true']) + } + }) + }) + } + }) + it('doesn\'t overwrite existing key-values for TrustX', function () { + 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 === 'trustx') { + expect(deepAccess(params, 'keywords.test_kv')).to.eql(['true']) + } + }) + }) + } + }) + }) + + 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', 'trustx'], + 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 () { + const div_1_sizes = [ + [300, 250], + [300, 600] + ] + const div_2_sizes = [ + [728, 90], + [970, 250] + ] + return [ + { + code: '/19968336/header-bid-tag-0', + mediaTypes: { + banner: { + sizes: div_1_sizes + } + }, + bids: [ + { + bidder: 'appnexus', + params: { + placementId: 13144370, + keywords: { + test_kv: ['true'] + } + } + }, + { + bidder: 'rubicon', + params: { + accountId: '9840', + siteId: '123564', + zoneId: '583584', + inventory: { + area: ['home'] + }, + visitor: { + test_kv: ['true'] + } + } + }, + { + bidder: 'ozone', + params: { + publisherId: 'OZONEGMG0001', + siteId: '4204204209', + placementId: '0420420500', + customData: [ + { + settings: {}, + targeting: { + test_kv: ['true'] + } + } + ], + ozoneData: {} + } + }, + { + bidder: 'trustx', + params: { + uid: 45, + keywords: { + test_kv: ['true'] + } + } + } + ] + }, + { + code: '/19968336/header-bid-tag-1', + mediaTypes: { + banner: { + sizes: div_2_sizes + } + }, + bids: [ + { + bidder: 'appnexus', + params: { + placementId: 13144370, + keywords: { + test_kv: ['true'] + } + } + }, + { + bidder: 'ozone', + params: { + publisherId: 'OZONEGMG0001', + siteId: '4204204209', + placementId: '0420420500', + customData: [ + { + targeting: { + test_kv: ['true'] + } + } + ] + } + } + ] + } + ] +} diff --git a/test/spec/modules/prebidServerBidAdapter_spec.js b/test/spec/modules/prebidServerBidAdapter_spec.js index 5abe068c100..6833daf49cf 100644 --- a/test/spec/modules/prebidServerBidAdapter_spec.js +++ b/test/spec/modules/prebidServerBidAdapter_spec.js @@ -15,7 +15,10 @@ let CONFIG = { bidders: ['appnexus'], timeout: 1000, cacheMarkup: 2, - endpoint: 'https://prebid.adnxs.com/pbs/v1/openrtb2/auction' + endpoint: { + p1Consent: 'https://prebid.adnxs.com/pbs/v1/openrtb2/auction', + noP1Consent: 'https://prebid.adnxs.com/pbs/v1/openrtb2/auction' + } }; const REQUEST = { @@ -26,6 +29,7 @@ const REQUEST = { 'secure': 0, 'url': '', 'prebid_version': '0.30.0-pre', + 's2sConfig': CONFIG, 'ad_units': [ { 'code': 'div-gpt-ad-1460505748561-0', @@ -80,6 +84,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 +115,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 +168,7 @@ const OUTSTREAM_VIDEO_REQUEST = { } } } - ] + ], }; let BID_REQUESTS; @@ -267,6 +273,7 @@ const RESPONSE_OPENRTB = { 'win': 'http://wurl.org?id=333' } }, + 'dchain': { 'ver': '1.0', 'complete': 0, 'nodes': [ { 'asi': 'magnite.com', 'bsid': '123456789', } ] }, 'bidder': { 'appnexus': { 'brand_id': 1, @@ -498,10 +505,8 @@ describe('S2S Adapter', function () { }); it('should not add outstrean without renderer', function () { - let ortb2Config = utils.deepClone(CONFIG); - ortb2Config.endpoint = 'https://prebid.adnxs.com/pbs/v1/openrtb2/auction'; + 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); @@ -509,6 +514,42 @@ describe('S2S Adapter', function () { expect(requestBid.imp[0].video).to.not.exist; }); + it('should default video placement if not defined and instream', function () { + let ortb2Config = utils.deepClone(CONFIG); + ortb2Config.endpoint.p1Consent = 'https://prebid.adnxs.com/pbs/v1/openrtb2/auction'; + + config.setConfig({ s2sConfig: ortb2Config }); + + let videoBid = utils.deepClone(VIDEO_REQUEST); + videoBid.ad_units[0].mediaTypes.video.context = 'instream'; + adapter.callBids(videoBid, BID_REQUESTS, addBidResponse, done, ajax); + + const requestBid = JSON.parse(server.requests[0].requestBody); + expect(requestBid.imp[0].banner).to.not.exist; + expect(requestBid.imp[0].video).to.exist; + expect(requestBid.imp[0].video.placement).to.equal(1); + }); + + it('converts video mediaType properties into openRTB format', function () { + let ortb2Config = utils.deepClone(CONFIG); + ortb2Config.endpoint.p1Consent = 'https://prebid.adnxs.com/pbs/v1/openrtb2/auction'; + + config.setConfig({ s2sConfig: ortb2Config }); + + let videoBid = utils.deepClone(VIDEO_REQUEST); + videoBid.ad_units[0].mediaTypes.video.context = 'instream'; + adapter.callBids(videoBid, BID_REQUESTS, addBidResponse, done, ajax); + + const requestBid = JSON.parse(server.requests[0].requestBody); + expect(requestBid.imp[0].banner).to.not.exist; + expect(requestBid.imp[0].video).to.exist; + expect(requestBid.imp[0].video.placement).to.equal(1); + expect(requestBid.imp[0].video.w).to.equal(640); + expect(requestBid.imp[0].video.h).to.equal(480); + expect(requestBid.imp[0].video.playerSize).to.be.undefined; + expect(requestBid.imp[0].video.context).to.be.undefined; + }); + it('exists and is a function', function () { expect(adapter.callBids).to.exist.and.to.be.a('function'); }); @@ -519,10 +560,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); @@ -548,10 +586,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); @@ -580,7 +615,7 @@ describe('S2S Adapter', function () { it('check gdpr info gets added into cookie_sync request: have consent data', function () { let cookieSyncConfig = utils.deepClone(CONFIG); - cookieSyncConfig.syncEndpoint = 'https://prebid.adnxs.com/pbs/v1/cookie_sync'; + cookieSyncConfig.syncEndpoint = { p1Consent: 'https://prebid.adnxs.com/pbs/v1/cookie_sync' }; let consentConfig = { consentManagement: { cmpApi: 'iab' }, s2sConfig: cookieSyncConfig }; config.setConfig(consentConfig); @@ -592,7 +627,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); @@ -603,27 +641,30 @@ describe('S2S Adapter', function () { it('check gdpr info gets added into cookie_sync request: have consent data but gdprApplies is false', function () { let cookieSyncConfig = utils.deepClone(CONFIG); - cookieSyncConfig.syncEndpoint = 'https://prebid.adnxs.com/pbs/v1/cookie_sync'; + cookieSyncConfig.syncEndpoint = { p1Consent: 'https://prebid.adnxs.com/pbs/v1/cookie_sync' }; let consentConfig = { consentManagement: { cmpApi: 'iab' }, s2sConfig: cookieSyncConfig }; config.setConfig(consentConfig); + const s2sBidRequest = utils.deepClone(REQUEST); + s2sBidRequest.s2sConfig = cookieSyncConfig; + let gdprBidRequest = utils.deepClone(BID_REQUESTS); gdprBidRequest[0].gdprConsent = { 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); expect(requestBid.gdpr_consent).is.undefined; }); - it('checks gdpr info gets added to cookie_sync request: consent data unknown', function () { + it('checks gdpr info gets added to cookie_sync request: applies is false', function () { let cookieSyncConfig = utils.deepClone(CONFIG); - cookieSyncConfig.syncEndpoint = 'https://prebid.adnxs.com/pbs/v1/cookie_sync'; + cookieSyncConfig.syncEndpoint = { p1Consent: 'https://prebid.adnxs.com/pbs/v1/cookie_sync' }; let consentConfig = { consentManagement: { cmpApi: 'iab' }, s2sConfig: cookieSyncConfig }; config.setConfig(consentConfig); @@ -631,13 +672,16 @@ describe('S2S Adapter', function () { let gdprBidRequest = utils.deepClone(BID_REQUESTS); gdprBidRequest[0].gdprConsent = { consentString: undefined, - gdprApplies: undefined + gdprApplies: false }; - 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; + expect(requestBid.gdpr).is.equal(0); expect(requestBid.gdpr_consent).is.undefined; }); }); @@ -648,9 +692,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'; @@ -671,13 +713,16 @@ describe('S2S Adapter', function () { it('is added to cookie_sync request when in bidRequest', function () { let cookieSyncConfig = utils.deepClone(CONFIG); - cookieSyncConfig.syncEndpoint = 'https://prebid.adnxs.com/pbs/v1/cookie_sync'; + cookieSyncConfig.syncEndpoint = { p1Consent: 'https://prebid.adnxs.com/pbs/v1/cookie_sync' }; config.setConfig({ s2sConfig: cookieSyncConfig }); let uspBidRequest = utils.deepClone(BID_REQUESTS); uspBidRequest[0].uspConsent = '1YNN'; - 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'); @@ -692,9 +737,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'; @@ -722,7 +765,7 @@ describe('S2S Adapter', function () { it('is added to cookie_sync request when in bidRequest', function () { let cookieSyncConfig = utils.deepClone(CONFIG); - cookieSyncConfig.syncEndpoint = 'https://prebid.adnxs.com/pbs/v1/cookie_sync'; + cookieSyncConfig.syncEndpoint = { p1Consent: 'https://prebid.adnxs.com/pbs/v1/cookie_sync' }; config.setConfig({ s2sConfig: cookieSyncConfig }); let consentBidRequest = utils.deepClone(BID_REQUESTS); @@ -732,7 +775,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'); @@ -766,7 +812,9 @@ describe('S2S Adapter', function () { it('adds device and app objects to request for OpenRTB', function () { const s2sConfig = Object.assign({}, CONFIG, { - endpoint: 'https://prebid.adnxs.com/pbs/v1/openrtb2/auction' + endpoint: { + p1Consent: 'https://prebid.adnxs.com/pbs/v1/openrtb2/auction' + } }); const _config = { @@ -790,11 +838,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' } }; @@ -810,13 +855,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' }, }; @@ -834,12 +970,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); @@ -893,12 +1025,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', @@ -933,14 +1061,73 @@ 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' + endpoint: { + p1Consent: '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); @@ -952,48 +1139,52 @@ describe('S2S Adapter', function () { expect(requestBid.ext).to.deep.equal({ prebid: { - aliases: { - brealtime: 'appnexus' - }, auctiontimestamp: 1510852447530, targeting: { includebidderkeys: false, includewinners: true + }, + channel: { + name: 'pbjs', + version: 'v$prebid.version$' } } }); }); - 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' + endpoint: { + p1Consent: '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, includewinners: true + }, + channel: { + name: 'pbjs', + version: 'v$prebid.version$' } } }); @@ -1001,7 +1192,9 @@ describe('S2S Adapter', function () { it('converts appnexus params to expected format for PBS', function () { const s2sConfig = Object.assign({}, CONFIG, { - endpoint: 'https://prebid.adnxs.com/pbs/v1/openrtb2/auction' + endpoint: { + p1Consent: 'https://prebid.adnxs.com/pbs/v1/openrtb2/auction' + } }); config.setConfig({ s2sConfig: s2sConfig }); @@ -1030,13 +1223,16 @@ describe('S2S Adapter', function () { it('adds limit to the cookie_sync request if userSyncLimit is greater than 0', function () { let cookieSyncConfig = utils.deepClone(CONFIG); - cookieSyncConfig.syncEndpoint = 'https://prebid.adnxs.com/pbs/v1/cookie_sync'; + cookieSyncConfig.syncEndpoint = { p1Consent: 'https://prebid.adnxs.com/pbs/v1/cookie_sync' }; cookieSyncConfig.userSyncLimit = 1; + const s2sBidRequest = utils.deepClone(REQUEST); + s2sBidRequest.s2sConfig = cookieSyncConfig; + config.setConfig({ s2sConfig: cookieSyncConfig }); let bidRequest = utils.deepClone(BID_REQUESTS); - adapter.callBids(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); @@ -1046,11 +1242,14 @@ describe('S2S Adapter', function () { it('does not add limit to cooke_sync request if userSyncLimit is missing or 0', function () { let cookieSyncConfig = utils.deepClone(CONFIG); - cookieSyncConfig.syncEndpoint = 'https://prebid.adnxs.com/pbs/v1/cookie_sync'; + cookieSyncConfig.syncEndpoint = { p1Consent: 'https://prebid.adnxs.com/pbs/v1/cookie_sync' }; config.setConfig({ s2sConfig: cookieSyncConfig }); + const s2sBidRequest = utils.deepClone(REQUEST); + s2sBidRequest.s2sConfig = cookieSyncConfig; + let bidRequest = utils.deepClone(BID_REQUESTS); - adapter.callBids(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); @@ -1061,8 +1260,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); @@ -1072,7 +1274,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' @@ -1085,8 +1286,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') @@ -1094,7 +1298,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' @@ -1106,6 +1309,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, @@ -1116,7 +1322,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'); @@ -1134,7 +1341,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'); @@ -1146,10 +1354,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); @@ -1162,7 +1367,13 @@ describe('S2S Adapter', function () { lipbid: 'li-xyz', segments: ['segA', 'segB'] }, - idl_env: '0000-1111-2222-3333' + idl_env: '0000-1111-2222-3333', + id5id: { + uid: '11111', + ext: { + linkType: 'some-link-type' + } + } }; userIdBidRequest[0].bids[0].userIdAsEids = createEidsArray(userIdBidRequest[0].bids[0].userId); @@ -1183,16 +1394,16 @@ describe('S2S Adapter', function () { expect(requestBid.user.ext.eids.filter(eid => eid.source === 'liveintent.com')[0].ext.segments.length).is.equal(2); expect(requestBid.user.ext.eids.filter(eid => eid.source === 'liveintent.com')[0].ext.segments[0]).is.equal('segA'); expect(requestBid.user.ext.eids.filter(eid => eid.source === 'liveintent.com')[0].ext.segments[1]).is.equal('segB'); + expect(requestBid.user.ext.eids.filter(eid => eid.source === 'id5-sync.com')).is.not.empty; + expect(requestBid.user.ext.eids.filter(eid => eid.source === 'id5-sync.com')[0].uids[0].id).is.equal('11111'); + expect(requestBid.user.ext.eids.filter(eid => eid.source === 'id5-sync.com')[0].uids[0].ext.linkType).is.equal('some-link-type'); // LiveRamp should exist expect(requestBid.user.ext.eids.filter(eid => eid.source === 'liveramp.com')[0].uids[0].id).is.equal('0000-1111-2222-3333'); }); 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); @@ -1203,11 +1414,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); @@ -1218,9 +1426,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); @@ -1231,7 +1437,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' @@ -1245,7 +1450,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'); @@ -1254,7 +1463,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' @@ -1266,9 +1474,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'); @@ -1277,7 +1488,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' } @@ -1288,13 +1498,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: { @@ -1306,7 +1519,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, @@ -1320,13 +1532,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, @@ -1337,7 +1552,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' @@ -1354,13 +1568,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' @@ -1397,6 +1614,52 @@ describe('S2S Adapter', function () { expect(parsedRequestBody.source.ext.schain).to.deep.equal(schainObject); }); + it('passes multibid array in request', function () { + const bidRequests = utils.deepClone(BID_REQUESTS); + const multibid = [{ + bidder: 'bidderA', + maxBids: 2 + }, { + bidder: 'bidderB', + maxBids: 2 + }]; + const expected = [{ + bidder: 'bidderA', + maxbids: 2 + }, { + bidder: 'bidderB', + maxbids: 2 + }]; + + config.setConfig({multibid: multibid}); + + adapter.callBids(REQUEST, bidRequests, addBidResponse, done, ajax); + const parsedRequestBody = JSON.parse(server.requests[0].requestBody); + expect(parsedRequestBody.ext.prebid.multibid).to.deep.equal(expected); + }); + + it('sets and passes pbjs version in request if channel does not exist in s2sConfig', () => { + const s2sBidRequest = utils.deepClone(REQUEST); + const bidRequests = utils.deepClone(BID_REQUESTS); + + adapter.callBids(s2sBidRequest, bidRequests, addBidResponse, done, ajax); + + const parsedRequestBody = JSON.parse(server.requests[0].requestBody); + expect(parsedRequestBody.ext.prebid.channel).to.deep.equal({name: 'pbjs', version: 'v$prebid.version$'}); + }); + + it('does not set pbjs version in request if channel does exist in s2sConfig', () => { + const s2sBidRequest = utils.deepClone(REQUEST); + const bidRequests = utils.deepClone(BID_REQUESTS); + + utils.deepSetValue(s2sBidRequest, 's2sConfig.extPrebid.channel', {test: 1}); + + adapter.callBids(s2sBidRequest, bidRequests, addBidResponse, done, ajax); + + const parsedRequestBody = JSON.parse(server.requests[0].requestBody); + expect(parsedRequestBody.ext.prebid.channel).to.deep.equal({test: 1}); + }); + it('passes first party data in request', () => { const s2sBidRequest = utils.deepClone(REQUEST); const bidRequests = utils.deepClone(BID_REQUESTS); @@ -1429,23 +1692,44 @@ describe('S2S Adapter', function () { const expected = allowedBidders.map(bidder => ({ bidders: [ bidder ], - config: { fpd: { site: context, user } } + config: { + ortb2: { + site: { + content: { userrating: 4 }, + ext: { + data: { + pageType: 'article', + category: 'tools' + } + } + }, + user: { + yob: '1984', + geo: { country: 'ca' }, + ext: { + data: { + registered: true, + interests: ['cars'] + } + } + } + } + } })); + const commonContextExpected = utils.mergeDeep({'page': 'http://mytestpage.com', 'publisher': {'id': '1'}}, commonContext); config.setConfig({ fpd: { context: commonContext, user: commonUser } }); config.setBidderConfig({ bidders: allowedBidders, config: { fpd: { context, user } } }); adapter.callBids(s2sBidRequest, bidRequests, addBidResponse, done, ajax); const parsedRequestBody = JSON.parse(server.requests[0].requestBody); expect(parsedRequestBody.ext.prebid.bidderconfig).to.deep.equal(expected); - expect(parsedRequestBody.site.ext.data).to.deep.equal(commonContext); - expect(parsedRequestBody.user.ext.data).to.deep.equal(commonUser); + expect(parsedRequestBody.site).to.deep.equal(commonContextExpected); + expect(parsedRequestBody.user).to.deep.equal(commonUser); }); 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 }; + it('should not send \"imp.ext.data.pbadslot\" if \"ortb2Imp.ext\" is undefined', function () { + const consentConfig = { s2sConfig: CONFIG }; config.setConfig(consentConfig); const bidRequest = utils.deepClone(REQUEST); @@ -1454,34 +1738,32 @@ describe('S2S Adapter', function () { expect(parsedRequestBody.imp).to.be.a('array'); expect(parsedRequestBody.imp[0]).to.be.a('object'); - expect(parsedRequestBody.imp[0]).to.not.have.deep.nested.property('ext.context.data.pbadslot'); + expect(parsedRequestBody.imp[0]).to.not.have.deep.nested.property('ext.data.pbadslot'); }); - 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 }; + it('should not send \"imp.ext.data.pbadslot\" if \"ortb2Imp.ext.data.pbadslot\" is undefined', function () { + const consentConfig = { s2sConfig: CONFIG }; config.setConfig(consentConfig); const bidRequest = utils.deepClone(REQUEST); - bidRequest.ad_units[0].fpd = {}; + bidRequest.ad_units[0].ortb2Imp = {}; adapter.callBids(bidRequest, BID_REQUESTS, addBidResponse, done, ajax); const parsedRequestBody = JSON.parse(server.requests[0].requestBody); expect(parsedRequestBody.imp).to.be.a('array'); expect(parsedRequestBody.imp[0]).to.be.a('object'); - expect(parsedRequestBody.imp[0]).to.not.have.deep.nested.property('ext.context.data.pbadslot'); + expect(parsedRequestBody.imp[0]).to.not.have.deep.nested.property('ext.data.pbadslot'); }); - 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 }; + it('should not send \"imp.ext.data.pbadslot\" if \"ortb2Imp.ext.data.pbadslot\" is empty string', function () { + const consentConfig = { s2sConfig: CONFIG }; config.setConfig(consentConfig); const bidRequest = utils.deepClone(REQUEST); - bidRequest.ad_units[0].fpd = { - context: { - pbAdSlot: '' + bidRequest.ad_units[0].ortb2Imp = { + ext: { + data: { + pbadslot: '' + } } }; @@ -1490,18 +1772,18 @@ describe('S2S Adapter', function () { expect(parsedRequestBody.imp).to.be.a('array'); expect(parsedRequestBody.imp[0]).to.be.a('object'); - expect(parsedRequestBody.imp[0]).to.not.have.deep.nested.property('ext.context.data.pbadslot'); + expect(parsedRequestBody.imp[0]).to.not.have.deep.nested.property('ext.data.pbadslot'); }); - 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 }; + it('should send \"imp.ext.data.pbadslot\" if \"ortb2Imp.ext.data.pbadslot\" value is a non-empty string', function () { + const consentConfig = { s2sConfig: CONFIG }; config.setConfig(consentConfig); const bidRequest = utils.deepClone(REQUEST); - bidRequest.ad_units[0].fpd = { - context: { - pbAdSlot: '/a/b/c' + bidRequest.ad_units[0].ortb2Imp = { + ext: { + data: { + pbadslot: '/a/b/c' + } } }; @@ -1510,16 +1792,14 @@ describe('S2S Adapter', function () { expect(parsedRequestBody.imp).to.be.a('array'); expect(parsedRequestBody.imp[0]).to.be.a('object'); - expect(parsedRequestBody.imp[0]).to.have.deep.nested.property('ext.context.data.pbadslot'); - expect(parsedRequestBody.imp[0].ext.context.data.pbadslot).to.equal('/a/b/c'); + expect(parsedRequestBody.imp[0]).to.have.deep.nested.property('ext.data.pbadslot'); + expect(parsedRequestBody.imp[0].ext.data.pbadslot).to.equal('/a/b/c'); }); }); describe('GAM ad unit config', function () { - it('should not send \"imp.ext.context.data.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 }; + it('should not send \"imp.ext.data.adserver.adslot\" if \"ortb2Imp.ext\" is undefined', function () { + const consentConfig = { s2sConfig: CONFIG }; config.setConfig(consentConfig); const bidRequest = utils.deepClone(REQUEST); @@ -1528,35 +1808,33 @@ describe('S2S Adapter', function () { expect(parsedRequestBody.imp).to.be.a('array'); expect(parsedRequestBody.imp[0]).to.be.a('object'); - expect(parsedRequestBody.imp[0]).to.not.have.deep.nested.property('ext.context.data.adslot'); + expect(parsedRequestBody.imp[0]).to.not.have.deep.nested.property('ext.data.adslot'); }); - it('should not send \"imp.ext.context.data.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 }; + it('should not send \"imp.ext.data.adserver.adslot\" if \"ortb2Imp.ext.data.adserver.adslot\" is undefined', function () { + const consentConfig = { s2sConfig: CONFIG }; config.setConfig(consentConfig); const bidRequest = utils.deepClone(REQUEST); - bidRequest.ad_units[0].fpd = {}; + bidRequest.ad_units[0].ortb2Imp = {}; adapter.callBids(bidRequest, BID_REQUESTS, addBidResponse, done, ajax); const parsedRequestBody = JSON.parse(server.requests[0].requestBody); expect(parsedRequestBody.imp).to.be.a('array'); expect(parsedRequestBody.imp[0]).to.be.a('object'); - expect(parsedRequestBody.imp[0]).to.not.have.deep.nested.property('ext.context.data.adslot'); + expect(parsedRequestBody.imp[0]).to.not.have.deep.nested.property('ext.data.adslot'); }); - it('should not send \"imp.ext.context.data.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 }; + it('should not send \"imp.ext.data.adserver.adslot\" if \"ortb2Imp.ext.data.adserver.adslot\" is empty string', function () { + const consentConfig = { s2sConfig: CONFIG }; config.setConfig(consentConfig); const bidRequest = utils.deepClone(REQUEST); - bidRequest.ad_units[0].fpd = { - context: { - adServer: { - adSlot: '' + bidRequest.ad_units[0].ortb2Imp = { + ext: { + data: { + adserver: { + adslot: '' + } } } }; @@ -1566,19 +1844,20 @@ describe('S2S Adapter', function () { expect(parsedRequestBody.imp).to.be.a('array'); expect(parsedRequestBody.imp[0]).to.be.a('object'); - expect(parsedRequestBody.imp[0]).to.not.have.deep.nested.property('ext.context.data.adslot'); + expect(parsedRequestBody.imp[0]).to.not.have.deep.nested.property('ext.data.adslot'); }); - it('should send \"imp.ext.context.data.adslot\" if \"fpd.context.adServer.adSlot\" 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 }; + it('should send both \"adslot\" and \"name\" from \"imp.ext.data.adserver\" if \"ortb2Imp.ext.data.adserver.adslot\" and \"ortb2Imp.ext.data.adserver.name\" values are non-empty strings', function () { + const consentConfig = { s2sConfig: CONFIG }; config.setConfig(consentConfig); const bidRequest = utils.deepClone(REQUEST); - bidRequest.ad_units[0].fpd = { - context: { - adServer: { - adSlot: '/a/b/c' + bidRequest.ad_units[0].ortb2Imp = { + ext: { + data: { + adserver: { + adslot: '/a/b/c', + name: 'adserverName1' + } } } }; @@ -1588,12 +1867,40 @@ describe('S2S Adapter', function () { expect(parsedRequestBody.imp).to.be.a('array'); expect(parsedRequestBody.imp[0]).to.be.a('object'); - expect(parsedRequestBody.imp[0]).to.have.deep.nested.property('ext.context.data.adslot'); - expect(parsedRequestBody.imp[0].ext.context.data.adslot).to.equal('/a/b/c'); + expect(parsedRequestBody.imp[0]).to.have.deep.nested.property('ext.data.adserver.adslot'); + expect(parsedRequestBody.imp[0]).to.have.deep.nested.property('ext.data.adserver.name'); + expect(parsedRequestBody.imp[0].ext.data.adserver.adslot).to.equal('/a/b/c'); + expect(parsedRequestBody.imp[0].ext.data.adserver.name).to.equal('adserverName1'); }); }); }); + describe('ext.prebid config', function () { + it('should send \"imp.ext.prebid.storedrequest.id\" if \"ortb2Imp.ext.prebid.storedrequest.id\" is set', function () { + const consentConfig = { s2sConfig: CONFIG }; + config.setConfig(consentConfig); + const bidRequest = utils.deepClone(REQUEST); + const storedRequestId = 'my-id'; + bidRequest.ad_units[0].ortb2Imp = { + ext: { + prebid: { + storedrequest: { + id: storedRequestId + } + } + } + }; + + adapter.callBids(bidRequest, BID_REQUESTS, addBidResponse, done, ajax); + const parsedRequestBody = JSON.parse(server.requests[0].requestBody); + + expect(parsedRequestBody.imp).to.be.a('array'); + expect(parsedRequestBody.imp[0]).to.be.a('object'); + expect(parsedRequestBody.imp[0]).to.have.deep.nested.property('ext.prebid.storedrequest.id'); + expect(parsedRequestBody.imp[0].ext.prebid.storedrequest.id).to.equal(storedRequestId); + }); + }); + describe('response handler', function () { beforeEach(function () { sinon.stub(utils, 'triggerPixel'); @@ -1742,10 +2049,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)); @@ -1756,10 +2060,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)); @@ -1778,6 +2079,9 @@ describe('S2S Adapter', function () { expect(response).to.have.property('meta'); expect(response.meta).to.have.property('advertiserDomains'); expect(response.meta.advertiserDomains[0]).to.equal('appnexus.com'); + expect(response.meta).to.have.property('dchain'); + expect(response.meta.dchain.ver).to.equal('1.0'); + expect(response.meta.dchain.nodes[0].asi).to.equal('magnite.com'); expect(response).to.not.have.property('vastUrl'); expect(response).to.not.have.property('videoCacheKey'); expect(response).to.have.property('ttl', 60); @@ -1785,12 +2089,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); @@ -1802,11 +2107,16 @@ describe('S2S Adapter', function () { it('handles OpenRTB video responses', function () { const s2sConfig = Object.assign({}, CONFIG, { - endpoint: 'https://prebidserverurl/openrtb2/auction?querystring=param' + endpoint: { + p1Consent: 'https://prebidserverurl/openrtb2/auction?querystring=param' + } }); 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); @@ -1821,7 +2131,9 @@ describe('S2S Adapter', function () { it('handles response cache from ext.prebid.cache.vastXml', function () { const s2sConfig = Object.assign({}, CONFIG, { - endpoint: 'https://prebidserverurl/openrtb2/auction?querystring=param' + endpoint: { + p1Consent: 'https://prebidserverurl/openrtb2/auction?querystring=param' + } }); config.setConfig({ s2sConfig }); const cacheResponse = utils.deepClone(RESPONSE_OPENRTB_VIDEO); @@ -1834,7 +2146,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); @@ -1847,7 +2162,9 @@ describe('S2S Adapter', function () { it('add adserverTargeting object to bids when ext.prebid.targeting is defined', function () { const s2sConfig = Object.assign({}, CONFIG, { - endpoint: 'https://prebidserverurl/openrtb2/auction?querystring=param' + endpoint: { + p1Consent: 'https://prebidserverurl/openrtb2/auction?querystring=param' + } }); config.setConfig({ s2sConfig }); const cacheResponse = utils.deepClone(RESPONSE_OPENRTB_VIDEO); @@ -1859,7 +2176,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); @@ -1874,7 +2195,9 @@ describe('S2S Adapter', function () { it('handles response cache from ext.prebid.targeting', function () { const s2sConfig = Object.assign({}, CONFIG, { - endpoint: 'https://prebidserverurl/openrtb2/auction?querystring=param' + endpoint: { + p1Consent: 'https://prebidserverurl/openrtb2/auction?querystring=param' + } }); config.setConfig({ s2sConfig }); const cacheResponse = utils.deepClone(RESPONSE_OPENRTB_VIDEO); @@ -1885,7 +2208,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); @@ -1898,7 +2225,9 @@ describe('S2S Adapter', function () { it('handles response cache from ext.prebid.targeting with wurl', function () { const s2sConfig = Object.assign({}, CONFIG, { - endpoint: 'https://prebidserverurl/openrtb2/auction?querystring=param' + endpoint: { + p1Consent: 'https://prebidserverurl/openrtb2/auction?querystring=param' + } }); config.setConfig({ s2sConfig }); const cacheResponse = utils.deepClone(RESPONSE_OPENRTB_VIDEO); @@ -1912,7 +2241,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); @@ -1922,7 +2254,9 @@ describe('S2S Adapter', function () { it('handles response cache from ext.prebid.targeting with wurl and removes invalid targeting', function () { const s2sConfig = Object.assign({}, CONFIG, { - endpoint: 'https://prebidserverurl/openrtb2/auction?querystring=param' + endpoint: { + p1Consent: 'https://prebidserverurl/openrtb2/auction?querystring=param' + } }); config.setConfig({ s2sConfig }); const cacheResponse = utils.deepClone(RESPONSE_OPENRTB_VIDEO); @@ -1938,7 +2272,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); @@ -1953,12 +2291,17 @@ describe('S2S Adapter', function () { it('add request property pbsBidId with ext.prebid.bidid value', function () { const s2sConfig = Object.assign({}, CONFIG, { - endpoint: 'https://prebidserverurl/openrtb2/auction?querystring=param' + endpoint: { + p1Consent: 'https://prebidserverurl/openrtb2/auction?querystring=param' + } }); 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); @@ -1974,11 +2317,16 @@ describe('S2S Adapter', function () { bidId: '123' }); const s2sConfig = Object.assign({}, CONFIG, { - endpoint: 'https://prebidserverurl/openrtb2/auction?querystring=param' + endpoint: { + p1Consent: 'https://prebidserverurl/openrtb2/auction?querystring=param' + } }); 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); @@ -2015,7 +2363,9 @@ describe('S2S Adapter', function () { config.setConfig({ s2sConfig: Object.assign({}, CONFIG, { - endpoint: 'https://prebid.adnxs.com/pbs/v1/openrtb2/auction' + endpoint: { + p1Consent: 'https://prebid.adnxs.com/pbs/v1/openrtb2/auction' + } }) }); }); @@ -2104,7 +2454,9 @@ describe('S2S Adapter', function () { bidders: ['appnexus'], timeout: 1000, adapter: 'prebidServer', - endpoint: 'https://prebid.adnxs.com/pbs/v1/openrtb2/auction' + endpoint: { + p1Consent: 'https://prebid.adnxs.com/pbs/v1/openrtb2/auction' + } }; config.setConfig({ s2sConfig: options }); @@ -2117,7 +2469,9 @@ describe('S2S Adapter', function () { enabled: true, timeout: 1000, adapter: 's2s', - endpoint: 'https://prebid.adnxs.com/pbs/v1/openrtb2/auction' + endpoint: { + p1Consent: 'https://prebid.adnxs.com/pbs/v1/openrtb2/auction' + } }; config.setConfig({ s2sConfig: options }); @@ -2164,8 +2518,8 @@ describe('S2S Adapter', function () { expect(vendorConfig).to.have.property('adapter', 'prebidServer'); expect(vendorConfig.bidders).to.deep.equal(['appnexus']); expect(vendorConfig.enabled).to.be.true; - expect(vendorConfig).to.have.property('endpoint', 'https://prebid.adnxs.com/pbs/v1/openrtb2/auction'); - expect(vendorConfig).to.have.property('syncEndpoint', 'https://prebid.adnxs.com/pbs/v1/cookie_sync'); + expect(vendorConfig.endpoint).to.deep.equal({p1Consent: 'https://prebid.adnxs.com/pbs/v1/openrtb2/auction', noP1Consent: 'https://prebid.adnxs-simple.com/pbs/v1/openrtb2/auction'}); + expect(vendorConfig.syncEndpoint).to.deep.equal({p1Consent: 'https://prebid.adnxs.com/pbs/v1/cookie_sync', noP1Consent: 'https://prebid.adnxs-simple.com/pbs/v1/cookie_sync'}); expect(vendorConfig).to.have.property('timeout', 750); }); @@ -2185,8 +2539,8 @@ describe('S2S Adapter', function () { expect(vendorConfig).to.have.property('adapter', 'prebidServer'); expect(vendorConfig.bidders).to.deep.equal(['rubicon']); expect(vendorConfig.enabled).to.be.true; - expect(vendorConfig).to.have.property('endpoint', 'https://prebid-server.rubiconproject.com/openrtb2/auction'); - expect(vendorConfig).to.have.property('syncEndpoint', 'https://prebid-server.rubiconproject.com/cookie_sync'); + expect(vendorConfig.endpoint).to.deep.equal({p1Consent: 'https://prebid-server.rubiconproject.com/openrtb2/auction', noP1Consent: 'https://prebid-server.rubiconproject.com/openrtb2/auction'}); + expect(vendorConfig.syncEndpoint).to.deep.equal({p1Consent: 'https://prebid-server.rubiconproject.com/cookie_sync', noP1Consent: 'https://prebid-server.rubiconproject.com/cookie_sync'}); expect(vendorConfig).to.have.property('timeout', 750); }); @@ -2205,8 +2559,8 @@ describe('S2S Adapter', function () { 'bidders': ['rubicon'], 'defaultVendor': 'rubicon', 'enabled': true, - 'endpoint': 'https://prebid-server.rubiconproject.com/openrtb2/auction', - 'syncEndpoint': 'https://prebid-server.rubiconproject.com/cookie_sync', + 'endpoint': {p1Consent: 'https://prebid-server.rubiconproject.com/openrtb2/auction', noP1Consent: 'https://prebid-server.rubiconproject.com/openrtb2/auction'}, + 'syncEndpoint': {p1Consent: 'https://prebid-server.rubiconproject.com/cookie_sync', noP1Consent: 'https://prebid-server.rubiconproject.com/cookie_sync'}, 'timeout': 750 }) }); @@ -2227,8 +2581,8 @@ describe('S2S Adapter', function () { accountId: 'abc', bidders: ['rubicon'], defaultVendor: 'rubicon', - endpoint: 'https://prebid-server.rubiconproject.com/openrtb2/auction', - syncEndpoint: 'https://prebid-server.rubiconproject.com/cookie_sync', + endpoint: {p1Consent: 'https://prebid-server.rubiconproject.com/openrtb2/auction', noP1Consent: 'https://prebid-server.rubiconproject.com/openrtb2/auction'}, + syncEndpoint: {p1Consent: 'https://prebid-server.rubiconproject.com/cookie_sync', noP1Consent: 'https://prebid-server.rubiconproject.com/cookie_sync'}, }) }); @@ -2276,13 +2630,14 @@ describe('S2S Adapter', function () { // Add syncEndpoint so that the request goes to the User Sync endpoint // Modify the bidders property to include an alias for Rubicon adapter - s2sConfig.syncEndpoint = 'https://prebid.adnxs.com/pbs/v1/cookie_sync'; + s2sConfig.syncEndpoint = {p1Consent: '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, @@ -2294,8 +2649,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', @@ -2339,10 +2692,41 @@ 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']); }); + + it('should add cooperative sync flag to cookie_sync request if property is present', function () { + let s2sConfig = utils.deepClone(CONFIG); + s2sConfig.coopSync = false; + s2sConfig.syncEndpoint = {p1Consent: 'https://prebid.adnxs.com/pbs/v1/cookie_sync'}; + + const s2sBidRequest = utils.deepClone(REQUEST); + s2sBidRequest.s2sConfig = s2sConfig; + + let bidRequest = utils.deepClone(BID_REQUESTS); + + adapter.callBids(s2sBidRequest, bidRequest, addBidResponse, done, ajax); + let requestBid = JSON.parse(server.requests[0].requestBody); + + expect(requestBid.coopSync).to.equal(false); + }); + + it('should not add cooperative sync flag to cookie_sync request if property is not present', function () { + let s2sConfig = utils.deepClone(CONFIG); + s2sConfig.syncEndpoint = {p1Consent: 'https://prebid.adnxs.com/pbs/v1/cookie_sync'}; + + const s2sBidRequest = utils.deepClone(REQUEST); + s2sBidRequest.s2sConfig = s2sConfig; + + let bidRequest = utils.deepClone(BID_REQUESTS); + + adapter.callBids(s2sBidRequest, bidRequest, addBidResponse, done, ajax); + let requestBid = JSON.parse(server.requests[0].requestBody); + + expect(requestBid.coopSync).to.be.undefined; + }); }); }); diff --git a/test/spec/modules/prebidmanagerAnalyticsAdapter_spec.js b/test/spec/modules/prebidmanagerAnalyticsAdapter_spec.js index e87be40314c..2505af0c2a7 100644 --- a/test/spec/modules/prebidmanagerAnalyticsAdapter_spec.js +++ b/test/spec/modules/prebidmanagerAnalyticsAdapter_spec.js @@ -1,6 +1,10 @@ -import prebidmanagerAnalytics from 'modules/prebidmanagerAnalyticsAdapter.js'; +import prebidmanagerAnalytics, { + storage +} from 'modules/prebidmanagerAnalyticsAdapter.js'; import {expect} from 'chai'; import {server} from 'test/mocks/xhr.js'; +import * as utils from 'src/utils.js'; + let events = require('src/events'); let constants = require('src/constants.json'); @@ -103,19 +107,18 @@ describe('Prebid Manager Analytics Adapter', function () { }); describe('build utm tag data', function () { + let getDataFromLocalStorageStub; beforeEach(function () { - localStorage.setItem('pm_utm_source', 'utm_source'); - localStorage.setItem('pm_utm_medium', 'utm_medium'); - localStorage.setItem('pm_utm_campaign', 'utm_camp'); - localStorage.setItem('pm_utm_term', ''); - localStorage.setItem('pm_utm_content', ''); + getDataFromLocalStorageStub = sinon.stub(storage, 'getDataFromLocalStorage'); + getDataFromLocalStorageStub.withArgs('pm_utm_source').returns('utm_source'); + getDataFromLocalStorageStub.withArgs('pm_utm_medium').returns('utm_medium'); + getDataFromLocalStorageStub.withArgs('pm_utm_campaign').returns('utm_camp'); + getDataFromLocalStorageStub.withArgs('pm_utm_term').returns(''); + getDataFromLocalStorageStub.withArgs('pm_utm_content').returns(''); + getDataFromLocalStorageStub.withArgs('pm_utm_source').returns('utm_source'); }); afterEach(function () { - localStorage.removeItem('pm_utm_source'); - localStorage.removeItem('pm_utm_medium'); - localStorage.removeItem('pm_utm_campaign'); - localStorage.removeItem('pm_utm_term'); - localStorage.removeItem('pm_utm_content'); + getDataFromLocalStorageStub.restore(); prebidmanagerAnalytics.disableAnalytics() }); it('should build utm data from local storage', function () { @@ -135,4 +138,23 @@ describe('Prebid Manager Analytics Adapter', function () { expect(pmEvents.utmTags.utm_content).to.equal(''); }); }); + + describe('build page info', function () { + afterEach(function () { + prebidmanagerAnalytics.disableAnalytics() + }); + it('should build page info', function () { + prebidmanagerAnalytics.enableAnalytics({ + provider: 'prebidmanager', + options: { + bundleId: 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx' + } + }); + + const pmEvents = JSON.parse(server.requests[0].requestBody.substring(2)); + + expect(pmEvents.pageInfo.domain).to.equal(window.location.hostname); + expect(pmEvents.pageInfo.referrerDomain).to.equal(utils.parseUrl(document.referrer).hostname); + }); + }); }); diff --git a/test/spec/modules/priceFloors_spec.js b/test/spec/modules/priceFloors_spec.js index 0a006e0eb48..548d2789d3e 100644 --- a/test/spec/modules/priceFloors_spec.js +++ b/test/spec/modules/priceFloors_spec.js @@ -23,6 +23,38 @@ describe('the price floors module', function () { let clock; const basicFloorData = { modelVersion: 'basic model', + modelWeight: 10, + modelTimestamp: 1606772895, + currency: 'USD', + schema: { + delimiter: '|', + fields: ['mediaType'] + }, + values: { + 'banner': 1.0, + 'video': 5.0, + '*': 2.5 + } + }; + const basicFloorDataHigh = { + floorMin: 7.0, + modelVersion: 'basic model', + modelWeight: 10, + currency: 'USD', + schema: { + delimiter: '|', + fields: ['mediaType'] + }, + values: { + 'banner': 1.0, + 'video': 5.0, + '*': 2.5 + } + }; + const basicFloorDataLow = { + floorMin: 2.3, + modelVersion: 'basic model', + modelWeight: 10, currency: 'USD', schema: { delimiter: '|', @@ -46,6 +78,32 @@ describe('the price floors module', function () { }, data: basicFloorData } + const minFloorConfigHigh = { + enabled: true, + auctionDelay: 0, + floorMin: 7, + endpoint: {}, + enforcement: { + enforceJS: true, + enforcePBS: false, + floorDeals: false, + bidAdjustment: true + }, + data: basicFloorDataHigh + } + const minFloorConfigLow = { + enabled: true, + auctionDelay: 0, + floorMin: 2.3, + endpoint: {}, + enforcement: { + enforceJS: true, + enforcePBS: false, + floorDeals: false, + bidAdjustment: true + }, + data: basicFloorDataLow + } const basicBidRequest = { bidder: 'rubicon', adUnitCode: 'test_div_1', @@ -130,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: '|', @@ -147,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: '^', @@ -165,22 +227,66 @@ describe('the price floors module', function () { it('selects the right floor for different mediaTypes', function () { // banner with * size (not in rule file so does not do anything) expect(getFirstMatchingFloor({...basicFloorData}, basicBidRequest, {mediaType: 'banner', size: '*'})).to.deep.equal({ + floorMin: 0, + floorRuleValue: 1.0, matchingFloor: 1.0, matchingData: 'banner', matchingRule: 'banner' }); // video with * size (not in rule file so does not do anything) expect(getFirstMatchingFloor({...basicFloorData}, basicBidRequest, {mediaType: 'video', size: '*'})).to.deep.equal({ + floorMin: 0, + floorRuleValue: 5.0, matchingFloor: 5.0, matchingData: 'video', matchingRule: 'video' }); // native (not in the rule list) with * size (not in rule file so does not do anything) expect(getFirstMatchingFloor({...basicFloorData}, basicBidRequest, {mediaType: 'native', size: '*'})).to.deep.equal({ + floorMin: 0, + floorRuleValue: 2.5, matchingFloor: 2.5, matchingData: 'native', matchingRule: '*' }); + // banner with floorMin higher than matching rule + handleSetFloorsConfig({ + ...minFloorConfigHigh + }); + expect(getFirstMatchingFloor({...basicFloorDataHigh}, basicBidRequest, {mediaType: 'banner', size: '*'})).to.deep.equal({ + floorMin: 7, + floorRuleValue: 1.0, + matchingFloor: 7, + matchingData: 'banner', + matchingRule: 'banner' + }); + // banner with floorMin higher than matching rule + handleSetFloorsConfig({ + ...minFloorConfigLow + }); + expect(getFirstMatchingFloor({...basicFloorDataLow}, basicBidRequest, {mediaType: 'video', size: '*'})).to.deep.equal({ + floorMin: 2.3, + floorRuleValue: 5, + matchingFloor: 5, + matchingData: 'video', + matchingRule: 'video' + }); + }); + it('does not alter cached matched input if conversion occurs', function () { + let inputData = {...basicFloorData}; + [0.2, 0.4, 0.6, 0.8].forEach(modifier => { + let result = getFirstMatchingFloor(inputData, basicBidRequest, {mediaType: 'banner', size: '*'}); + // result should always be the same + expect(result).to.deep.equal({ + floorMin: 0, + floorRuleValue: 1.0, + matchingFloor: 1.0, + matchingData: 'banner', + matchingRule: 'banner' + }); + // make sure a post retrieval adjustment does not alter the cached floor + result.matchingFloor = result.matchingFloor * modifier; + }); }); it('selects the right floor for different sizes', function () { let inputFloorData = { @@ -199,24 +305,32 @@ describe('the price floors module', function () { } // banner with 300x250 size expect(getFirstMatchingFloor(inputFloorData, basicBidRequest, {mediaType: 'banner', size: [300, 250]})).to.deep.equal({ + floorMin: 0, + floorRuleValue: 1.1, matchingFloor: 1.1, matchingData: '300x250', matchingRule: '300x250' }); // video with 300x250 size expect(getFirstMatchingFloor(inputFloorData, basicBidRequest, {mediaType: 'video', size: [300, 250]})).to.deep.equal({ + floorMin: 0, + floorRuleValue: 1.1, matchingFloor: 1.1, matchingData: '300x250', matchingRule: '300x250' }); // native (not in the rule list) with 300x600 size expect(getFirstMatchingFloor(inputFloorData, basicBidRequest, {mediaType: 'native', size: [600, 300]})).to.deep.equal({ + floorMin: 0, + floorRuleValue: 4.4, matchingFloor: 4.4, matchingData: '600x300', matchingRule: '600x300' }); // n/a mediaType with a size not in file should go to catch all expect(getFirstMatchingFloor(inputFloorData, basicBidRequest, {mediaType: undefined, size: [1, 1]})).to.deep.equal({ + floorMin: 0, + floorRuleValue: 5.5, matchingFloor: 5.5, matchingData: '1x1', matchingRule: '*' @@ -240,12 +354,16 @@ describe('the price floors module', function () { }; // banner with 300x250 size expect(getFirstMatchingFloor(inputFloorData, basicBidRequest, {mediaType: 'banner', size: [300, 250]})).to.deep.equal({ + floorMin: 0, + floorRuleValue: 1.1, matchingFloor: 1.1, matchingData: 'test_div_1^banner^300x250', matchingRule: 'test_div_1^banner^300x250' }); // video with 300x250 size -> No matching rule so should use default expect(getFirstMatchingFloor(inputFloorData, basicBidRequest, {mediaType: 'video', size: [300, 250]})).to.deep.equal({ + floorMin: 0, + floorRuleValue: 0.5, matchingFloor: 0.5, matchingData: 'test_div_1^video^300x250', matchingRule: undefined @@ -253,6 +371,8 @@ describe('the price floors module', function () { // remove default and should still return the same floor as above since matches are cached delete inputFloorData.default; expect(getFirstMatchingFloor(inputFloorData, basicBidRequest, {mediaType: 'video', size: [300, 250]})).to.deep.equal({ + floorMin: 0, + floorRuleValue: 0.5, matchingFloor: 0.5, matchingData: 'test_div_1^video^300x250', matchingRule: undefined @@ -260,6 +380,8 @@ describe('the price floors module', function () { // update adUnitCode to test_div_2 with weird other params let newBidRequest = { ...basicBidRequest, adUnitCode: 'test_div_2' } expect(getFirstMatchingFloor(inputFloorData, newBidRequest, {mediaType: 'badmediatype', size: [900, 900]})).to.deep.equal({ + floorMin: 0, + floorRuleValue: 3.3, matchingFloor: 3.3, matchingData: 'test_div_2^badmediatype^900x900', matchingRule: 'test_div_2^*^*' @@ -313,7 +435,10 @@ describe('the price floors module', function () { runStandardAuction(); validateBidRequests(false, { skipped: true, + floorMin: undefined, modelVersion: undefined, + modelWeight: undefined, + modelTimestamp: undefined, location: 'noData', skipRate: 0, fetchStatus: undefined, @@ -346,19 +471,61 @@ describe('the price floors module', function () { runStandardAuction([adUnitWithFloors1, adUnitWithFloors2]); validateBidRequests(true, { skipped: false, + floorMin: undefined, modelVersion: 'adUnit Model Version', + modelWeight: 10, + modelTimestamp: 1606772895, location: 'adUnit', skipRate: 0, fetchStatus: undefined, floorProvider: undefined }); }); + it('should use adUnit level data and minFloor should be set', function () { + handleSetFloorsConfig({ + ...minFloorConfigHigh, + data: undefined + }); + // attach floor data onto an adUnit and run an auction + let adUnitWithFloors1 = { + ...getAdUnitMock('adUnit-Div-1'), + floors: { + ...basicFloorData, + modelVersion: 'adUnit Model Version', // change the model name + } + }; + let adUnitWithFloors2 = { + ...getAdUnitMock('adUnit-Div-2'), + floors: { + ...basicFloorData, + values: { + 'banner': 5.0, + '*': 10.4 + } + } + }; + runStandardAuction([adUnitWithFloors1, adUnitWithFloors2]); + validateBidRequests(true, { + skipped: false, + modelVersion: 'adUnit Model Version', + modelWeight: 10, + modelTimestamp: 1606772895, + location: 'adUnit', + skipRate: 0, + floorMin: 7, + fetchStatus: undefined, + floorProvider: undefined + }); + }); it('bidRequests should have getFloor function and flooring meta data when setConfig occurs', function () { handleSetFloorsConfig({...basicFloorConfig, floorProvider: 'floorprovider'}); runStandardAuction(); validateBidRequests(true, { skipped: false, + floorMin: undefined, modelVersion: 'basic model', + modelWeight: 10, + modelTimestamp: 1606772895, location: 'setConfig', skipRate: 0, fetchStatus: undefined, @@ -378,7 +545,10 @@ describe('the price floors module', function () { runStandardAuction(); validateBidRequests(true, { skipped: false, + floorMin: undefined, modelVersion: 'basic model', + modelWeight: 10, + modelTimestamp: 1606772895, location: 'setConfig', skipRate: 0, fetchStatus: undefined, @@ -391,7 +561,10 @@ describe('the price floors module', function () { runStandardAuction(); validateBidRequests(true, { skipped: false, + floorMin: undefined, modelVersion: 'basic model', + modelWeight: 10, + modelTimestamp: 1606772895, location: 'setConfig', skipRate: 0, fetchStatus: undefined, @@ -404,7 +577,10 @@ describe('the price floors module', function () { runStandardAuction(); validateBidRequests(true, { skipped: false, + floorMin: undefined, modelVersion: 'basic model', + modelWeight: 10, + modelTimestamp: 1606772895, location: 'setConfig', skipRate: 0, fetchStatus: undefined, @@ -426,7 +602,10 @@ describe('the price floors module', function () { runStandardAuction(); validateBidRequests(true, { skipped: false, + floorMin: undefined, modelVersion: 'basic model', + modelWeight: 10, + modelTimestamp: 1606772895, location: 'setConfig', skipRate: 50, fetchStatus: undefined, @@ -439,7 +618,10 @@ describe('the price floors module', function () { runStandardAuction(); validateBidRequests(true, { skipped: false, + floorMin: undefined, modelVersion: 'basic model', + modelWeight: 10, + modelTimestamp: 1606772895, location: 'setConfig', skipRate: 10, fetchStatus: undefined, @@ -452,7 +634,10 @@ describe('the price floors module', function () { runStandardAuction(); validateBidRequests(true, { skipped: false, + floorMin: undefined, modelVersion: 'basic model', + modelWeight: 10, + modelTimestamp: 1606772895, location: 'setConfig', skipRate: 0, fetchStatus: undefined, @@ -515,7 +700,10 @@ describe('the price floors module', function () { runStandardAuction(); validateBidRequests(true, { skipped: false, + floorMin: undefined, modelVersion: 'model-1', + modelWeight: 10, + modelTimestamp: undefined, location: 'setConfig', skipRate: 0, fetchStatus: undefined, @@ -527,7 +715,10 @@ describe('the price floors module', function () { runStandardAuction(); validateBidRequests(true, { skipped: false, + floorMin: undefined, modelVersion: 'model-2', + modelWeight: 40, + modelTimestamp: undefined, location: 'setConfig', skipRate: 0, fetchStatus: undefined, @@ -539,7 +730,10 @@ describe('the price floors module', function () { runStandardAuction(); validateBidRequests(true, { skipped: false, + floorMin: undefined, modelVersion: 'model-3', + modelWeight: 50, + modelTimestamp: undefined, location: 'setConfig', skipRate: 0, fetchStatus: undefined, @@ -567,7 +761,10 @@ describe('the price floors module', function () { runStandardAuction(); validateBidRequests(true, { skipped: false, + floorMin: undefined, modelVersion: 'basic model', + modelWeight: 10, + modelTimestamp: 1606772895, location: 'setConfig', skipRate: 0, fetchStatus: undefined, @@ -645,7 +842,10 @@ describe('the price floors module', function () { // the exposedAdUnits should be from the fetch not setConfig level data validateBidRequests(true, { skipped: false, + floorMin: undefined, modelVersion: 'basic model', + modelWeight: 10, + modelTimestamp: 1606772895, location: 'setConfig', skipRate: 0, fetchStatus: 'timeout', @@ -682,7 +882,10 @@ describe('the price floors module', function () { // and fetchStatus is success since fetch worked validateBidRequests(true, { skipped: false, + floorMin: undefined, modelVersion: 'fetch model name', + modelWeight: 10, + modelTimestamp: 1606772895, location: 'fetch', skipRate: 0, fetchStatus: 'success', @@ -718,7 +921,10 @@ describe('the price floors module', function () { // and fetchStatus is success since fetch worked validateBidRequests(true, { skipped: false, + floorMin: undefined, modelVersion: 'fetch model name', + modelWeight: 10, + modelTimestamp: 1606772895, location: 'fetch', skipRate: 0, fetchStatus: 'success', @@ -757,7 +963,10 @@ describe('the price floors module', function () { // and fetchStatus is success since fetch worked validateBidRequests(true, { skipped: false, + floorMin: undefined, modelVersion: 'fetch model name', + modelWeight: 10, + modelTimestamp: 1606772895, location: 'fetch', skipRate: 95, fetchStatus: 'success', @@ -778,7 +987,10 @@ describe('the price floors module', function () { // and fetch failed is true validateBidRequests(true, { skipped: false, + floorMin: undefined, modelVersion: 'basic model', + modelWeight: 10, + modelTimestamp: 1606772895, location: 'setConfig', skipRate: 0, fetchStatus: 'error', @@ -801,7 +1013,10 @@ describe('the price floors module', function () { // and fetchStatus is 'success' but location is setConfig since it had bad data validateBidRequests(true, { skipped: false, + floorMin: undefined, modelVersion: 'basic model', + modelWeight: 10, + modelTimestamp: 1606772895, location: 'setConfig', skipRate: 0, fetchStatus: 'success', @@ -1101,6 +1316,74 @@ 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: { + bidCpmAdjustment: function (bidCpm, bidResponse) { + return bidResponse.cpm * 0.5; + }, + }, + appnexus: { + 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 correctly pick the right attributes if * is passed in and context can be assumed', function () { let inputBidReq = { bidder: 'rubicon', @@ -1256,6 +1539,7 @@ describe('the price floors module', function () { runBidResponse(); expect(returnedBidResponse).to.haveOwnProperty('floorData'); expect(returnedBidResponse.floorData).to.deep.equal({ + floorRuleValue: 0.3, floorValue: 0.3, floorCurrency: 'USD', floorRule: 'banner', @@ -1293,6 +1577,7 @@ describe('the price floors module', function () { expect(returnedBidResponse).to.haveOwnProperty('floorData'); expect(returnedBidResponse.floorData).to.deep.equal({ floorValue: 0.5, + floorRuleValue: 0.5, floorCurrency: 'USD', floorRule: 'banner|300x250', cpmAfterAdjustments: 0.5, @@ -1319,6 +1604,7 @@ describe('the price floors module', function () { }); expect(returnedBidResponse).to.haveOwnProperty('floorData'); expect(returnedBidResponse.floorData).to.deep.equal({ + floorRuleValue: 5.5, floorValue: 5.5, floorCurrency: 'USD', floorRule: 'video|*', diff --git a/test/spec/modules/proxistoreBidAdapter_spec.js b/test/spec/modules/proxistoreBidAdapter_spec.js index 410c3c59fb6..bdcdca06183 100644 --- a/test/spec/modules/proxistoreBidAdapter_spec.js +++ b/test/spec/modules/proxistoreBidAdapter_spec.js @@ -1,55 +1,62 @@ import { expect } from 'chai'; let { spec } = require('modules/proxistoreBidAdapter'); - const BIDDER_CODE = 'proxistore'; describe('ProxistoreBidAdapter', function () { + const consentString = 'BOJ8RZsOJ8RZsABAB8AAAAAZ+A=='; const bidderRequest = { - 'bidderCode': BIDDER_CODE, - 'auctionId': '1025ba77-5463-4877-b0eb-14b205cb9304', - 'bidderRequestId': '10edf38ec1a719', - 'gdprConsent': { - 'gdprApplies': true, - 'consentString': 'CONSENT_STRING', - 'vendorData': { - 'vendorConsents': { - '418': true - } - } - } + bidderCode: BIDDER_CODE, + auctionId: '1025ba77-5463-4877-b0eb-14b205cb9304', + bidderRequestId: '10edf38ec1a719', + gdprConsent: { + apiVersion: 2, + gdprApplies: true, + consentString: consentString, + vendorData: { + vendor: { + consents: { + 418: true, + }, + }, + }, + }, }; let bid = { sizes: [[300, 600]], params: { website: 'example.fr', - language: 'fr' + language: 'fr', + }, + ortb2: { + user: { ext: { data: { segments: [], contextual_categories: {} } } }, }, auctionId: 442133079, bidId: 464646969, - transactionId: 511916005 + transactionId: 511916005, }; describe('isBidRequestValid', function () { it('it should be true if required params are presents and there is no info in the local storage', function () { expect(spec.isBidRequestValid(bid)).to.equal(true); }); - - it('it should be false if the value in the localstorage is less than 5minutes of the actual time', function() { + it('it should be false if the value in the localstorage is less than 5minutes of the actual time', function () { const date = new Date(); - date.setMinutes(date.getMinutes() - 1) - localStorage.setItem(`PX_NoAds_${bid.params.website}`, date) - expect(spec.isBidRequestValid(bid)).to.equal(false); + date.setMinutes(date.getMinutes() - 1); + localStorage.setItem(`PX_NoAds_${bid.params.website}`, date); + expect(spec.isBidRequestValid(bid)).to.equal(true); }); - - it('it should be true if the value in the localstorage is more than 5minutes of the actual time', function() { + it('it should be true if the value in the localstorage is more than 5minutes of the actual time', function () { const date = new Date(); - date.setMinutes(date.getMinutes() - 10) - localStorage.setItem(`PX_NoAds_${bid.params.website}`, date) + date.setMinutes(date.getMinutes() - 10); + localStorage.setItem(`PX_NoAds_${bid.params.website}`, date); expect(spec.isBidRequestValid(bid)).to.equal(true); }); }); - describe('buildRequests', function () { - const url = 'https://abs.proxistore.com/fr/v3/rtb/prebid/multi'; - const request = spec.buildRequests([bid], bidderRequest); + const url = { + cookieBase: 'https://abs.proxistore.com/fr/v3/rtb/prebid/multi', + cookieLess: + 'https://abs.cookieless-proxistore.com/fr/v3/rtb/prebid/multi', + }; + let request = spec.buildRequests([bid], bidderRequest); it('should return a valid object', function () { expect(request).to.be.an('object'); expect(request.method).to.exist; @@ -59,16 +66,45 @@ describe('ProxistoreBidAdapter', function () { it('request method should be POST', function () { expect(request.method).to.equal('POST'); }); - it('should contain a valid url', function () { - expect(request.url).equal(url); - }); it('should have the value consentGiven to true bc we have 418 in the vendor list', function () { const data = JSON.parse(request.data); - - expect(data.gdpr.consentString).equal(bidderRequest.gdprConsent.consentString); + expect(data.gdpr.consentString).equal( + bidderRequest.gdprConsent.consentString + ); expect(data.gdpr.applies).to.be.true; expect(data.gdpr.consentGiven).to.be.true; }); + it('should contain a valid url', function () { + // has gdpr consent + expect(request.url).equal(url.cookieBase); + // doens't have gpdr consent + bidderRequest.gdprConsent.vendorData = null; + + request = spec.buildRequests([bid], bidderRequest); + expect(request.url).equal(url.cookieLess); + + // api v2 + bidderRequest.gdprConsent = { + gdprApplies: true, + allowAuctionWithoutConsent: true, + consentString: consentString, + vendorData: { + vendor: { + consents: { + '418': true + } + }, + }, + apiVersion: 2 + }; + // has gdpr consent + request = spec.buildRequests([bid], bidderRequest); + expect(request.url).equal(url.cookieBase); + + bidderRequest.gdprConsent.vendorData.vendor = {}; + request = spec.buildRequests([bid], bidderRequest); + expect(request.url).equal(url.cookieLess); + }); it('should have a property a length of bids equal to one if there is only one bid', function () { const data = JSON.parse(request.data); expect(data.hasOwnProperty('bids')).to.be.true; @@ -77,51 +113,54 @@ describe('ProxistoreBidAdapter', function () { expect(data.bids[0].hasOwnProperty('id')).to.be.true; expect(data.bids[0].sizes).to.be.an('array'); }); - }); + it('should correctly set bidfloor on imp when getfloor in scope', function () { + let data = JSON.parse(request.data); + expect(data.bids[0].floor).to.be.null; + // make it respond with a non USD floor should not send it + bid.getFloor = function () { + return { currency: 'EUR', floor: 1.0 }; + }; + let req = spec.buildRequests([bid], bidderRequest); + data = JSON.parse(req.data); + expect(data.bids[0].floor).equal(1); + bid.getFloor = function () { + return { currency: 'USD', floor: 1.0 }; + }; + req = spec.buildRequests([bid], bidderRequest); + data = JSON.parse(req.data); + expect(data.bids[0].floor).to.be.null; + }); + }); describe('interpretResponse', function () { - const responses = { - body: - [{ + const emptyResponseParam = { body: [] }; + const fakeResponseParam = { + body: [ + { + ad: '', cpm: 6.25, - creativeId: '48fd47c9-ce35-4fda-804b-17e16c8c36ac', + creativeId: '22c3290b-8cd5-4cd6-8e8c-28a2de180ccd', currency: 'EUR', - dealId: '2019-10_e3ecad8e-d07a-4c90-ad46-cd0f306c8960', + dealId: '2021-03_a63ec55e-b9bb-4ca4-b2c9-f456be67e656', height: 600, netRevenue: true, - requestId: '923756713', + requestId: '3543724f2a033c9', + segments: [], ttl: 10, vastUrl: null, vastXml: null, width: 300, - }] + }, + ], }; - const badResponse = { body: [] }; - const interpretedResponse = spec.interpretResponse(responses, bid)[0]; - it('should send an empty array if body is empty', function () { - expect(spec.interpretResponse(badResponse, bid)).to.be.an('array'); - expect(spec.interpretResponse(badResponse, bid).length).equal(0); - }); - it('should interpret the response correctly if it is valid', function () { - expect(interpretedResponse.cpm).equal(6.25); - expect(interpretedResponse.creativeId).equal('48fd47c9-ce35-4fda-804b-17e16c8c36ac'); - expect(interpretedResponse.currency).equal('EUR'); - expect(interpretedResponse.height).equal(600); - expect(interpretedResponse.width).equal(300); - expect(interpretedResponse.requestId).equal('923756713'); - expect(interpretedResponse.netRevenue).to.be.true; - expect(interpretedResponse.netRevenue).to.be.true; - }); - it('should have a value in the local storage if the response is empty', function() { - spec.interpretResponse(badResponse, bid); - expect(localStorage.getItem(`PX_NoAds_${bid.params.website}`)).to.be.string; - }); - }); - describe('interpretResponse', function () { - it('should aways return an empty array', function () { - expect(spec.getUserSyncs()).to.be.an('array'); - expect(spec.getUserSyncs().length).equal(0); + it('should always return an array', function () { + let response = spec.interpretResponse(emptyResponseParam, bid); + expect(response).to.be.an('array'); + expect(response.length).equal(0); + response = spec.interpretResponse(fakeResponseParam, bid); + expect(response).to.be.an('array'); + expect(response.length).equal(1); }); }); }); diff --git a/test/spec/modules/pubgeniusBidAdapter_spec.js b/test/spec/modules/pubgeniusBidAdapter_spec.js index 52f2e3aeefe..6568f7aa782 100644 --- a/test/spec/modules/pubgeniusBidAdapter_spec.js +++ b/test/spec/modules/pubgeniusBidAdapter_spec.js @@ -1,8 +1,9 @@ import { expect } from 'chai'; import { spec } from 'modules/pubgeniusBidAdapter.js'; -import { deepClone, parseQueryStringParameters } from 'src/utils.js'; import { config } from 'src/config.js'; +import { VIDEO } from 'src/mediaTypes.js'; +import { deepClone, parseQueryStringParameters } from 'src/utils.js'; import { server } from 'test/mocks/xhr.js'; const { @@ -23,8 +24,8 @@ describe('pubGENIUS adapter', () => { }); describe('supportedMediaTypes', () => { - it('should contain only banner', () => { - expect(supportedMediaTypes).to.deep.equal(['banner']); + it('should contain banner and video', () => { + expect(supportedMediaTypes).to.deep.equal(['banner', 'video']); }); }); @@ -77,6 +78,51 @@ describe('pubGENIUS adapter', () => { expect(isBidRequestValid(bid)).to.be.false; }); + + it('should return false without banner or video', () => { + bid.mediaTypes = {}; + + expect(isBidRequestValid(bid)).to.be.false; + }); + + it('should return true with valid video media type', () => { + bid.mediaTypes = { + video: { + context: 'instream', + playerSize: [[100, 100]], + mimes: ['video/mp4'], + protocols: [1], + }, + }; + + expect(isBidRequestValid(bid)).to.be.true; + }); + + it('should return true with valid video params', () => { + bid.params.video = { + placement: 1, + w: 200, + h: 200, + mimes: ['video/mp4'], + protocols: [1], + }; + + expect(isBidRequestValid(bid)).to.be.true; + }); + + it('should return false without video protocols', () => { + bid.mediaTypes = { + video: { + context: 'instream', + playerSize: [[100, 100]], + }, + }; + bid.params.video = { + mimes: ['video/mp4'], + }; + + expect(isBidRequestValid(bid)).to.be.false; + }); }); describe('buildRequests', () => { @@ -122,6 +168,7 @@ describe('pubGENIUS adapter', () => { bidderCode: 'pubgenius', bidderRequestId: 'fakebidderrequestid', refererInfo: {}, + timeout: 1200, }; expectedRequest = { @@ -142,14 +189,14 @@ describe('pubGENIUS adapter', () => { tmax: 1200, ext: { pbadapter: { - version: '1.0.0', + version: '1.1.0', }, }, }, }; config.setConfig({ - bidderTimeout: 1200, + bidderTimeout: 1000, pageUrl: undefined, coppa: undefined, }); @@ -178,8 +225,8 @@ describe('pubGENIUS adapter', () => { expect(buildRequests([bidRequest, bidRequest1], bidderRequest)).to.deep.equal(expectedRequest); }); - it('should take bid floor in bidder params', () => { - bidRequest.params.bidFloor = 0.5; + it('should take bid floor from getFloor interface', () => { + bidRequest.getFloor = () => ({ floor: 0.5, currency: 'USD' }); expectedRequest.data.imp[0].bidfloor = 0.5; expect(buildRequests([bidRequest], bidderRequest)).to.deep.equal(expectedRequest); @@ -313,6 +360,47 @@ describe('pubGENIUS adapter', () => { expect(buildRequests([bidRequest], bidderRequest)).to.deep.equal(expectedRequest); }); + + it('should build video imp', () => { + bidRequest.mediaTypes = { + video: { + context: 'instream', + playerSize: [[200, 100]], + mimes: ['video/mp4'], + protocols: [2, 3], + api: [1, 2], + playbackmethod: [3, 4], + maxduration: 10, + linearity: 1, + }, + }; + bidRequest.params.video = { + minduration: 5, + maxduration: 100, + skip: 1, + skipafter: 1, + startdelay: -1, + }; + + delete expectedRequest.data.imp[0].banner; + expectedRequest.data.imp[0].video = { + mimes: ['video/mp4'], + minduration: 5, + maxduration: 100, + protocols: [2, 3], + w: 200, + h: 100, + startdelay: -1, + placement: 1, + skip: 1, + skipafter: 1, + playbackmethod: [3, 4], + api: [1, 2], + linearity: 1, + }; + + expect(buildRequests([bidRequest], bidderRequest)).to.deep.equal(expectedRequest); + }); }); describe('interpretResponse', () => { @@ -369,6 +457,29 @@ describe('pubGENIUS adapter', () => { it('should interpret no bids', () => { expect(interpretResponse({ body: {} })).to.deep.equal([]); }); + + it('should interpret video response', () => { + serverResponse.body.seatbid[0].bid[0] = { + ...serverResponse.body.seatbid[0].bid[0], + nurl: 'http://vasturl/cache?id=x', + ext: { + pbadapter: { + mediaType: 'video', + cacheKey: 'x', + }, + }, + }; + + delete expectedBidResponse.ad; + expectedBidResponse = { + ...expectedBidResponse, + vastUrl: 'http://vasturl/cache?id=x', + vastXml: 'fake_creative', + mediaType: VIDEO, + }; + + expect(interpretResponse(serverResponse)).to.deep.equal([expectedBidResponse]); + }); }); describe('getUserSyncs', () => { diff --git a/test/spec/modules/pubmaticAnalyticsAdapter_spec.js b/test/spec/modules/pubmaticAnalyticsAdapter_spec.js index 537ba4483cf..11de7b13208 100755 --- a/test/spec/modules/pubmaticAnalyticsAdapter_spec.js +++ b/test/spec/modules/pubmaticAnalyticsAdapter_spec.js @@ -11,6 +11,12 @@ let events = require('src/events'); let ajax = require('src/ajax'); let utils = require('src/utils'); +const DEFAULT_USER_AGENT = window.navigator.userAgent; +const MOBILE_USER_AGENT = 'Mozilla/5.0 (iPhone; CPU iPhone OS 13_3_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0.5 Mobile/15E148 Safari/604.1'; +const setUADefault = () => { window.navigator.__defineGetter__('userAgent', function () { return DEFAULT_USER_AGENT }) }; +const setUAMobile = () => { window.navigator.__defineGetter__('userAgent', function () { return MOBILE_USER_AGENT }) }; +const setUANull = () => { window.navigator.__defineGetter__('userAgent', function () { return null }) }; + const { EVENTS: { AUCTION_INIT, @@ -247,6 +253,7 @@ describe('pubmatic analytics adapter', function () { let clock; beforeEach(function () { + setUADefault(); sandbox = sinon.sandbox.create(); xhr = sandbox.useFakeXMLHttpRequest(); @@ -316,7 +323,7 @@ describe('pubmatic analytics adapter', function () { clock.tick(2000 + 1000); expect(requests.length).to.equal(3); // 1 logger and 2 win-tracker let request = requests[2]; // logger is executed late, trackers execute first - expect(request.url).to.equal('https://t.pubmatic.com/wl?pubid=9999&gdEn=1'); + expect(request.url).to.equal('https://t.pubmatic.com/wl?pubid=9999'); let data = getLoggerJsonFromRequest(request.requestBody); expect(data.pubid).to.equal('9999'); expect(data.pid).to.equal('1111'); @@ -326,8 +333,6 @@ describe('pubmatic analytics adapter', function () { expect(data.purl).to.equal('http://www.test.com/page.html'); expect(data.orig).to.equal('www.test.com'); expect(data.tst).to.equal(1519767016); - expect(data.cns).to.equal('here-goes-gdpr-consent-string'); - expect(data.gdpr).to.equal(1); expect(data.tgid).to.equal(15); expect(data.s).to.be.an('array'); expect(data.s.length).to.equal(2); @@ -423,7 +428,7 @@ describe('pubmatic analytics adapter', function () { clock.tick(2000 + 1000); expect(requests.length).to.equal(3); // 1 logger and 2 win-tracker let request = requests[2]; // logger is executed late, trackers execute first - expect(request.url).to.equal('https://t.pubmatic.com/wl?pubid=9999&gdEn=1'); + expect(request.url).to.equal('https://t.pubmatic.com/wl?pubid=9999'); let data = getLoggerJsonFromRequest(request.requestBody); expect(data.pubid).to.equal('9999'); expect(data.pid).to.equal('1111'); @@ -490,7 +495,7 @@ describe('pubmatic analytics adapter', function () { clock.tick(2000 + 1000); expect(requests.length).to.equal(3); // 1 logger and 2 win-tracker let request = requests[2]; // logger is executed late, trackers execute first - expect(request.url).to.equal('https://t.pubmatic.com/wl?pubid=9999&gdEn=1'); + expect(request.url).to.equal('https://t.pubmatic.com/wl?pubid=9999'); let data = getLoggerJsonFromRequest(request.requestBody); expect(data.pubid).to.equal('9999'); expect(data.pid).to.equal('1111'); @@ -645,6 +650,7 @@ describe('pubmatic analytics adapter', function () { }); it('Logger: currency conversion check', function() { + setUANull(); setConfig({ adServerCurrency: 'JPY', rates: { @@ -672,7 +678,7 @@ describe('pubmatic analytics adapter', function () { clock.tick(2000 + 1000); expect(requests.length).to.equal(3); // 1 logger and 2 win-tracker let request = requests[2]; // logger is executed late, trackers execute first - expect(request.url).to.equal('https://t.pubmatic.com/wl?pubid=9999&gdEn=1'); + expect(request.url).to.equal('https://t.pubmatic.com/wl?pubid=9999'); let data = getLoggerJsonFromRequest(request.requestBody); expect(data.s[1].sn).to.equal('/19968336/header-bid-tag-1'); expect(data.s[1].sz).to.deep.equal(['1000x300', '970x250', '728x90']); @@ -697,6 +703,212 @@ describe('pubmatic analytics adapter', function () { expect(data.s[1].ps[0].af).to.equal('banner'); expect(data.s[1].ps[0].ocpm).to.equal(100); expect(data.s[1].ps[0].ocry).to.equal('JPY'); + expect(data.dvc).to.deep.equal({'plt': 3}); + }); + + it('Logger: regexPattern in bid.params', function() { + setUAMobile(); + const BID_REQUESTED_COPY = utils.deepClone(MOCK.BID_REQUESTED); + BID_REQUESTED_COPY.bids[1].params.regexPattern = '*'; + events.emit(AUCTION_INIT, MOCK.AUCTION_INIT); + events.emit(BID_REQUESTED, BID_REQUESTED_COPY); + events.emit(BID_RESPONSE, MOCK.BID_RESPONSE[0]); + events.emit(BID_RESPONSE, BID2); + events.emit(BIDDER_DONE, MOCK.BIDDER_DONE); + events.emit(AUCTION_END, MOCK.AUCTION_END); + events.emit(SET_TARGETING, MOCK.SET_TARGETING); + events.emit(BID_WON, MOCK.BID_WON[0]); + events.emit(BID_WON, MOCK.BID_WON[1]); + + clock.tick(2000 + 1000); + expect(requests.length).to.equal(3); // 1 logger and 2 win-tracker + let request = requests[2]; // logger is executed late, trackers execute first + expect(request.url).to.equal('https://t.pubmatic.com/wl?pubid=9999'); + let data = getLoggerJsonFromRequest(request.requestBody); + expect(data.s[1].sn).to.equal('/19968336/header-bid-tag-1'); + expect(data.s[1].sz).to.deep.equal(['1000x300', '970x250', '728x90']); + expect(data.s[1].ps).to.be.an('array'); + expect(data.s[1].ps.length).to.equal(1); + expect(data.s[1].ps[0].pn).to.equal('pubmatic'); + expect(data.s[1].ps[0].bidid).to.equal('3bd4ebb1c900e2'); + expect(data.s[1].ps[0].db).to.equal(0); + expect(data.s[1].ps[0].kgpv).to.equal('*'); + expect(data.s[1].ps[0].kgpsv).to.equal('this-is-a-kgpv'); + expect(data.s[1].ps[0].psz).to.equal('728x90'); + expect(data.s[1].ps[0].eg).to.equal(1.52); + expect(data.s[1].ps[0].en).to.equal(1.52); + expect(data.s[1].ps[0].di).to.equal('the-deal-id'); + expect(data.s[1].ps[0].dc).to.equal('PMP'); + expect(data.s[1].ps[0].mi).to.equal('matched-impression'); + expect(data.s[1].ps[0].l1).to.equal(3214); + expect(data.s[1].ps[0].l2).to.equal(0); + expect(data.s[1].ps[0].ss).to.equal(1); + expect(data.s[1].ps[0].t).to.equal(0); + expect(data.s[1].ps[0].wb).to.equal(0); // bidPriceUSD is not getting set as currency module is not added, so unable to set wb to 1 + expect(data.s[1].ps[0].af).to.equal('banner'); + expect(data.s[1].ps[0].ocpm).to.equal(1.52); + expect(data.s[1].ps[0].ocry).to.equal('USD'); + expect(data.dvc).to.deep.equal({'plt': 2}); + // respective tracker slot + let firstTracker = requests[1].url; + expect(firstTracker.split('?')[0]).to.equal('https://t.pubmatic.com/wt'); + data = {}; + firstTracker.split('?')[1].split('&').map(e => e.split('=')).forEach(e => data[e[0]] = e[1]); + expect(data.kgpv).to.equal('*'); + }); + + it('Logger: regexPattern in bid.bidResponse', function() { + const BID2_COPY = utils.deepClone(BID2); + BID2_COPY.regexPattern = '*'; + events.emit(AUCTION_INIT, MOCK.AUCTION_INIT); + 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, BID2_COPY); + events.emit(BIDDER_DONE, MOCK.BIDDER_DONE); + events.emit(AUCTION_END, MOCK.AUCTION_END); + events.emit(SET_TARGETING, MOCK.SET_TARGETING); + events.emit(BID_WON, MOCK.BID_WON[0]); + events.emit(BID_WON, Object.assign({}, BID2_COPY, { + 'status': 'rendered' + })); + + clock.tick(2000 + 1000); + expect(requests.length).to.equal(3); // 1 logger and 2 win-tracker + let request = requests[2]; // logger is executed late, trackers execute first + expect(request.url).to.equal('https://t.pubmatic.com/wl?pubid=9999'); + let data = getLoggerJsonFromRequest(request.requestBody); + expect(data.s[1].sn).to.equal('/19968336/header-bid-tag-1'); + expect(data.s[1].sz).to.deep.equal(['1000x300', '970x250', '728x90']); + expect(data.s[1].ps).to.be.an('array'); + expect(data.s[1].ps.length).to.equal(1); + expect(data.s[1].ps[0].pn).to.equal('pubmatic'); + expect(data.s[1].ps[0].bidid).to.equal('3bd4ebb1c900e2'); + expect(data.s[1].ps[0].db).to.equal(0); + expect(data.s[1].ps[0].kgpv).to.equal('*'); + expect(data.s[1].ps[0].kgpsv).to.equal('this-is-a-kgpv'); + expect(data.s[1].ps[0].psz).to.equal('728x90'); + expect(data.s[1].ps[0].eg).to.equal(1.52); + expect(data.s[1].ps[0].en).to.equal(1.52); + expect(data.s[1].ps[0].di).to.equal('the-deal-id'); + expect(data.s[1].ps[0].dc).to.equal('PMP'); + expect(data.s[1].ps[0].mi).to.equal('matched-impression'); + expect(data.s[1].ps[0].l1).to.equal(3214); + expect(data.s[1].ps[0].l2).to.equal(0); + expect(data.s[1].ps[0].ss).to.equal(1); + expect(data.s[1].ps[0].t).to.equal(0); + expect(data.s[1].ps[0].wb).to.equal(0); // bidPriceUSD is not getting set as currency module is not added, so unable to set wb to 1 + expect(data.s[1].ps[0].af).to.equal('banner'); + expect(data.s[1].ps[0].ocpm).to.equal(1.52); + expect(data.s[1].ps[0].ocry).to.equal('USD'); + expect(data.dvc).to.deep.equal({'plt': 1}); + // respective tracker slot + let firstTracker = requests[1].url; + expect(firstTracker.split('?')[0]).to.equal('https://t.pubmatic.com/wt'); + data = {}; + firstTracker.split('?')[1].split('&').map(e => e.split('=')).forEach(e => data[e[0]] = e[1]); + expect(data.kgpv).to.equal('*'); + }); + + it('Logger: regexPattern in bid.params', function() { + const BID_REQUESTED_COPY = utils.deepClone(MOCK.BID_REQUESTED); + BID_REQUESTED_COPY.bids[1].params.regexPattern = '*'; + events.emit(AUCTION_INIT, MOCK.AUCTION_INIT); + events.emit(BID_REQUESTED, BID_REQUESTED_COPY); + events.emit(BID_RESPONSE, MOCK.BID_RESPONSE[0]); + events.emit(BID_RESPONSE, BID2); + events.emit(BIDDER_DONE, MOCK.BIDDER_DONE); + events.emit(AUCTION_END, MOCK.AUCTION_END); + events.emit(SET_TARGETING, MOCK.SET_TARGETING); + events.emit(BID_WON, MOCK.BID_WON[0]); + events.emit(BID_WON, MOCK.BID_WON[1]); + + clock.tick(2000 + 1000); + expect(requests.length).to.equal(3); // 1 logger and 2 win-tracker + let request = requests[2]; // logger is executed late, trackers execute first + expect(request.url).to.equal('https://t.pubmatic.com/wl?pubid=9999'); + let data = getLoggerJsonFromRequest(request.requestBody); + expect(data.s[1].sn).to.equal('/19968336/header-bid-tag-1'); + expect(data.s[1].sz).to.deep.equal(['1000x300', '970x250', '728x90']); + expect(data.s[1].ps).to.be.an('array'); + expect(data.s[1].ps.length).to.equal(1); + expect(data.s[1].ps[0].pn).to.equal('pubmatic'); + expect(data.s[1].ps[0].bidid).to.equal('3bd4ebb1c900e2'); + expect(data.s[1].ps[0].db).to.equal(0); + expect(data.s[1].ps[0].kgpv).to.equal('*'); + expect(data.s[1].ps[0].kgpsv).to.equal('this-is-a-kgpv'); + expect(data.s[1].ps[0].psz).to.equal('728x90'); + expect(data.s[1].ps[0].eg).to.equal(1.52); + expect(data.s[1].ps[0].en).to.equal(1.52); + expect(data.s[1].ps[0].di).to.equal('the-deal-id'); + expect(data.s[1].ps[0].dc).to.equal('PMP'); + expect(data.s[1].ps[0].mi).to.equal('matched-impression'); + expect(data.s[1].ps[0].l1).to.equal(3214); + expect(data.s[1].ps[0].l2).to.equal(0); + expect(data.s[1].ps[0].ss).to.equal(1); + expect(data.s[1].ps[0].t).to.equal(0); + expect(data.s[1].ps[0].wb).to.equal(0); // bidPriceUSD is not getting set as currency module is not added, so unable to set wb to 1 + expect(data.s[1].ps[0].af).to.equal('banner'); + expect(data.s[1].ps[0].ocpm).to.equal(1.52); + expect(data.s[1].ps[0].ocry).to.equal('USD'); + // respective tracker slot + let firstTracker = requests[1].url; + expect(firstTracker.split('?')[0]).to.equal('https://t.pubmatic.com/wt'); + data = {}; + firstTracker.split('?')[1].split('&').map(e => e.split('=')).forEach(e => data[e[0]] = e[1]); + expect(data.kgpv).to.equal('*'); + }); + + it('Logger: regexPattern in bid.bidResponse', function() { + const BID2_COPY = utils.deepClone(BID2); + BID2_COPY.regexPattern = '*'; + events.emit(AUCTION_INIT, MOCK.AUCTION_INIT); + 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, BID2_COPY); + events.emit(BIDDER_DONE, MOCK.BIDDER_DONE); + events.emit(AUCTION_END, MOCK.AUCTION_END); + events.emit(SET_TARGETING, MOCK.SET_TARGETING); + events.emit(BID_WON, MOCK.BID_WON[0]); + events.emit(BID_WON, Object.assign({}, BID2_COPY, { + 'status': 'rendered' + })); + + clock.tick(2000 + 1000); + expect(requests.length).to.equal(3); // 1 logger and 2 win-tracker + let request = requests[2]; // logger is executed late, trackers execute first + expect(request.url).to.equal('https://t.pubmatic.com/wl?pubid=9999'); + let data = getLoggerJsonFromRequest(request.requestBody); + expect(data.s[1].sn).to.equal('/19968336/header-bid-tag-1'); + expect(data.s[1].sz).to.deep.equal(['1000x300', '970x250', '728x90']); + expect(data.s[1].ps).to.be.an('array'); + expect(data.s[1].ps.length).to.equal(1); + expect(data.s[1].ps[0].pn).to.equal('pubmatic'); + expect(data.s[1].ps[0].bidid).to.equal('3bd4ebb1c900e2'); + expect(data.s[1].ps[0].db).to.equal(0); + expect(data.s[1].ps[0].kgpv).to.equal('*'); + expect(data.s[1].ps[0].kgpsv).to.equal('this-is-a-kgpv'); + expect(data.s[1].ps[0].psz).to.equal('728x90'); + expect(data.s[1].ps[0].eg).to.equal(1.52); + expect(data.s[1].ps[0].en).to.equal(1.52); + expect(data.s[1].ps[0].di).to.equal('the-deal-id'); + expect(data.s[1].ps[0].dc).to.equal('PMP'); + expect(data.s[1].ps[0].mi).to.equal('matched-impression'); + expect(data.s[1].ps[0].l1).to.equal(3214); + expect(data.s[1].ps[0].l2).to.equal(0); + expect(data.s[1].ps[0].ss).to.equal(1); + expect(data.s[1].ps[0].t).to.equal(0); + expect(data.s[1].ps[0].wb).to.equal(0); // bidPriceUSD is not getting set as currency module is not added, so unable to set wb to 1 + expect(data.s[1].ps[0].af).to.equal('banner'); + expect(data.s[1].ps[0].ocpm).to.equal(1.52); + expect(data.s[1].ps[0].ocry).to.equal('USD'); + // respective tracker slot + let firstTracker = requests[1].url; + expect(firstTracker.split('?')[0]).to.equal('https://t.pubmatic.com/wt'); + data = {}; + firstTracker.split('?')[1].split('&').map(e => e.split('=')).forEach(e => data[e[0]] = e[1]); + expect(data.kgpv).to.equal('*'); }); }); }); diff --git a/test/spec/modules/pubmaticBidAdapter_spec.js b/test/spec/modules/pubmaticBidAdapter_spec.js index 0f51a8df61c..e8948fdc2d3 100644 --- a/test/spec/modules/pubmaticBidAdapter_spec.js +++ b/test/spec/modules/pubmaticBidAdapter_spec.js @@ -796,18 +796,24 @@ describe('PubMatic adapter', function () { describe('Request formation', function () { it('buildRequests function should not modify original bidRequests object', function () { let originalBidRequests = utils.deepClone(bidRequests); - let request = spec.buildRequests(bidRequests); + let request = spec.buildRequests(bidRequests, { + auctionId: 'new-auction-id' + }); 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); + let request = spec.buildRequests(nativeBidRequests, { + auctionId: 'new-auction-id' + }); expect(nativeBidRequests).to.deep.equal(originalBidRequests); }); it('Endpoint checking', function () { - let request = spec.buildRequests(bidRequests); + let request = spec.buildRequests(bidRequests, { + auctionId: 'new-auction-id' + }); expect(request.url).to.equal('https://hbopenbid.pubmatic.com/translator?source=prebid-client'); expect(request.method).to.equal('POST'); }); @@ -823,7 +829,9 @@ describe('PubMatic adapter', function () { }); it('test flag not sent when pubmaticTest=true is absent in page url', function() { - let request = spec.buildRequests(bidRequests); + let request = spec.buildRequests(bidRequests, { + auctionId: 'new-auction-id' + }); let data = JSON.parse(request.data); expect(data.test).to.equal(undefined); }); @@ -833,13 +841,17 @@ describe('PubMatic adapter', function () { xit('test flag set to 1 when pubmaticTest=true is present in page url', function() { window.location.href += '#pubmaticTest=true'; // now all the test cases below will have window.location.href with #pubmaticTest=true - let request = spec.buildRequests(bidRequests); + let request = spec.buildRequests(bidRequests, { + auctionId: 'new-auction-id' + }); let data = JSON.parse(request.data); expect(data.test).to.equal(1); }); it('Request params check', function () { - let request = spec.buildRequests(bidRequests); + 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 @@ -882,7 +894,9 @@ describe('PubMatic adapter', function () { }; return config[key]; }); - let request = spec.buildRequests(bidRequests); + let request = spec.buildRequests(bidRequests, { + auctionId: 'new-auction-id' + }); let data = JSON.parse(request.data); expect(data.site.content).to.deep.equal(content); sandbox.restore(); @@ -898,7 +912,9 @@ describe('PubMatic adapter', function () { }; return config[key]; }); - let request = spec.buildRequests(bidRequests); + let request = spec.buildRequests(bidRequests, { + auctionId: 'new-auction-id' + }); let data = JSON.parse(request.data); expect(data.device.js).to.equal(1); expect(data.device.dnt).to.equal((navigator.doNotTrack == 'yes' || navigator.doNotTrack == '1' || navigator.msDoNotTrack == '1') ? 1 : 0); @@ -920,7 +936,9 @@ describe('PubMatic adapter', function () { }; return config[key]; }); - let request = spec.buildRequests(bidRequests); + let request = spec.buildRequests(bidRequests, { + auctionId: 'new-auction-id' + }); let data = JSON.parse(request.data); expect(data.device.js).to.equal(1); expect(data.device.dnt).to.equal((navigator.doNotTrack == 'yes' || navigator.doNotTrack == '1' || navigator.msDoNotTrack == '1') ? 1 : 0); @@ -942,7 +960,9 @@ describe('PubMatic adapter', function () { }; return config[key]; }); - let request = spec.buildRequests(bidRequests); + let request = spec.buildRequests(bidRequests, { + auctionId: 'new-auction-id' + }); let data = JSON.parse(request.data); expect(data.app.bundle).to.equal('org.prebid.mobile.demoapp'); expect(data.app.domain).to.equal('prebid.org'); @@ -967,7 +987,9 @@ describe('PubMatic adapter', function () { }; return config[key]; }); - let request = spec.buildRequests(bidRequests); + let request = spec.buildRequests(bidRequests, { + auctionId: 'new-auction-id' + }); let data = JSON.parse(request.data); expect(data.app.bundle).to.equal('org.prebid.mobile.demoapp'); expect(data.app.domain).to.equal('prebid.org'); @@ -997,7 +1019,9 @@ describe('PubMatic adapter', function () { }; return config[key]; }); - let request = spec.buildRequests(bidRequests); + let request = spec.buildRequests(bidRequests, { + auctionId: 'new-auction-id' + }); let data = JSON.parse(request.data); expect(data.app.bundle).to.equal('org.prebid.mobile.demoapp'); expect(data.app.domain).to.equal('prebid.org'); @@ -1010,7 +1034,9 @@ describe('PubMatic adapter', function () { it('Request params check: without adSlot', function () { delete bidRequests[0].params.adSlot; - let request = spec.buildRequests(bidRequests); + 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 @@ -1068,7 +1094,9 @@ describe('PubMatic adapter', function () { } ]; /* case 1 - size passed in adslot */ - let request = spec.buildRequests(bidRequests); + let request = spec.buildRequests(bidRequests, { + auctionId: 'new-auction-id' + }); let data = JSON.parse(request.data); expect(data.imp[0].banner.w).to.equal(300); // width @@ -1081,7 +1109,9 @@ describe('PubMatic adapter', function () { sizes: [[300, 600], [300, 250]] } }; - request = spec.buildRequests(bidRequests); + request = spec.buildRequests(bidRequests, { + auctionId: 'new-auction-id' + }); data = JSON.parse(request.data); expect(data.imp[0].banner.w).to.equal(300); // width @@ -1095,7 +1125,9 @@ describe('PubMatic adapter', function () { sizes: [[300, 250], [300, 600]] } }; - request = spec.buildRequests(bidRequests); + request = spec.buildRequests(bidRequests, { + auctionId: 'new-auction-id' + }); data = JSON.parse(request.data); expect(data.imp[0].banner.w).to.equal(300); // width @@ -1163,7 +1195,9 @@ describe('PubMatic adapter', function () { output: imp[0] and imp[1] both use currency specified in bidRequests[0].params.currency */ - let request = spec.buildRequests(multipleBidRequests); + let request = spec.buildRequests(multipleBidRequests, { + auctionId: 'new-auction-id' + }); let data = JSON.parse(request.data); expect(data.imp[0].bidfloorcur).to.equal(bidRequests[0].params.currency); @@ -1175,7 +1209,9 @@ describe('PubMatic adapter', function () { */ delete multipleBidRequests[1].params.currency; - request = spec.buildRequests(multipleBidRequests); + request = spec.buildRequests(multipleBidRequests, { + auctionId: 'new-auction-id' + }); data = JSON.parse(request.data); expect(data.imp[0].bidfloorcur).to.equal(bidRequests[0].params.currency); expect(data.imp[1].bidfloorcur).to.equal(bidRequests[0].params.currency); @@ -1186,7 +1222,9 @@ describe('PubMatic adapter', function () { */ delete multipleBidRequests[0].params.currency; - request = spec.buildRequests(multipleBidRequests); + request = spec.buildRequests(multipleBidRequests, { + auctionId: 'new-auction-id' + }); data = JSON.parse(request.data); expect(data.imp[0].bidfloorcur).to.equal('USD'); expect(data.imp[1].bidfloorcur).to.equal('USD'); @@ -1197,12 +1235,46 @@ describe('PubMatic adapter', function () { */ multipleBidRequests[1].params.currency = 'AUD'; - request = spec.buildRequests(multipleBidRequests); + request = spec.buildRequests(multipleBidRequests, { + auctionId: 'new-auction-id' + }); data = JSON.parse(request.data); expect(data.imp[0].bidfloorcur).to.equal('USD'); expect(data.imp[1].bidfloorcur).to.equal('USD'); }); + it('Pass auctiondId as wiid if wiid is not passed in params', function () { + let bidRequest = { + auctionId: 'new-auction-id' + }; + delete bidRequests[0].params.wiid; + let request = spec.buildRequests(bidRequests, bidRequest); + 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.page).to.equal(bidRequests[0].params.kadpageurl); // forced pageURL + expect(data.site.publisher.id).to.equal(bidRequests[0].params.publisherId); // publisher Id + expect(data.user.yob).to.equal(parseInt(bidRequests[0].params.yob)); // YOB + expect(data.user.gender).to.equal(bidRequests[0].params.gender); // Gender + expect(data.device.geo.lat).to.equal(parseFloat(bidRequests[0].params.lat)); // Latitude + expect(data.device.geo.lon).to.equal(parseFloat(bidRequests[0].params.lon)); // Lognitude + expect(data.user.geo.lat).to.equal(parseFloat(bidRequests[0].params.lat)); // Latitude + expect(data.user.geo.lon).to.equal(parseFloat(bidRequests[0].params.lon)); // Lognitude + expect(data.ext.wrapper.wv).to.equal($$REPO_AND_VERSION$$); // Wrapper Version + expect(data.ext.wrapper.transactionId).to.equal(bidRequests[0].transactionId); // Prebid TransactionId + expect(data.ext.wrapper.wiid).to.equal('new-auction-id'); // OpenWrap: Wrapper Impression ID + expect(data.ext.wrapper.profile).to.equal(parseInt(bidRequests[0].params.profId)); // OpenWrap: Wrapper Profile ID + expect(data.ext.wrapper.version).to.equal(parseInt(bidRequests[0].params.verId)); // OpenWrap: Wrapper Profile Version ID + + expect(data.imp[0].id).to.equal(bidRequests[0].bidId); // Prebid bid id is passed as id + expect(data.imp[0].bidfloor).to.equal(parseFloat(bidRequests[0].params.kadfloor)); // kadfloor + expect(data.imp[0].tagid).to.equal('/15671365/DMDemo'); // tagid + expect(data.imp[0].banner.w).to.equal(300); // width + expect(data.imp[0].banner.h).to.equal(250); // height + expect(data.imp[0].ext.pmZoneId).to.equal(bidRequests[0].params.pmzoneid.split(',').slice(0, 50).map(id => id.trim()).join()); // pmzoneid + }); + it('Request params check with GDPR Consent', function () { let bidRequest = { gdprConsent: { @@ -1276,6 +1348,214 @@ describe('PubMatic adapter', function () { expect(data2.regs).to.equal(undefined);// USP/CCPAs }); + describe('FPD', function() { + let newRequest; + + it('ortb2.site should be merged in the request', function() { + let sandbox = sinon.sandbox.create(); + sandbox.stub(config, 'getConfig').callsFake(key => { + const config = { + 'ortb2': { + site: { + domain: 'page.example.com', + cat: ['IAB2'], + sectioncat: ['IAB2-2'] + } + } + }; + return config[key]; + }); + const request = spec.buildRequests(bidRequests, {}); + let data = JSON.parse(request.data); + expect(data.site.domain).to.equal('page.example.com'); + expect(data.site.cat).to.deep.equal(['IAB2']); + expect(data.site.sectioncat).to.deep.equal(['IAB2-2']); + sandbox.restore(); + }); + + it('ortb2.user should be merged in the request', function() { + let sandbox = sinon.sandbox.create(); + sandbox.stub(config, 'getConfig').callsFake(key => { + const config = { + 'ortb2': { + user: { + yob: 1985 + } + } + }; + return config[key]; + }); + const request = spec.buildRequests(bidRequests, {}); + let data = JSON.parse(request.data); + expect(data.user.yob).to.equal(1985); + sandbox.restore(); + }); + + describe('ortb2Imp', function() { + describe('ortb2Imp.ext.data.pbadslot', function() { + beforeEach(function () { + if (bidRequests[0].hasOwnProperty('ortb2Imp')) { + delete bidRequests[0].ortb2Imp; + } + }); + + it('should not send if imp[].ext.data object is invalid', function() { + bidRequests[0].ortb2Imp = { + ext: {} + }; + const request = spec.buildRequests(bidRequests, {}); + let data = JSON.parse(request.data); + expect(data.imp[0].ext).to.not.have.property('data'); + }); + + it('should not send if imp[].ext.data.pbadslot is undefined', function() { + bidRequests[0].ortb2Imp = { + ext: { + data: { + } + } + }; + const request = spec.buildRequests(bidRequests, {}); + let data = JSON.parse(request.data); + if (data.imp[0].ext.data) { + expect(data.imp[0].ext.data).to.not.have.property('pbadslot'); + } else { + expect(data.imp[0].ext).to.not.have.property('data'); + } + }); + + it('should not send if imp[].ext.data.pbadslot is empty string', function() { + bidRequests[0].ortb2Imp = { + ext: { + data: { + pbadslot: '' + } + } + }; + const request = spec.buildRequests(bidRequests, {}); + let data = JSON.parse(request.data); + if (data.imp[0].ext.data) { + expect(data.imp[0].ext.data).to.not.have.property('pbadslot'); + } else { + expect(data.imp[0].ext).to.not.have.property('data'); + } + }); + + it('should send if imp[].ext.data.pbadslot is string', function() { + bidRequests[0].ortb2Imp = { + ext: { + data: { + pbadslot: 'abcd' + } + } + }; + const request = spec.buildRequests(bidRequests, {}); + let data = JSON.parse(request.data); + expect(data.imp[0].ext.data).to.have.property('pbadslot'); + expect(data.imp[0].ext.data.pbadslot).to.equal('abcd'); + }); + }); + + describe('ortb2Imp.ext.data.adserver', function() { + beforeEach(function () { + if (bidRequests[0].hasOwnProperty('ortb2Imp')) { + delete bidRequests[0].ortb2Imp; + } + }); + + it('should not send if imp[].ext.data object is invalid', function() { + bidRequests[0].ortb2Imp = { + ext: {} + }; + const request = spec.buildRequests(bidRequests, {}); + let data = JSON.parse(request.data); + expect(data.imp[0].ext).to.not.have.property('data'); + }); + + it('should not send if imp[].ext.data.adserver is undefined', function() { + bidRequests[0].ortb2Imp = { + ext: { + data: { + } + } + }; + const request = spec.buildRequests(bidRequests, {}); + let data = JSON.parse(request.data); + if (data.imp[0].ext.data) { + expect(data.imp[0].ext.data).to.not.have.property('adserver'); + } else { + expect(data.imp[0].ext).to.not.have.property('data'); + } + }); + + it('should send', function() { + let adSlotValue = 'abc'; + bidRequests[0].ortb2Imp = { + ext: { + data: { + adserver: { + name: 'GAM', + adslot: adSlotValue + } + } + } + }; + const request = spec.buildRequests(bidRequests, {}); + let data = JSON.parse(request.data); + expect(data.imp[0].ext.data.adserver.name).to.equal('GAM'); + expect(data.imp[0].ext.data.adserver.adslot).to.equal(adSlotValue); + expect(data.imp[0].ext.dfp_ad_unit_code).to.equal(adSlotValue); + }); + }); + + describe('ortb2Imp.ext.data.other', function() { + beforeEach(function () { + if (bidRequests[0].hasOwnProperty('ortb2Imp')) { + delete bidRequests[0].ortb2Imp; + } + }); + + it('should not send if imp[].ext.data object is invalid', function() { + bidRequests[0].ortb2Imp = { + ext: {} + }; + const request = spec.buildRequests(bidRequests, {}); + let data = JSON.parse(request.data); + expect(data.imp[0].ext).to.not.have.property('data'); + }); + + it('should not send if imp[].ext.data.other is undefined', function() { + bidRequests[0].ortb2Imp = { + ext: { + data: { + } + } + }; + const request = spec.buildRequests(bidRequests, {}); + let data = JSON.parse(request.data); + if (data.imp[0].ext.data) { + expect(data.imp[0].ext.data).to.not.have.property('other'); + } else { + expect(data.imp[0].ext).to.not.have.property('data'); + } + }); + + it('ortb2Imp.ext.data.other', function() { + bidRequests[0].ortb2Imp = { + ext: { + data: { + other: 1234 + } + } + }; + const request = spec.buildRequests(bidRequests, {}); + let data = JSON.parse(request.data); + expect(data.imp[0].ext.data.other).to.equal(1234); + }); + }); + }); + }); + describe('setting imp.floor using floorModule', function() { /* Use the minimum value among floor from floorModule per mediaType @@ -1311,7 +1591,9 @@ describe('PubMatic adapter', function () { it('bidfloor should be undefined if calculation is <= 0', function() { floorModuleTestData.banner.floor = 0; // lowest of them all newRequest[0].params.kadfloor = undefined; - let request = spec.buildRequests(newRequest); + let request = spec.buildRequests(newRequest, { + auctionId: 'new-auction-id' + }); let data = JSON.parse(request.data); data = data.imp[0]; expect(data.bidfloor).to.equal(undefined); @@ -1320,7 +1602,9 @@ describe('PubMatic adapter', function () { it('ignore floormodule o/p if floor is not number', function() { floorModuleTestData.banner.floor = 'INR'; newRequest[0].params.kadfloor = undefined; - let request = spec.buildRequests(newRequest); + let request = spec.buildRequests(newRequest, { + auctionId: 'new-auction-id' + }); let data = JSON.parse(request.data); data = data.imp[0]; expect(data.bidfloor).to.equal(2.5); // video will be lowest now @@ -1329,7 +1613,9 @@ describe('PubMatic adapter', function () { it('ignore floormodule o/p if currency is not matched', function() { floorModuleTestData.banner.currency = 'INR'; newRequest[0].params.kadfloor = undefined; - let request = spec.buildRequests(newRequest); + let request = spec.buildRequests(newRequest, { + auctionId: 'new-auction-id' + }); let data = JSON.parse(request.data); data = data.imp[0]; expect(data.bidfloor).to.equal(2.5); // video will be lowest now @@ -1337,7 +1623,9 @@ describe('PubMatic adapter', function () { it('kadfloor is not passed, use minimum from floorModule', function() { newRequest[0].params.kadfloor = undefined; - let request = spec.buildRequests(newRequest); + let request = spec.buildRequests(newRequest, { + auctionId: 'new-auction-id' + }); let data = JSON.parse(request.data); data = data.imp[0]; expect(data.bidfloor).to.equal(1.5); @@ -1345,7 +1633,9 @@ describe('PubMatic adapter', function () { it('kadfloor is passed as 3, use kadfloor as it is highest', function() { newRequest[0].params.kadfloor = '3.0';// yes, we want it as a string - let request = spec.buildRequests(newRequest); + let request = spec.buildRequests(newRequest, { + auctionId: 'new-auction-id' + }); let data = JSON.parse(request.data); data = data.imp[0]; expect(data.bidfloor).to.equal(3); @@ -1353,7 +1643,9 @@ describe('PubMatic adapter', function () { it('kadfloor is passed as 1, use min of fllorModule as it is highest', function() { newRequest[0].params.kadfloor = '1.0';// yes, we want it as a string - let request = spec.buildRequests(newRequest); + let request = spec.buildRequests(newRequest, { + auctionId: 'new-auction-id' + }); let data = JSON.parse(request.data); data = data.imp[0]; expect(data.bidfloor).to.equal(1.5); @@ -1521,7 +1813,7 @@ describe('PubMatic adapter', function () { describe('ID5 Id', function() { it('send the id5 id if it is present', function() { bidRequests[0].userId = {}; - bidRequests[0].userId.id5id = 'id5-user-id'; + bidRequests[0].userId.id5id = { uid: 'id5-user-id' }; bidRequests[0].userIdAsEids = createEidsArray(bidRequests[0].userId); let request = spec.buildRequests(bidRequests, {}); let data = JSON.parse(request.data); @@ -1536,22 +1828,22 @@ describe('PubMatic adapter', function () { it('do not pass if not string', function() { bidRequests[0].userId = {}; - bidRequests[0].userId.id5id = 1; + bidRequests[0].userId.id5id = { uid: 1 }; bidRequests[0].userIdAsEids = createEidsArray(bidRequests[0].userId); let request = spec.buildRequests(bidRequests, {}); let data = JSON.parse(request.data); expect(data.user.eids).to.equal(undefined); - bidRequests[0].userId.id5id = []; + bidRequests[0].userId.id5id = { uid: [] }; bidRequests[0].userIdAsEids = createEidsArray(bidRequests[0].userId); request = spec.buildRequests(bidRequests, {}); data = JSON.parse(request.data); expect(data.user.eids).to.equal(undefined); - bidRequests[0].userId.id5id = null; + bidRequests[0].userId.id5id = { uid: null }; bidRequests[0].userIdAsEids = createEidsArray(bidRequests[0].userId); request = spec.buildRequests(bidRequests, {}); data = JSON.parse(request.data); expect(data.user.eids).to.equal(undefined); - bidRequests[0].userId.id5id = {}; + bidRequests[0].userId.id5id = { uid: {} }; bidRequests[0].userIdAsEids = createEidsArray(bidRequests[0].userId); request = spec.buildRequests(bidRequests, {}); data = JSON.parse(request.data); @@ -1611,7 +1903,7 @@ describe('PubMatic adapter', function () { 'source': 'liveramp.com', 'uids': [{ 'id': 'identity-link-user-id', - 'atype': 1 + 'atype': 3 }] }]); }); @@ -1652,7 +1944,7 @@ describe('PubMatic adapter', function () { 'source': 'liveintent.com', 'uids': [{ 'id': 'live-intent-user-id', - 'atype': 1 + 'atype': 3 }] }]); }); @@ -1734,7 +2026,7 @@ describe('PubMatic adapter', function () { 'source': 'britepool.com', 'uids': [{ 'id': 'britepool-user-id', - 'atype': 1 + 'atype': 3 }] }]); }); @@ -1804,10 +2096,74 @@ describe('PubMatic adapter', function () { expect(data.user.eids).to.equal(undefined); }); }); + + describe('FlocId', function() { + it('send the FlocId if it is present', function() { + bidRequests[0].userId = {}; + bidRequests[0].userId.flocId = { + id: '1234', + version: 'chrome1.1' + } + let request = spec.buildRequests(bidRequests, {}); + let data = JSON.parse(request.data); + expect(data.user.eids).to.deep.equal([{ + 'source': 'chrome.com', + 'uids': [{ + 'id': '1234', + 'atype': 1, + 'ext': { + 'ver': 'chrome1.1' + } + }] + }]); + expect(data.user.data).to.deep.equal([{ + id: 'FLOC', + name: 'FLOC', + ext: { + ver: 'chrome1.1' + }, + segment: [{ + id: '1234', + name: 'chrome.com', + value: '1234' + }] + }]); + }); + + it('appnend the flocId if userIds are present', function() { + bidRequests[0].userId = {}; + bidRequests[0].userId.netId = 'netid-user-id'; + bidRequests[0].userIdAsEids = createEidsArray(bidRequests[0].userId); + bidRequests[0].userId.flocId = { + id: '1234', + version: 'chrome1.1' + } + let request = spec.buildRequests(bidRequests, {}); + let data = JSON.parse(request.data); + expect(data.user.eids).to.deep.equal([{ + 'source': 'netid.de', + 'uids': [{ + 'id': 'netid-user-id', + 'atype': 1 + }] + }, { + 'source': 'chrome.com', + 'uids': [{ + 'id': '1234', + 'atype': 1, + 'ext': { + 'ver': 'chrome1.1' + } + }] + }]); + }); + }); }); it('Request params check for video ad', function () { - let request = spec.buildRequests(videoBidRequests); + let request = spec.buildRequests(videoBidRequests, { + auctionId: 'new-auction-id' + }); let data = JSON.parse(request.data); expect(data.imp[0].video).to.exist; expect(data.imp[0].tagid).to.equal('Div1'); @@ -1845,7 +2201,9 @@ describe('PubMatic adapter', function () { }); it('Request params check for 1 banner and 1 video ad', function () { - let request = spec.buildRequests(multipleMediaRequests); + let request = spec.buildRequests(multipleMediaRequests, { + auctionId: 'new-auction-id' + }); let data = JSON.parse(request.data); expect(data.imp).to.be.an('array') @@ -1913,7 +2271,9 @@ describe('PubMatic adapter', function () { }); it('Request params should have valid native bid request for all valid params', function () { - let request = spec.buildRequests(nativeBidRequests); + let request = spec.buildRequests(nativeBidRequests, { + auctionId: 'new-auction-id' + }); let data = JSON.parse(request.data); expect(data.imp[0].native).to.exist; expect(data.imp[0].native['request']).to.exist; @@ -1923,13 +2283,17 @@ describe('PubMatic adapter', function () { }); it('Request params should not have valid native bid request for non native request', function () { - let request = spec.buildRequests(bidRequests); + let request = spec.buildRequests(bidRequests, { + auctionId: 'new-auction-id' + }); let data = JSON.parse(request.data); expect(data.imp[0].native).to.not.exist; }); it('Request params should have valid native bid request with valid required param values for all valid params', function () { - let request = spec.buildRequests(nativeBidRequestsWithRequiredParam); + let request = spec.buildRequests(nativeBidRequestsWithRequiredParam, { + auctionId: 'new-auction-id' + }); let data = JSON.parse(request.data); expect(data.imp[0].native).to.exist; expect(data.imp[0].native['request']).to.exist; @@ -1939,12 +2303,16 @@ describe('PubMatic adapter', function () { }); it('should not have valid native request if assets are not defined with minimum required params and only native is the slot', function () { - let request = spec.buildRequests(nativeBidRequestsWithoutAsset); + let request = spec.buildRequests(nativeBidRequestsWithoutAsset, { + auctionId: 'new-auction-id' + }); expect(request).to.deep.equal(undefined); }); it('Request params should have valid native bid request for all native params', function () { - let request = spec.buildRequests(nativeBidRequestsWithAllParams); + let request = spec.buildRequests(nativeBidRequestsWithAllParams, { + auctionId: 'new-auction-id' + }); let data = JSON.parse(request.data); expect(data.imp[0].native).to.exist; expect(data.imp[0].native['request']).to.exist; @@ -1954,7 +2322,9 @@ describe('PubMatic adapter', function () { }); it('Request params - should handle banner and video format in single adunit', function() { - let request = spec.buildRequests(bannerAndVideoBidRequests); + let request = spec.buildRequests(bannerAndVideoBidRequests, { + auctionId: 'new-auction-id' + }); let data = JSON.parse(request.data); data = data.imp[0]; expect(data.banner).to.exist; @@ -1965,7 +2335,9 @@ describe('PubMatic adapter', function () { // Case: when size is not present in adslo bannerAndVideoBidRequests[0].params.adSlot = '/15671365/DMDemo'; - request = spec.buildRequests(bannerAndVideoBidRequests); + request = spec.buildRequests(bannerAndVideoBidRequests, { + auctionId: 'new-auction-id' + }); data = JSON.parse(request.data); data = data.imp[0]; expect(data.banner).to.exist; @@ -1987,7 +2359,9 @@ describe('PubMatic adapter', function () { */ bannerAndVideoBidRequests[0].mediaTypes.banner.sizes = [['fluid'], [160, 600]]; - let request = spec.buildRequests(bannerAndVideoBidRequests); + let request = spec.buildRequests(bannerAndVideoBidRequests, { + auctionId: 'new-auction-id' + }); let data = JSON.parse(request.data); data = data.imp[0]; @@ -2006,7 +2380,9 @@ describe('PubMatic adapter', function () { bannerAndVideoBidRequests[0].mediaTypes.banner.sizes = [['fluid'], [160, 600]]; bannerAndVideoBidRequests[0].params.adSlot = '/15671365/DMDemo'; - request = spec.buildRequests(bannerAndVideoBidRequests); + request = spec.buildRequests(bannerAndVideoBidRequests, { + auctionId: 'new-auction-id' + }); data = JSON.parse(request.data); data = data.imp[0]; @@ -2024,7 +2400,9 @@ describe('PubMatic adapter', function () { */ bannerAndVideoBidRequests[0].mediaTypes.banner.sizes = [[728, 90], ['fluid'], [300, 250]]; - request = spec.buildRequests(bannerAndVideoBidRequests); + request = spec.buildRequests(bannerAndVideoBidRequests, { + auctionId: 'new-auction-id' + }); data = JSON.parse(request.data); data = data.imp[0]; @@ -2042,7 +2420,9 @@ describe('PubMatic adapter', function () { */ bannerAndVideoBidRequests[0].mediaTypes.banner.sizes = [['fluid']]; - request = spec.buildRequests(bannerAndVideoBidRequests); + request = spec.buildRequests(bannerAndVideoBidRequests, { + auctionId: 'new-auction-id' + }); data = JSON.parse(request.data); data = data.imp[0]; @@ -2054,14 +2434,18 @@ describe('PubMatic adapter', function () { delete bannerAndVideoBidRequests[0].mediaTypes.banner; bannerAndVideoBidRequests[0].params.sizes = [300, 250]; - let request = spec.buildRequests(bannerAndVideoBidRequests); + let request = spec.buildRequests(bannerAndVideoBidRequests, { + auctionId: 'new-auction-id' + }); let data = JSON.parse(request.data); data = data.imp[0]; expect(data.banner).to.not.exist; }); it('Request params - should handle banner and native format in single adunit', function() { - let request = spec.buildRequests(bannerAndNativeBidRequests); + let request = spec.buildRequests(bannerAndNativeBidRequests, { + auctionId: 'new-auction-id' + }); let data = JSON.parse(request.data); data = data.imp[0]; @@ -2076,7 +2460,9 @@ describe('PubMatic adapter', function () { }); it('Request params - should handle video and native format in single adunit', function() { - let request = spec.buildRequests(videoAndNativeBidRequests); + let request = spec.buildRequests(videoAndNativeBidRequests, { + auctionId: 'new-auction-id' + }); let data = JSON.parse(request.data); data = data.imp[0]; @@ -2089,7 +2475,9 @@ describe('PubMatic adapter', function () { }); it('Request params - should handle banner, video and native format in single adunit', function() { - let request = spec.buildRequests(bannerVideoAndNativeBidRequests); + let request = spec.buildRequests(bannerVideoAndNativeBidRequests, { + auctionId: 'new-auction-id' + }); let data = JSON.parse(request.data); data = data.imp[0]; @@ -2111,7 +2499,9 @@ describe('PubMatic adapter', function () { delete bannerAndNativeBidRequests[0].mediaTypes.banner; bannerAndNativeBidRequests[0].sizes = [729, 90]; - let request = spec.buildRequests(bannerAndNativeBidRequests); + let request = spec.buildRequests(bannerAndNativeBidRequests, { + auctionId: 'new-auction-id' + }); let data = JSON.parse(request.data); data = data.imp[0]; @@ -2134,7 +2524,9 @@ describe('PubMatic adapter', function () { sponsoredBy: { required: true }, clickUrl: { required: true } } - let request = spec.buildRequests(bannerAndNativeBidRequests); + let request = spec.buildRequests(bannerAndNativeBidRequests, { + auctionId: 'new-auction-id' + }); let data = JSON.parse(request.data); data = data.imp[0]; @@ -2155,7 +2547,9 @@ describe('PubMatic adapter', function () { sponsoredBy: { required: true }, clickUrl: { required: true } } - let request = spec.buildRequests(videoAndNativeBidRequests); + let request = spec.buildRequests(videoAndNativeBidRequests, { + auctionId: 'new-auction-id' + }); let data = JSON.parse(request.data); data = data.imp[0]; @@ -2218,7 +2612,9 @@ describe('PubMatic adapter', function () { } ]; - let request = spec.buildRequests(multipleBidRequests); + let request = spec.buildRequests(multipleBidRequests, { + auctionId: 'new-auction-id' + }); let data = JSON.parse(request.data); /* case 1 - @@ -2232,7 +2628,9 @@ describe('PubMatic adapter', function () { dctr not present in adunit[0] */ delete multipleBidRequests[0].params.dctr; - request = spec.buildRequests(multipleBidRequests); + request = spec.buildRequests(multipleBidRequests, { + auctionId: 'new-auction-id' + }); data = JSON.parse(request.data); expect(data.site.ext).to.not.exist; @@ -2241,7 +2639,9 @@ describe('PubMatic adapter', function () { dctr is present in adunit[0], but is not a string value */ multipleBidRequests[0].params.dctr = 123; - request = spec.buildRequests(multipleBidRequests); + request = spec.buildRequests(multipleBidRequests, { + auctionId: 'new-auction-id' + }); data = JSON.parse(request.data); expect(data.site.ext).to.not.exist; @@ -2301,7 +2701,9 @@ describe('PubMatic adapter', function () { } ]; - let request = spec.buildRequests(multipleBidRequests); + let request = spec.buildRequests(multipleBidRequests, { + auctionId: 'new-auction-id' + }); let data = JSON.parse(request.data); // case 1 - deals are passed as expected, ['', ''] , in both adUnits expect(data.imp[0].pmp).to.deep.equal({ @@ -2329,19 +2731,25 @@ describe('PubMatic adapter', function () { // case 2 - deals not present in adunit[0] delete multipleBidRequests[0].params.deals; - request = spec.buildRequests(multipleBidRequests); + request = spec.buildRequests(multipleBidRequests, { + auctionId: 'new-auction-id' + }); data = JSON.parse(request.data); expect(data.imp[0].pmp).to.not.exist; // case 3 - deals is present in adunit[0], but is not an array multipleBidRequests[0].params.deals = 123; - request = spec.buildRequests(multipleBidRequests); + request = spec.buildRequests(multipleBidRequests, { + auctionId: 'new-auction-id' + }); data = JSON.parse(request.data); expect(data.imp[0].pmp).to.not.exist; // case 4 - deals is present in adunit[0] as an array but one of the value is not a string multipleBidRequests[0].params.deals = [123, 'deal-id-1']; - request = spec.buildRequests(multipleBidRequests); + request = spec.buildRequests(multipleBidRequests, { + auctionId: 'new-auction-id' + }); data = JSON.parse(request.data); expect(data.imp[0].pmp).to.deep.equal({ 'private_auction': 0, @@ -2409,21 +2817,27 @@ describe('PubMatic adapter', function () { it('bcat: pass only strings', function() { multipleBidRequests[0].params.bcat = [1, 2, 3, 'IAB1', 'IAB2']; - let request = spec.buildRequests(multipleBidRequests); + let request = spec.buildRequests(multipleBidRequests, { + auctionId: 'new-auction-id' + }); let data = JSON.parse(request.data); expect(data.bcat).to.exist.and.to.deep.equal(['IAB1', 'IAB2']); }); it('bcat: pass strings with length greater than 3', function() { multipleBidRequests[0].params.bcat = ['AB', 'CD', 'IAB1', 'IAB2']; - let request = spec.buildRequests(multipleBidRequests); + let request = spec.buildRequests(multipleBidRequests, { + auctionId: 'new-auction-id' + }); let data = JSON.parse(request.data); expect(data.bcat).to.exist.and.to.deep.equal(['IAB1', 'IAB2']); }); it('bcat: trim the strings', function() { multipleBidRequests[0].params.bcat = [' IAB1 ', ' IAB2 ']; - let request = spec.buildRequests(multipleBidRequests); + let request = spec.buildRequests(multipleBidRequests, { + auctionId: 'new-auction-id' + }); let data = JSON.parse(request.data); expect(data.bcat).to.exist.and.to.deep.equal(['IAB1', 'IAB2']); }); @@ -2432,7 +2846,9 @@ describe('PubMatic adapter', function () { // multi slot multipleBidRequests[0].params.bcat = ['IAB1', 'IAB2', 'IAB1', 'IAB2', 'IAB1', 'IAB2']; multipleBidRequests[1].params.bcat = ['IAB1', 'IAB2', 'IAB1', 'IAB2', 'IAB1', 'IAB3']; - let request = spec.buildRequests(multipleBidRequests); + let request = spec.buildRequests(multipleBidRequests, { + auctionId: 'new-auction-id' + }); let data = JSON.parse(request.data); expect(data.bcat).to.exist.and.to.deep.equal(['IAB1', 'IAB2', 'IAB3']); }); @@ -2441,7 +2857,9 @@ describe('PubMatic adapter', function () { // multi slot multipleBidRequests[0].params.bcat = ['', 'IAB', 'IAB']; multipleBidRequests[1].params.bcat = [' ', 22, 99999, 'IA']; - let request = spec.buildRequests(multipleBidRequests); + let request = spec.buildRequests(multipleBidRequests, { + auctionId: 'new-auction-id' + }); let data = JSON.parse(request.data); expect(data.bcat).to.deep.equal(undefined); }); @@ -2449,7 +2867,9 @@ describe('PubMatic adapter', function () { describe('Response checking', function () { it('should check for valid response values', function () { - let request = spec.buildRequests(bidRequests); + 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); @@ -2464,7 +2884,7 @@ describe('PubMatic adapter', function () { } expect(response[0].dealId).to.equal(bidResponses.body.seatbid[0].bid[0].dealid); expect(response[0].currency).to.equal('USD'); - expect(response[0].netRevenue).to.equal(false); + expect(response[0].netRevenue).to.equal(true); expect(response[0].ttl).to.equal(300); expect(response[0].meta.networkId).to.equal(123); expect(response[0].adserverTargeting.hb_buyid_pubmatic).to.equal('BUYER-ID-987'); @@ -2488,7 +2908,7 @@ describe('PubMatic adapter', function () { } expect(response[1].dealId).to.equal(bidResponses.body.seatbid[1].bid[0].dealid); expect(response[1].currency).to.equal('USD'); - expect(response[1].netRevenue).to.equal(false); + expect(response[1].netRevenue).to.equal(true); expect(response[1].ttl).to.equal(300); expect(response[1].meta.networkId).to.equal(422); expect(response[1].adserverTargeting.hb_buyid_pubmatic).to.equal('BUYER-ID-789'); @@ -2503,7 +2923,9 @@ describe('PubMatic adapter', function () { }); it('should check for dealChannel value selection', function () { - let request = spec.buildRequests(bidRequests); + let request = spec.buildRequests(bidRequests, { + auctionId: 'new-auction-id' + }); let response = spec.interpretResponse(bidResponses, request); expect(response).to.be.an('array').with.length.above(0); expect(response[0].dealChannel).to.equal('PMPG'); @@ -2511,7 +2933,9 @@ describe('PubMatic adapter', function () { }); it('should check for unexpected dealChannel value selection', function () { - let request = spec.buildRequests(bidRequests); + let request = spec.buildRequests(bidRequests, { + auctionId: 'new-auction-id' + }); let updateBiResponse = bidResponses; updateBiResponse.body.seatbid[0].bid[0].ext.deal_channel = 11; @@ -2522,7 +2946,9 @@ describe('PubMatic adapter', function () { }); it('should have a valid native bid response', function() { - let request = spec.buildRequests(nativeBidRequests); + let request = spec.buildRequests(nativeBidRequests, { + auctionId: 'new-auction-id' + }); let data = JSON.parse(request.data); data.imp[0].id = '2a5571261281d4'; request.data = JSON.stringify(data); @@ -2540,20 +2966,26 @@ describe('PubMatic adapter', function () { }); it('should check for valid banner mediaType in case of multiformat request', function() { - let request = spec.buildRequests(bidRequests); + let request = spec.buildRequests(bidRequests, { + auctionId: 'new-auction-id' + }); let response = spec.interpretResponse(bannerBidResponse, request); expect(response[0].mediaType).to.equal('banner'); }); it('should check for valid video mediaType in case of multiformat request', function() { - let request = spec.buildRequests(videoBidRequests); + let request = spec.buildRequests(videoBidRequests, { + auctionId: 'new-auction-id' + }); let response = spec.interpretResponse(videoBidResponse, request); expect(response[0].mediaType).to.equal('video'); }); it('should check for valid native mediaType in case of multiformat request', function() { - let request = spec.buildRequests(nativeBidRequests); + let request = spec.buildRequests(nativeBidRequests, { + auctionId: 'new-auction-id' + }); let response = spec.interpretResponse(nativeBidResponse, request); expect(response[0].mediaType).to.equal('native'); @@ -2566,25 +2998,33 @@ describe('PubMatic adapter', function () { }); it('should not assign renderer if bidderRequest is not present', function() { - let request = spec.buildRequests(outstreamBidRequest); + let request = spec.buildRequests(outstreamBidRequest, { + auctionId: 'new-auction-id' + }); let response = spec.interpretResponse(outstreamVideoBidResponse, request); expect(response[0].renderer).to.not.exist; }); it('should not assign renderer if bid is video and request is for instream', function() { - let request = spec.buildRequests(videoBidRequests); + let request = spec.buildRequests(videoBidRequests, { + auctionId: 'new-auction-id' + }); let response = spec.interpretResponse(videoBidResponse, request); expect(response[0].renderer).to.not.exist; }); it('should not assign renderer if bid is native', function() { - let request = spec.buildRequests(nativeBidRequests); + let request = spec.buildRequests(nativeBidRequests, { + auctionId: 'new-auction-id' + }); let response = spec.interpretResponse(nativeBidResponse, request); expect(response[0].renderer).to.not.exist; }); it('should not assign renderer if bid is of banner', function() { - let request = spec.buildRequests(bidRequests); + let request = spec.buildRequests(bidRequests, { + auctionId: 'new-auction-id' + }); let response = spec.interpretResponse(bidResponses, request); expect(response[0].renderer).to.not.exist; }); diff --git a/test/spec/modules/pubwiseAnalyticsAdapter_spec.js b/test/spec/modules/pubwiseAnalyticsAdapter_spec.js index 5e4b2be894e..3be4ea3d69c 100644 --- a/test/spec/modules/pubwiseAnalyticsAdapter_spec.js +++ b/test/spec/modules/pubwiseAnalyticsAdapter_spec.js @@ -1,47 +1,164 @@ +import { expect } from 'chai'; import pubwiseAnalytics from 'modules/pubwiseAnalyticsAdapter.js'; +import {server} from 'test/mocks/xhr.js'; let events = require('src/events'); let adapterManager = require('src/adapterManager').default; let constants = require('src/constants.json'); describe('PubWise Prebid Analytics', function () { - after(function () { + let requests; + let sandbox; + let xhr; + let clock; + let mock = {}; + + mock.DEFAULT_PW_CONFIG = { + provider: 'pubwiseanalytics', + options: { + site: ['b1ccf317-a6fc-428d-ba69-0c9c208aa61c'], + custom: {'c_script_type': 'test-script-type', 'c_host': 'test-host', 'c_slot1': 'test-slot1', 'c_slot2': 'test-slot2', 'c_slot3': 'test-slot3', 'c_slot4': 'test-slot4'} + } + }; + mock.AUCTION_INIT = {auctionId: '53c35d77-bd62-41e7-b920-244140e30c77'}; + mock.AUCTION_INIT_EXTRAS = { + auctionId: '53c35d77-bd62-41e7-b920-244140e30c77', + adUnitCodes: 'not empty', + adUnits: '', + bidderRequests: ['0'], + bidsReceived: '0', + config: {test: 'config'}, + noBids: 'no bids today', + winningBids: 'winning bids', + extraProp: 'extraProp retained' + }; + + beforeEach(function() { + sandbox = sinon.sandbox.create(); + clock = sandbox.useFakeTimers(); + sandbox.stub(events, 'getEvents').returns([]); + + xhr = sandbox.useFakeXMLHttpRequest(); + requests = []; + xhr.onCreate = request => requests.push(request); + }); + + afterEach(function () { + sandbox.restore(); + clock.restore(); pubwiseAnalytics.disableAnalytics(); }); describe('enableAnalytics', function () { beforeEach(function () { - sinon.stub(events, 'getEvents').returns([]); - }); - - afterEach(function () { - events.getEvents.restore(); + requests = []; }); it('should catch all events', function () { - sinon.spy(pubwiseAnalytics, 'track'); + pubwiseAnalytics.enableAnalytics(mock.DEFAULT_PW_CONFIG); - adapterManager.registerAnalyticsAdapter({ - code: 'pubwiseanalytics', - adapter: pubwiseAnalytics - }); + sandbox.spy(pubwiseAnalytics, 'track'); - adapterManager.enableAnalytics({ - provider: 'pubwiseanalytics', - options: { - site: ['test-test-test-test'] - } - }); - - events.emit(constants.EVENTS.AUCTION_INIT, {}); + // sent + events.emit(constants.EVENTS.AUCTION_INIT, mock.AUCTION_INIT); events.emit(constants.EVENTS.BID_REQUESTED, {}); events.emit(constants.EVENTS.BID_RESPONSE, {}); events.emit(constants.EVENTS.BID_WON, {}); + events.emit(constants.EVENTS.AD_RENDER_FAILED, {}); + events.emit(constants.EVENTS.TCF2_ENFORCEMENT, {}); + events.emit(constants.EVENTS.BID_TIMEOUT, {}); + // forces flush events.emit(constants.EVENTS.AUCTION_END, {}); - events.emit(constants.EVENTS.BID_TIMEOUT, {}); + + // eslint-disable-next-line + //console.log(requests); /* testing for 6 calls, including the 2 we're not currently tracking */ - sinon.assert.callCount(pubwiseAnalytics.track, 6); + sandbox.assert.callCount(pubwiseAnalytics.track, 7); + }); + + it('should initialize the auction properly', function () { + pubwiseAnalytics.enableAnalytics(mock.DEFAULT_PW_CONFIG); + + // sent + events.emit(constants.EVENTS.AUCTION_INIT, mock.AUCTION_INIT); + events.emit(constants.EVENTS.BID_REQUESTED, {}); + events.emit(constants.EVENTS.BID_RESPONSE, {}); + events.emit(constants.EVENTS.BID_WON, {}); + // force flush + clock.tick(500); + + /* check for critical values */ + let request = requests[0]; + let data = JSON.parse(request.requestBody); + // eslint-disable-next-line + // console.log(data.metaData); + expect(data.metaData, 'metaData property').to.exist; + expect(data.metaData.pbjs_version, 'pbjs version').to.equal('$prebid.version$') + expect(data.metaData.session_id, 'session id').not.to.be.empty + expect(data.metaData.activation_id, 'activation id').not.to.be.empty + + // check custom metadata slots + expect(data.metaData.c_script_type, 'c_script_type property').to.exist; + expect(data.metaData.c_script_type, 'c_script_type').not.to.be.empty + expect(data.metaData.c_script_type).to.equal('test-script-type'); + + expect(data.metaData.c_host, 'c_host property').to.exist; + expect(data.metaData.c_host, 'c_host').not.to.be.empty + expect(data.metaData.c_host).to.equal('test-host'); + + expect(data.metaData.c_slot1, 'c_slot1 property').to.exist; + expect(data.metaData.c_slot1, 'c_slot1').not.to.be.empty + expect(data.metaData.c_slot1).to.equal('test-slot1'); + + expect(data.metaData.c_slot2, 'c_slot1 property').to.exist; + expect(data.metaData.c_slot2, 'c_slot1').not.to.be.empty + expect(data.metaData.c_slot2).to.equal('test-slot2'); + + expect(data.metaData.c_slot3, 'c_slot1 property').to.exist; + expect(data.metaData.c_slot3, 'c_slot1').not.to.be.empty + expect(data.metaData.c_slot3).to.equal('test-slot3'); + + expect(data.metaData.c_slot4, 'c_slot1 property').to.exist; + expect(data.metaData.c_slot4, 'c_slot1').not.to.be.empty + expect(data.metaData.c_slot4).to.equal('test-slot4'); + + // check for version info too + expect(data.metaData.pw_version, 'pw_version property').to.exist; + expect(data.metaData.pbjs_version, 'pbjs_version property').to.exist; + }); + + it('should remove extra data on init', function () { + pubwiseAnalytics.enableAnalytics(mock.DEFAULT_PW_CONFIG); + + // sent + events.emit(constants.EVENTS.AUCTION_INIT, mock.AUCTION_INIT_EXTRAS); + // force flush + clock.tick(500); + + /* check for critical values */ + let request = requests[0]; + let data = JSON.parse(request.requestBody); + + // check the basics + expect(data.eventList, 'eventList property').to.exist; + expect(data.eventList[0], 'eventList property').to.exist; + expect(data.eventList[0].args, 'eventList property').to.exist; + + // eslint-disable-next-line + // console.log(data.eventList[0].args); + + let eventArgs = data.eventList[0].args; + // the props we want removed should go away + expect(eventArgs.adUnitCodes, 'adUnitCodes property').not.to.exist; + expect(eventArgs.bidderRequests, 'adUnitCodes property').not.to.exist; + expect(eventArgs.bidsReceived, 'adUnitCodes property').not.to.exist; + expect(eventArgs.config, 'adUnitCodes property').not.to.exist; + expect(eventArgs.noBids, 'adUnitCodes property').not.to.exist; + expect(eventArgs.winningBids, 'adUnitCodes property').not.to.exist; + + // the extra prop should still exist + expect(eventArgs.extraProp, 'adUnitCodes property').to.exist; }); }); }); 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 new file mode 100644 index 00000000000..6cea8787845 --- /dev/null +++ b/test/spec/modules/pubxBidAdapter_spec.js @@ -0,0 +1,189 @@ +import {expect} from 'chai'; +import {spec} from 'modules/pubxBidAdapter.js'; +import {newBidder} from 'src/adapters/bidderFactory.js'; + +describe('pubxAdapter', function () { + const adapter = newBidder(spec); + const ENDPOINT = 'https://api.primecaster.net/adlogue/api/slot/bid'; + + 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: 'pubx', + params: { + sid: '12345abc' + } + }; + + it('should return true when required params found', function () { + expect(spec.isBidRequestValid(bid)).to.equal(true); + }); + + it('should return false when required params are not passed', function () { + let bid = Object.assign({}, bid); + delete bid.params; + bid.params = {}; + expect(spec.isBidRequestValid(bid)).to.equal(false); + }); + }); + + describe('buildRequests', function () { + const bidRequests = [ + { + id: '26c1ee0038ac11', + params: { + sid: '12345abc' + } + } + ]; + + const data = { + banner: { + sid: '12345abc' + } + }; + + it('sends bid request to ENDPOINT via GET', function () { + const request = spec.buildRequests(bidRequests)[0]; + expect(request.url).to.equal(ENDPOINT); + expect(request.method).to.equal('GET'); + }); + + it('should attach params to the banner request', function () { + const request = spec.buildRequests(bidRequests)[0]; + expect(request.data).to.deep.equal(data.banner); + }); + }); + + 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: { + TTL: 300, + adm: '
some creative
', + cid: 'TKmB', + cpm: 500, + currency: 'JPY', + height: 250, + width: 300, + } + } + + const bidRequests = [ + { + id: '26c1ee0038ac11', + params: { + sid: '12345abc' + } + } + ]; + + const bidResponses = [ + { + requestId: '26c1ee0038ac11', + cpm: 500, + currency: 'JPY', + width: 300, + height: 250, + creativeId: 'TKmB', + netRevenue: true, + ttl: 300, + ad: '
some creative
' + } + ]; + it('should return empty array when required param is empty', function () { + const serverResponseWithCidEmpty = { + body: { + TTL: 300, + adm: '
some creative
', + cid: '', + cpm: '', + currency: 'JPY', + height: 250, + width: 300, + } + } + const result = spec.interpretResponse(serverResponseWithCidEmpty, bidRequests[0]); + expect(result).to.be.empty; + }); + it('handles banner responses', function () { + const result = spec.interpretResponse(serverResponse, bidRequests[0])[0]; + expect(result.requestId).to.equal(bidResponses[0].requestId); + expect(result.width).to.equal(bidResponses[0].width); + expect(result.height).to.equal(bidResponses[0].height); + expect(result.creativeId).to.equal(bidResponses[0].creativeId); + expect(result.currency).to.equal(bidResponses[0].currency); + expect(result.netRevenue).to.equal(bidResponses[0].netRevenue); + expect(result.ttl).to.equal(bidResponses[0].ttl); + expect(result.ad).to.equal(bidResponses[0].ad); + }); + }); +}); diff --git a/test/spec/modules/pubxaiAnalyticsAdapter_spec.js b/test/spec/modules/pubxaiAnalyticsAdapter_spec.js new file mode 100644 index 00000000000..40f7afeeb6a --- /dev/null +++ b/test/spec/modules/pubxaiAnalyticsAdapter_spec.js @@ -0,0 +1,683 @@ +import pubxaiAnalyticsAdapter from 'modules/pubxaiAnalyticsAdapter.js'; +import { getDeviceType, getBrowser, getOS } from 'modules/pubxaiAnalyticsAdapter.js'; +import { + expect +} from 'chai'; +import adapterManager from 'src/adapterManager.js'; +import * as utils from 'src/utils.js'; +import { + server +} from 'test/mocks/xhr.js'; + +let events = require('src/events'); +let constants = require('src/constants.json'); + +describe('pubxai analytics adapter', function() { + beforeEach(function() { + sinon.stub(events, 'getEvents').returns([]); + }); + + afterEach(function() { + events.getEvents.restore(); + }); + + describe('track', function() { + let initOptions = { + samplingRate: '1', + pubxId: '6c415fc0-8b0e-4cf5-be73-01526a4db625' + }; + + let location = utils.getWindowLocation(); + + let prebidEvent = { + 'auctionInit': { + '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': 'test model 1.0', + 'location': 'fetch', + 'floorProvider': 'PubXFloorProvider', + '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': 'test model 1.0', + 'location': 'fetch', + 'floorProvider': 'PubXFloorProvider', + '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' + } + }, + 'bidRequested': { + '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': 'test model 1.0', + 'location': 'fetch', + 'floorProvider': 'PubXFloorProvider', + '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 + }, + 'bidTimeout': [], + 'bidResponse': { + 'bidderCode': 'appnexus', + 'width': 300, + 'height': 250, + 'statusMessage': 'Bid available', + 'adId': '32780c4bc382cb', + 'requestId': '248f9a4489835e', + 'mediaType': 'banner', + 'source': 'client', + 'cpm': 0.5, + 'creativeId': 96846035, + 'currency': 'USD', + 'netRevenue': true, + 'ttl': 300, + 'adUnitCode': '/19968336/header-bid-tag-1', + 'appnexus': { + 'buyerMemberId': 9325 + }, + 'meta': { + 'advertiserId': 2529885 + }, + 'ad': '', + 'originalCpm': 0.5, + 'originalCurrency': 'USD', + 'floorData': { + 'fetchStatus': 'success', + 'floorProvider': 'PubXFloorProvider', + 'location': 'fetch', + 'modelVersion': 'test model 1.0', + 'skipRate': 0, + 'skipped': false, + 'floorValue': 0.4, + 'floorRule': '/19968336/header-bid-tag-1|banner', + 'floorCurrency': 'USD', + 'cpmAfterAdjustments': 0.5, + 'enforcements': { + 'enforceJS': true, + 'enforcePBS': false, + 'floorDeals': true, + 'bidAdjustment': true + }, + 'matchedFields': { + 'gptSlot': '/19968336/header-bid-tag-1', + 'mediaType': 'banner' + } + }, + 'auctionId': 'bc3806e4-873e-453c-8ae5-204f35e923b4', + 'responseTimestamp': 1616654313071, + 'requestTimestamp': 1616654312804, + 'bidder': 'appnexus', + 'timeToRespond': 267, + 'pbLg': '0.50', + 'pbMg': '0.50', + 'pbHg': '0.50', + 'pbAg': '0.50', + 'pbDg': '0.50', + 'pbCg': '0.50', + 'size': '300x250', + 'adserverTargeting': { + 'hb_bidder': 'appnexus', + 'hb_adid': '32780c4bc382cb', + 'hb_pb': '0.50', + 'hb_size': '300x250', + 'hb_source': 'client', + 'hb_format': 'banner' + }, + }, + 'auctionEnd': { + 'auctionId': 'bc3806e4-873e-453c-8ae5-204f35e923b4', + 'timestamp': 1616654312804, + 'auctionEnd': 1616654313090, + 'auctionStatus': 'completed', + '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': 'test model 1.0', + 'location': 'fetch', + 'floorProvider': 'PubXFloorProvider', + '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': 'test model 1.0', + 'location': 'fetch', + 'floorProvider': 'PubXFloorProvider', + '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': [{ + 'bidderCode': 'appnexus', + 'width': 300, + 'height': 250, + 'statusMessage': 'Bid available', + 'adId': '32780c4bc382cb', + 'requestId': '248f9a4489835e', + 'mediaType': 'banner', + 'source': 'client', + 'cpm': 0.5, + 'creativeId': 96846035, + 'currency': 'USD', + 'netRevenue': true, + 'ttl': 300, + 'adUnitCode': '/19968336/header-bid-tag-1', + 'appnexus': { + 'buyerMemberId': 9325 + }, + 'meta': { + 'advertiserId': 2529885 + }, + 'ad': '', + 'originalCpm': 0.5, + 'originalCurrency': 'USD', + 'floorData': { + 'fetchStatus': 'success', + 'floorProvider': 'PubXFloorProvider', + 'location': 'fetch', + 'modelVersion': 'test model 1.0', + 'skipRate': 0, + 'skipped': false, + 'floorValue': 0.4, + 'floorRule': '/19968336/header-bid-tag-1|banner', + 'floorCurrency': 'USD', + 'cpmAfterAdjustments': 0.5, + 'enforcements': { + 'enforceJS': true, + 'enforcePBS': false, + 'floorDeals': true, + 'bidAdjustment': true + }, + 'matchedFields': { + 'gptSlot': '/19968336/header-bid-tag-1', + 'mediaType': 'banner' + } + }, + 'auctionId': 'bc3806e4-873e-453c-8ae5-204f35e923b4', + 'responseTimestamp': 1616654313071, + 'requestTimestamp': 1616654312804, + 'bidder': 'appnexus', + 'timeToRespond': 267, + 'pbLg': '0.50', + 'pbMg': '0.50', + 'pbHg': '0.50', + 'pbAg': '0.50', + 'pbDg': '0.50', + 'pbCg': '0.50', + 'size': '300x250', + 'adserverTargeting': { + 'hb_bidder': 'appnexus', + 'hb_adid': '32780c4bc382cb', + 'hb_pb': '0.50', + 'hb_size': '300x250', + 'hb_source': 'client', + 'hb_format': 'banner' + }, + 'status': 'rendered', + 'params': [{ + 'placementId': 13144370 + }] + }], + 'winningBids': [], + 'timeout': 1000 + }, + 'bidWon': { + 'bidderCode': 'appnexus', + 'width': 300, + 'height': 250, + 'statusMessage': 'Bid available', + 'adId': '32780c4bc382cb', + 'requestId': '248f9a4489835e', + 'mediaType': 'banner', + 'source': 'client', + 'cpm': 0.5, + 'creativeId': 96846035, + 'currency': 'USD', + 'netRevenue': true, + 'ttl': 300, + 'adUnitCode': '/19968336/header-bid-tag-1', + 'appnexus': { + 'buyerMemberId': 9325 + }, + 'meta': { + 'advertiserId': 2529885 + }, + 'ad': '', + 'originalCpm': 0.5, + 'originalCurrency': 'USD', + 'floorData': { + 'fetchStatus': 'success', + 'floorProvider': 'PubXFloorProvider', + 'location': 'fetch', + 'modelVersion': 'test model 1.0', + 'skipRate': 0, + 'skipped': false, + 'floorValue': 0.4, + 'floorRule': '/19968336/header-bid-tag-1|banner', + 'floorCurrency': 'USD', + 'cpmAfterAdjustments': 0.5, + 'enforcements': { + 'enforceJS': true, + 'enforcePBS': false, + 'floorDeals': true, + 'bidAdjustment': true + }, + 'matchedFields': { + 'gptSlot': '/19968336/header-bid-tag-1', + 'mediaType': 'banner' + } + }, + 'auctionId': 'bc3806e4-873e-453c-8ae5-204f35e923b4', + 'responseTimestamp': 1616654313071, + 'requestTimestamp': 1616654312804, + 'bidder': 'appnexus', + 'timeToRespond': 267, + 'pbLg': '0.50', + 'pbMg': '0.50', + 'pbHg': '0.50', + 'pbAg': '0.50', + 'pbDg': '0.50', + 'pbCg': '0.50', + 'size': '300x250', + 'adserverTargeting': { + 'hb_bidder': 'appnexus', + 'hb_adid': '32780c4bc382cb', + 'hb_pb': '0.50', + 'hb_size': '300x250', + 'hb_source': 'client', + 'hb_format': 'banner' + }, + 'status': 'rendered', + 'params': [{ + 'placementId': 13144370 + }] + }, + 'pageDetail': { + 'host': location.host, + 'path': location.pathname, + 'search': location.search + }, + }; + + let expectedAfterBid = { + 'bids': [{ + 'bidderCode': 'appnexus', + 'bidId': '248f9a4489835e', + 'adUnitCode': '/19968336/header-bid-tag-1', + 'auctionId': 'bc3806e4-873e-453c-8ae5-204f35e923b4', + 'sizes': '300x250', + 'renderStatus': 2, + 'requestTimestamp': 1616654312804, + 'creativeId': 96846035, + 'currency': 'USD', + 'cpm': 0.5, + 'netRevenue': true, + 'mediaType': 'banner', + 'statusMessage': 'Bid available', + 'floorData': { + 'fetchStatus': 'success', + 'floorProvider': 'PubXFloorProvider', + 'location': 'fetch', + 'modelVersion': 'test model 1.0', + 'skipRate': 0, + 'skipped': false, + 'floorValue': 0.4, + 'floorRule': '/19968336/header-bid-tag-1|banner', + 'floorCurrency': 'USD', + 'cpmAfterAdjustments': 0.5, + 'enforcements': { + 'enforceJS': true, + 'enforcePBS': false, + 'floorDeals': true, + 'bidAdjustment': true + }, + 'matchedFields': { + 'gptSlot': '/19968336/header-bid-tag-1', + 'mediaType': 'banner' + } + }, + 'timeToRespond': 267, + 'responseTimestamp': 1616654313071 + }], + 'pageDetail': { + 'host': location.host, + 'path': location.pathname, + 'search': location.search, + 'adUnitCount': 1 + }, + 'floorDetail': { + 'fetchStatus': 'success', + 'floorProvider': 'PubXFloorProvider', + 'location': 'fetch', + 'modelVersion': 'test model 1.0', + 'skipRate': 0, + 'skipped': false + }, + 'deviceDetail': { + 'platform': navigator.platform, + 'deviceType': getDeviceType(), + 'deviceOS': getOS(), + 'browser': getBrowser() + }, + 'initOptions': initOptions + }; + + let expectedAfterBidWon = { + 'winningBid': { + 'adUnitCode': '/19968336/header-bid-tag-1', + 'auctionId': 'bc3806e4-873e-453c-8ae5-204f35e923b4', + 'bidderCode': 'appnexus', + 'bidId': '248f9a4489835e', + 'cpm': 0.5, + 'creativeId': 96846035, + 'currency': 'USD', + 'floorData': { + 'fetchStatus': 'success', + 'floorProvider': 'PubXFloorProvider', + 'location': 'fetch', + 'modelVersion': 'test model 1.0', + 'skipRate': 0, + 'skipped': false, + 'floorValue': 0.4, + 'floorRule': '/19968336/header-bid-tag-1|banner', + 'floorCurrency': 'USD', + 'cpmAfterAdjustments': 0.5, + 'enforcements': { + 'enforceJS': true, + 'enforcePBS': false, + 'floorDeals': true, + 'bidAdjustment': true + }, + 'matchedFields': { + 'gptSlot': '/19968336/header-bid-tag-1', + 'mediaType': 'banner' + } + }, + 'floorProvider': 'PubXFloorProvider', + 'isWinningBid': true, + 'mediaType': 'banner', + 'netRevenue': true, + 'placementId': 13144370, + 'renderedSize': '300x250', + 'renderStatus': 4, + 'responseTimestamp': 1616654313071, + 'requestTimestamp': 1616654312804, + 'status': 'rendered', + 'statusMessage': 'Bid available', + 'timeToRespond': 267 + }, + 'deviceDetail': { + 'platform': navigator.platform, + 'deviceType': getDeviceType(), + 'deviceOS': getOS(), + 'browser': getBrowser() + }, + 'initOptions': initOptions + } + + adapterManager.registerAnalyticsAdapter({ + code: 'pubxai', + adapter: pubxaiAnalyticsAdapter + }); + + beforeEach(function() { + adapterManager.enableAnalytics({ + provider: 'pubxai', + options: initOptions + }); + }); + + afterEach(function() { + pubxaiAnalyticsAdapter.disableAnalytics(); + }); + + it('builds and sends auction data', function() { + // Step 1: Send auction init event + events.emit(constants.EVENTS.AUCTION_INIT, prebidEvent['auctionInit']); + + // Step 2: Send bid requested event + events.emit(constants.EVENTS.BID_REQUESTED, prebidEvent['bidRequested']); + + // Step 3: Send bid response event + events.emit(constants.EVENTS.BID_RESPONSE, prebidEvent['bidResponse']); + + // Step 4: Send bid time out event + events.emit(constants.EVENTS.BID_TIMEOUT, prebidEvent['bidTimeout']); + + // Step 5: Send auction end event + events.emit(constants.EVENTS.AUCTION_END, prebidEvent['auctionEnd']); + + expect(server.requests.length).to.equal(1); + + let realAfterBid = JSON.parse(server.requests[0].requestBody); + + expect(realAfterBid).to.deep.equal(expectedAfterBid); + + // Step 6: Send auction bid won event + events.emit(constants.EVENTS.BID_WON, prebidEvent['bidWon']); + + expect(server.requests.length).to.equal(2); + + let winEventData = JSON.parse(server.requests[1].requestBody); + + expect(winEventData).to.deep.equal(expectedAfterBidWon); + }); + }); +}); diff --git a/test/spec/modules/pulsepointBidAdapter_spec.js b/test/spec/modules/pulsepointBidAdapter_spec.js index d71bd018ab3..c3830d5cb46 100644 --- a/test/spec/modules/pulsepointBidAdapter_spec.js +++ b/test/spec/modules/pulsepointBidAdapter_spec.js @@ -19,6 +19,11 @@ describe('PulsePoint Adapter Tests', function () { } }, { placementCode: '/DfpAccount2/slot2', + mediaTypes: { + banner: { + sizes: [[728, 90]] + } + }, bidId: 'bid23456', params: { cp: 'p10000', @@ -72,6 +77,11 @@ describe('PulsePoint Adapter Tests', function () { }]; const additionalParamsConfig = [{ placementCode: '/DfpAccount1/slot1', + mediaTypes: { + banner: { + sizes: [[1, 1]] + } + }, bidId: 'bid12345', params: { cp: 'p10000', @@ -89,6 +99,11 @@ describe('PulsePoint Adapter Tests', function () { const ortbParamsSlotConfig = [{ placementCode: '/DfpAccount1/slot1', + mediaTypes: { + banner: { + sizes: [[1, 1]] + } + }, bidId: 'bid12345', params: { cp: 'p10000', @@ -146,6 +161,11 @@ describe('PulsePoint Adapter Tests', function () { const schainParamsSlotConfig = [{ placementCode: '/DfpAccount1/slot1', + mediaTypes: { + banner: { + sizes: [[1, 1]] + } + }, bidId: 'bid12345', params: { cp: 'p10000', @@ -630,7 +650,7 @@ describe('PulsePoint Adapter Tests', function () { britepoolid: 'britepool_id123', criteoId: 'criteo_id234', idl_env: 'idl_id123', - id5id: 'id5id_234', + id5id: { uid: 'id5id_234' }, parrableId: { eid: 'parrable_id234' }, lipb: { lipbid: 'liveintent_id123' @@ -681,7 +701,10 @@ describe('PulsePoint Adapter Tests', function () { expect(ortbRequest.imp[1].banner).to.not.be.null; expect(ortbRequest.imp[1].banner.w).to.equal(728); expect(ortbRequest.imp[1].banner.h).to.equal(90); - expect(ortbRequest.imp[1].banner.format).to.be.null; + expect(ortbRequest.imp[1].banner.format).to.not.be.null; + expect(ortbRequest.imp[1].banner.format).to.have.lengthOf(1); + expect(ortbRequest.imp[1].banner.format[0].w).to.equal(728); + expect(ortbRequest.imp[1].banner.format[0].h).to.equal(90); // adsize on response const ortbResponse = { seatbid: [{ @@ -701,4 +724,58 @@ describe('PulsePoint Adapter Tests', function () { expect(bid.width).to.equal(728); expect(bid.height).to.equal(90); }); + it('Verify multi-format response', function () { + const bidRequests = deepClone(slotConfigs); + bidRequests[0].mediaTypes['native'] = { + title: { + required: true + }, + image: { + required: true + }, + sponsoredBy: { + required: true + } + }; + bidRequests[1].params.video = { + w: 400, + h: 300, + minduration: 5, + maxduration: 10, + }; + const request = spec.buildRequests(bidRequests, bidderRequest); + expect(request).to.be.not.null; + expect(request.data).to.be.not.null; + const ortbRequest = request.data; + expect(ortbRequest.imp).to.have.lengthOf(2); + // adsize on response + const ortbResponse = { + seatbid: [{ + bid: [{ + impid: ortbRequest.imp[0].id, + price: 1.25, + adm: 'This is an Ad', + crid: 'Creative#123', + w: 728, + h: 90 + }, { + impid: ortbRequest.imp[1].id, + price: 2.5, + adm: '', + crid: 'Creative#234', + w: 728, + h: 90 + }] + }] + }; + // request has both types - banner and native, response is parsed as banner. + // for impression#2, response is parsed as video + const bids = spec.interpretResponse({ body: ortbResponse }, request); + expect(bids).to.have.lengthOf(2); + const bid = bids[0]; + expect(bid.width).to.equal(728); + expect(bid.height).to.equal(90); + const secondBid = bids[1]; + expect(secondBid.vastXml).to.equal(''); + }); }); diff --git a/test/spec/modules/quantcastBidAdapter_spec.js b/test/spec/modules/quantcastBidAdapter_spec.js index cd168ec61e6..5b4e7963e60 100644 --- a/test/spec/modules/quantcastBidAdapter_spec.js +++ b/test/spec/modules/quantcastBidAdapter_spec.js @@ -7,7 +7,8 @@ import { QUANTCAST_TEST_PUBLISHER, QUANTCAST_PROTOCOL, QUANTCAST_PORT, - spec as qcSpec + spec as qcSpec, + storage } from '../../../modules/quantcastBidAdapter.js'; import { newBidder } from '../../../src/adapters/bidderFactory.js'; import { parseUrl } from 'src/utils.js'; @@ -42,6 +43,8 @@ describe('Quantcast adapter', function () { canonicalUrl: 'http://example.com/hello.html' } }; + + storage.setCookie('__qca', '', 'Thu, 01 Jan 1970 00:00:00 GMT'); }); function setupVideoBidRequest(videoParams) { @@ -140,7 +143,8 @@ describe('Quantcast adapter', function () { gdprSignal: 0, uspSignal: 0, coppa: 0, - prebidJsVersion: '$prebid.version$' + prebidJsVersion: '$prebid.version$', + fpa: '' }; it('sends banner bid requests contains all the required parameters', function () { @@ -208,7 +212,8 @@ describe('Quantcast adapter', function () { gdprSignal: 0, uspSignal: 0, coppa: 0, - prebidJsVersion: '$prebid.version$' + prebidJsVersion: '$prebid.version$', + fpa: '' }; expect(requests[0].data).to.equal(JSON.stringify(expectedVideoBidRequest)); @@ -244,7 +249,8 @@ describe('Quantcast adapter', function () { gdprSignal: 0, uspSignal: 0, coppa: 0, - prebidJsVersion: '$prebid.version$' + prebidJsVersion: '$prebid.version$', + fpa: '' }; expect(requests[0].data).to.equal(JSON.stringify(expectedVideoBidRequest)); @@ -276,7 +282,8 @@ describe('Quantcast adapter', function () { gdprSignal: 0, uspSignal: 0, coppa: 0, - prebidJsVersion: '$prebid.version$' + prebidJsVersion: '$prebid.version$', + fpa: '' }; expect(requests[0].data).to.equal(JSON.stringify(expectedVideoBidRequest)); @@ -340,7 +347,8 @@ describe('Quantcast adapter', function () { gdprSignal: 0, uspSignal: 0, coppa: 0, - prebidJsVersion: '$prebid.version$' + prebidJsVersion: '$prebid.version$', + fpa: '' }; expect(requests[0].data).to.equal(JSON.stringify(expectedBidRequest)); @@ -430,26 +438,6 @@ describe('Quantcast adapter', function () { expect(requests).to.equal(undefined); }); - it('allows TCF v2 request from Germany for purpose 1', function () { - const bidderRequest = { - gdprConsent: { - gdprApplies: true, - consentString: 'consentString', - vendorData: { - publisherCC: 'DE', - purposeOneTreatment: true - }, - apiVersion: 2 - } - }; - - const requests = qcSpec.buildRequests([bidRequest], bidderRequest); - const parsed = JSON.parse(requests[0].data); - - expect(parsed.gdprSignal).to.equal(1); - expect(parsed.gdprConsent).to.equal('consentString'); - }); - it('allows TCF v2 request when Quantcast has consent for purpose 1', function() { const bidderRequest = { gdprConsent: { @@ -604,6 +592,13 @@ describe('Quantcast adapter', function () { expect(parsed.uspConsent).to.equal('consentString'); }); + it('propagates Quantcast first-party cookie (fpa)', function() { + storage.setCookie('__qca', 'P0-TestFPA'); + const requests = qcSpec.buildRequests([bidRequest], bidderRequest); + const parsed = JSON.parse(requests[0].data); + expect(parsed.fpa).to.equal('P0-TestFPA'); + }); + describe('propagates coppa', function() { let sandbox; beforeEach(() => { diff --git a/test/spec/modules/quantcastIdSystem_spec.js b/test/spec/modules/quantcastIdSystem_spec.js new file mode 100644 index 00000000000..12c8689fd3f --- /dev/null +++ b/test/spec/modules/quantcastIdSystem_spec.js @@ -0,0 +1,19 @@ +import { quantcastIdSubmodule, storage } from 'modules/quantcastIdSystem.js'; + +describe('QuantcastId module', function () { + beforeEach(function() { + storage.setCookie('__qca', '', 'Thu, 01 Jan 1970 00:00:00 GMT'); + }); + + it('getId() should return a quantcast id when the Quantcast first party cookie exists', function () { + storage.setCookie('__qca', 'P0-TestFPA'); + + const id = quantcastIdSubmodule.getId(); + expect(id).to.be.deep.equal({id: {quantcastId: 'P0-TestFPA'}}); + }); + + it('getId() should return an empty id when the Quantcast first party cookie is missing', function () { + const id = quantcastIdSubmodule.getId(); + expect(id).to.be.deep.equal({id: undefined}); + }); +}); diff --git a/test/spec/modules/qwarryBidAdapter_spec.js b/test/spec/modules/qwarryBidAdapter_spec.js new file mode 100644 index 00000000000..06d4af0756c --- /dev/null +++ b/test/spec/modules/qwarryBidAdapter_spec.js @@ -0,0 +1,148 @@ +import { expect } from 'chai' +import { ENDPOINT, spec } from 'modules/qwarryBidAdapter.js' +import { newBidder } from 'src/adapters/bidderFactory.js' + +const REQUEST = { + 'bidId': '456', + 'bidder': 'qwarry', + 'sizes': [[100, 200], [300, 400]], + 'params': { + zoneToken: 'e64782a4-8e68-4c38-965b-80ccf115d46f', + pos: 7 + } +} + +const BIDDER_BANNER_RESPONSE = { + 'prebidResponse': [{ + 'ad': '
test
', + 'requestId': 'e64782a4-8e68-4c38-965b-80ccf115d46d', + 'cpm': 900.5, + 'currency': 'USD', + 'width': 640, + 'height': 480, + 'ttl': 300, + 'creativeId': 1, + 'netRevenue': true, + 'winUrl': 'http://test.com', + 'format': 'banner' + }] +} + +const BIDDER_VIDEO_RESPONSE = { + 'prebidResponse': [{ + 'ad': 'vast', + 'requestId': 'e64782a4-8e68-4c38-965b-80ccf115d46z', + 'cpm': 800.4, + 'currency': 'USD', + 'width': 1024, + 'height': 768, + 'ttl': 200, + 'creativeId': 2, + 'netRevenue': true, + 'winUrl': 'http://test.com', + 'format': 'video' + }] +} + +const BIDDER_NO_BID_RESPONSE = '' + +describe('qwarryBidAdapter', 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 () { + expect(spec.isBidRequestValid(REQUEST)).to.equal(true) + }) + + it('should return false when required params are not passed', function () { + let bid = Object.assign({}, REQUEST) + delete bid.params.zoneToken + expect(spec.isBidRequestValid(bid)).to.equal(false) + delete bid.params + expect(spec.isBidRequestValid(bid)).to.equal(false) + }) + }) + + describe('buildRequests', function () { + let bidRequests = [REQUEST] + const bidderRequest = spec.buildRequests(bidRequests, { + bidderRequestId: '123', + gdprConsent: { + gdprApplies: true, + consentString: 'BOJ8RZsOJ8RZsABAB8AAAAAZ+A==' + }, + refererInfo: { + referer: 'http://test.com/path.html' + } + }) + + it('sends bid request to ENDPOINT via POST', function () { + expect(bidderRequest.method).to.equal('POST') + expect(bidderRequest.data.requestId).to.equal('123') + expect(bidderRequest.data.referer).to.equal('http://test.com/path.html') + expect(bidderRequest.data.bids).to.deep.contains({ bidId: '456', zoneToken: 'e64782a4-8e68-4c38-965b-80ccf115d46f', pos: 7, sizes: [{ width: 100, height: 200 }, { width: 300, height: 400 }] }) + expect(bidderRequest.data.gdprConsent).to.deep.contains({ consentRequired: true, consentString: 'BOJ8RZsOJ8RZsABAB8AAAAAZ+A==' }) + expect(bidderRequest.options.customHeaders).to.deep.equal({ 'Rtb-Direct': true }) + expect(bidderRequest.options.contentType).to.equal('application/json') + expect(bidderRequest.url).to.equal(ENDPOINT) + }) + }) + + describe('interpretResponse', function () { + it('handles banner request : should get correct bid response', function () { + const result = spec.interpretResponse({ body: BIDDER_BANNER_RESPONSE }, {}) + + expect(result[0]).to.have.property('ad').equal('
test
') + expect(result[0]).to.have.property('requestId').equal('e64782a4-8e68-4c38-965b-80ccf115d46d') + expect(result[0]).to.have.property('cpm').equal(900.5) + expect(result[0]).to.have.property('currency').equal('USD') + expect(result[0]).to.have.property('width').equal(640) + expect(result[0]).to.have.property('height').equal(480) + expect(result[0]).to.have.property('ttl').equal(300) + expect(result[0]).to.have.property('creativeId').equal(1) + expect(result[0]).to.have.property('netRevenue').equal(true) + expect(result[0]).to.have.property('winUrl').equal('http://test.com') + expect(result[0]).to.have.property('format').equal('banner') + }) + + it('handles video request : should get correct bid response', function () { + const result = spec.interpretResponse({ body: BIDDER_VIDEO_RESPONSE }, {}) + + expect(result[0]).to.have.property('ad').equal('vast') + expect(result[0]).to.have.property('requestId').equal('e64782a4-8e68-4c38-965b-80ccf115d46z') + expect(result[0]).to.have.property('cpm').equal(800.4) + expect(result[0]).to.have.property('currency').equal('USD') + expect(result[0]).to.have.property('width').equal(1024) + expect(result[0]).to.have.property('height').equal(768) + expect(result[0]).to.have.property('ttl').equal(200) + expect(result[0]).to.have.property('creativeId').equal(2) + expect(result[0]).to.have.property('netRevenue').equal(true) + expect(result[0]).to.have.property('winUrl').equal('http://test.com') + expect(result[0]).to.have.property('format').equal('video') + expect(result[0]).to.have.property('vastXml').equal('vast') + }) + + it('handles no bid response : should get empty array', function () { + let result = spec.interpretResponse({ body: undefined }, {}) + expect(result).to.deep.equal([]) + + result = spec.interpretResponse({ body: BIDDER_NO_BID_RESPONSE }, {}) + expect(result).to.deep.equal([]) + }) + }) + + describe('onBidWon', function () { + it('handles banner win: should get true', function () { + const win = BIDDER_BANNER_RESPONSE.prebidResponse[0] + const bidWonResult = spec.onBidWon(win) + + expect(bidWonResult).to.equal(true) + }) + }) +}) diff --git a/test/spec/modules/radsBidAdapter_spec.js b/test/spec/modules/radsBidAdapter_spec.js index c629daf3da5..c3c3b4b2746 100644 --- a/test/spec/modules/radsBidAdapter_spec.js +++ b/test/spec/modules/radsBidAdapter_spec.js @@ -87,12 +87,25 @@ describe('radsAdapter', function () { 'auctionId': '1d1a030790a475' }]; + // Without gdprConsent let bidderRequest = { refererInfo: { referer: 'some_referrer.net' } } + // With gdprConsent + var bidderRequestGdprConsent = { + refererInfo: { + referer: 'some_referrer.net' + }, + gdprConsent: { + consentString: 'BOJ/P2HOJ/P2HABABMAAAAAZ+A==', + vendorData: {someData: 'value'}, + gdprApplies: true + } + }; + // without gdprConsent const request = spec.buildRequests(bidRequests, bidderRequest); it('sends bid request to our endpoint via GET', function () { expect(request[0].method).to.equal('GET'); @@ -105,6 +118,20 @@ describe('radsAdapter', function () { let data = request[1].data.replace(/rnd=\d+\&/g, '').replace(/ref=.*\&bid/g, 'bid'); expect(data).to.equal('rt=vast2&_f=prebid_js&_ps=6682&srw=640&srh=480&idt=100&p=some_referrer.net&bid_id=30b31c1838de1e&pfilter%5Bfloorprice%5D=1000000&pfilter%5Bgeo%5D%5Bcountry%5D=DE&pfilter%5Bgeo%5D%5Bregion%5D=DE-BE&bcat=IAB2%2CIAB4&dvt=desktop'); }); + + // with gdprConsent + const request2 = spec.buildRequests(bidRequests, bidderRequestGdprConsent); + it('sends bid request to our endpoint via GET', function () { + expect(request2[0].method).to.equal('GET'); + let data = request2[0].data.replace(/rnd=\d+\&/g, '').replace(/ref=.*\&bid/g, 'bid'); + expect(data).to.equal('rt=bid-response&_f=prebid_js&_ps=6682&srw=300&srh=250&idt=100&p=some_referrer.net&bid_id=30b31c1838de1e&pfilter%5Bfloorprice%5D=1000000&pfilter%5Bgeo%5D%5Bcountry%5D=DE&pfilter%5Bgdpr_consent%5D=BOJ%2FP2HOJ%2FP2HABABMAAAAAZ%2BA%3D%3D&pfilter%5Bgdpr%5D=true&bcat=IAB2%2CIAB4&dvt=desktop&i=1.1.1.1'); + }); + + it('sends bid video request to our rads endpoint via GET', function () { + expect(request2[1].method).to.equal('GET'); + let data = request2[1].data.replace(/rnd=\d+\&/g, '').replace(/ref=.*\&bid/g, 'bid'); + expect(data).to.equal('rt=vast2&_f=prebid_js&_ps=6682&srw=640&srh=480&idt=100&p=some_referrer.net&bid_id=30b31c1838de1e&pfilter%5Bfloorprice%5D=1000000&pfilter%5Bgeo%5D%5Bcountry%5D=DE&pfilter%5Bgeo%5D%5Bregion%5D=DE-BE&pfilter%5Bgdpr_consent%5D=BOJ%2FP2HOJ%2FP2HABABMAAAAAZ%2BA%3D%3D&pfilter%5Bgdpr%5D=true&bcat=IAB2%2CIAB4&dvt=desktop'); + }); }); describe('interpretResponse', function () { @@ -203,4 +230,60 @@ describe('radsAdapter', function () { expect(result.length).to.equal(0); }); }); + + describe(`getUserSyncs test usage`, function () { + let serverResponses; + + beforeEach(function () { + serverResponses = [{ + body: { + requestId: '23beaa6af6cdde', + cpm: 0.5, + width: 0, + height: 0, + creativeId: 100500, + dealId: '', + currency: 'EUR', + netRevenue: true, + ttl: 300, + type: 'sspHTML', + ad: '', + userSync: { + iframeUrl: ['anyIframeUrl?a=1'], + imageUrl: ['anyImageUrl', 'anyImageUrl2'] + } + } + }]; + }); + + it(`return value should be an array`, function () { + expect(spec.getUserSyncs({ iframeEnabled: true })).to.be.an('array'); + }); + it(`array should have only one object and it should have a property type = 'iframe'`, function () { + expect(spec.getUserSyncs({ iframeEnabled: true }, serverResponses).length).to.be.equal(1); + let [userSync] = spec.getUserSyncs({ iframeEnabled: true }, serverResponses); + expect(userSync).to.have.property('type'); + expect(userSync.type).to.be.equal('iframe'); + }); + it(`we have valid sync url for iframe`, function () { + let [userSync] = spec.getUserSyncs({ iframeEnabled: true }, serverResponses, {consentString: 'anyString'}); + expect(userSync.url).to.be.equal('anyIframeUrl?a=1&gdpr_consent=anyString') + expect(userSync.type).to.be.equal('iframe'); + }); + it(`we have valid sync url for image`, function () { + let [userSync] = spec.getUserSyncs({ pixelEnabled: true }, serverResponses, {gdprApplies: true, consentString: 'anyString'}); + expect(userSync.url).to.be.equal('anyImageUrl?gdpr=1&gdpr_consent=anyString') + expect(userSync.type).to.be.equal('image'); + }); + it(`we have valid sync url for image and iframe`, function () { + let userSync = spec.getUserSyncs({ iframeEnabled: true, pixelEnabled: true }, serverResponses, {gdprApplies: true, consentString: 'anyString'}); + expect(userSync.length).to.be.equal(3); + expect(userSync[0].url).to.be.equal('anyIframeUrl?a=1&gdpr=1&gdpr_consent=anyString') + expect(userSync[0].type).to.be.equal('iframe'); + expect(userSync[1].url).to.be.equal('anyImageUrl?gdpr=1&gdpr_consent=anyString') + expect(userSync[1].type).to.be.equal('image'); + expect(userSync[2].url).to.be.equal('anyImageUrl2?gdpr=1&gdpr_consent=anyString') + expect(userSync[2].type).to.be.equal('image'); + }); + }); }); diff --git a/test/spec/modules/readpeakBidAdapter_spec.js b/test/spec/modules/readpeakBidAdapter_spec.js index eb9077fac39..eefd7792a7c 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,20 +179,83 @@ 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.site).to.deep.equal({ - publisher: { - id: bidRequest.params.publisherId, - domain: 'http://localhost:9876' - }, - id: bidRequest.params.siteId, - page: bidderRequest.refererInfo.referer, - domain: parseUrl(bidderRequest.refererInfo.referer).hostname - }); + expect(data.imp[0].tagId).to.equal('test-tag-1'); + expect(data.site.publisher.id).to.equal(bidRequest.params.publisherId); + expect(data.site.id).to.equal(bidRequest.params.siteId); + expect(data.site.page).to.equal(bidderRequest.refererInfo.referer); + expect(data.site.domain).to.equal(parseUrl(bidderRequest.refererInfo.referer).hostname); expect(data.device).to.deep.contain({ ua: navigator.userAgent, language: navigator.language }); expect(data.cur).to.deep.equal(['EUR']); + expect(data.user).to.be.undefined; + expect(data.regs).to.be.undefined; + }); + + it('should get bid floor from module', function() { + const floorModuleData = { + currency: 'USD', + floor: 3.2, + } + bidRequest.getFloor = function () { + return floorModuleData + } + const request = spec.buildRequests([bidRequest], bidderRequest); + + const data = JSON.parse(request.data); + + expect(data.source.ext.prebid).to.equal('$prebid.version$'); + expect(data.id).to.equal(bidRequest.bidderRequestId); + expect(data.imp[0].bidfloor).to.equal(floorModuleData.floor); + expect(data.imp[0].bidfloorcur).to.equal(floorModuleData.currency); + }); + + it('should send gdpr data when gdpr does not apply', function() { + const gdprData = { + gdprConsent: { + gdprApplies: false, + consentString: undefined, + } + } + const request = spec.buildRequests([bidRequest], {...bidderRequest, ...gdprData}); + + const data = JSON.parse(request.data); + + expect(data.user).to.deep.equal({ + ext: { + consent: '' + } + }); + expect(data.regs).to.deep.equal({ + ext: { + gdpr: false + } + }); + }); + + it('should send gdpr data when gdpr applies', function() { + const tcString = 'sometcstring'; + const gdprData = { + gdprConsent: { + gdprApplies: true, + consentString: tcString + } + } + const request = spec.buildRequests([bidRequest], {...bidderRequest, ...gdprData}); + + const data = JSON.parse(request.data); + + expect(data.user).to.deep.equal({ + ext: { + consent: tcString + } + }); + expect(data.regs).to.deep.equal({ + ext: { + gdpr: true + } + }); }); }); @@ -215,6 +280,9 @@ describe('ReadPeakAdapter', function() { currency: serverResponse.cur }); + expect(bidResponse.meta).to.deep.equal({ + advertiserDomains: ['readpeak.com'], + }) expect(bidResponse.native.title).to.equal('Title'); expect(bidResponse.native.body).to.equal('Description'); expect(bidResponse.native.image).to.deep.equal({ diff --git a/test/spec/modules/realTimeDataModule_spec.js b/test/spec/modules/realTimeDataModule_spec.js new file mode 100644 index 00000000000..b84aef15feb --- /dev/null +++ b/test/spec/modules/realTimeDataModule_spec.js @@ -0,0 +1,160 @@ +import * as rtdModule from 'modules/rtdModule/index.js'; +import { config } from 'src/config.js'; +import * as sinon from 'sinon'; + +const getBidRequestDataSpy = sinon.spy(); + +const validSM = { + name: 'validSM', + init: () => { return true }, + getTargetingData: (adUnitsCodes) => { + return {'ad2': {'key': 'validSM'}} + }, + getBidRequestData: getBidRequestDataSpy +}; + +const validSMWait = { + name: 'validSMWait', + init: () => { return true }, + getTargetingData: (adUnitsCodes) => { + return {'ad1': {'key': 'validSMWait'}} + }, + getBidRequestData: getBidRequestDataSpy +}; + +const invalidSM = { + name: 'invalidSM' +}; + +const failureSM = { + name: 'failureSM', + init: () => { return false } +}; + +const nonConfSM = { + name: 'nonConfSM', + init: () => { return true } +}; + +const conf = { + 'realTimeData': { + 'auctionDelay': 100, + dataProviders: [ + { + 'name': 'validSMWait', + 'waitForIt': true, + }, + { + 'name': 'validSM', + 'waitForIt': false, + }, + { + 'name': 'invalidSM' + }, + { + 'name': 'failureSM' + }] + } +}; + +describe('Real time module', function () { + before(function () { + rtdModule.attachRealTimeDataProvider(validSM); + rtdModule.attachRealTimeDataProvider(invalidSM); + rtdModule.attachRealTimeDataProvider(failureSM); + rtdModule.attachRealTimeDataProvider(nonConfSM); + rtdModule.attachRealTimeDataProvider(validSMWait); + }); + + after(function () { + config.resetConfig(); + }); + + beforeEach(function () { + config.setConfig(conf); + }); + + it('should use only valid modules', function () { + rtdModule.init(config); + expect(rtdModule.subModules).to.eql([validSMWait, validSM]); + }); + + it('should be able to modify bid request', function (done) { + rtdModule.setBidRequestsData(() => { + assert(getBidRequestDataSpy.calledTwice); + assert(getBidRequestDataSpy.calledWith({bidRequest: {}})); + done(); + }, {bidRequest: {}}) + }); + + it('deep merge object', function () { + const obj1 = { + id1: { + key: 'value', + key2: 'value2' + }, + id2: { + k: 'v' + } + }; + const obj2 = { + id1: { + key3: 'value3' + } + }; + const obj3 = { + id3: { + key: 'value' + } + }; + const expected = { + id1: { + key: 'value', + key2: 'value2', + key3: 'value3' + }, + id2: { + k: 'v' + }, + id3: { + key: 'value' + } + }; + + const merged = rtdModule.deepMerge([obj1, obj2, obj3]); + assert.deepEqual(expected, merged); + }); + + it('sould place targeting on adUnits', function (done) { + const auction = { + adUnitCodes: ['ad1', 'ad2'], + adUnits: [ + { + code: 'ad1' + }, + { + code: 'ad2', + adserverTargeting: {preKey: 'preValue'} + } + ] + }; + + const expectedAdUnits = [ + { + code: 'ad1', + adserverTargeting: {key: 'validSMWait'} + }, + { + code: 'ad2', + adserverTargeting: { + preKey: 'preValue', + key: 'validSM' + } + } + ]; + + const adUnits = rtdModule.getAdUnitTargeting(auction); + assert.deepEqual(expectedAdUnits, adUnits) + done(); + }) +}); diff --git a/test/spec/modules/realTimeModule_spec.js b/test/spec/modules/realTimeModule_spec.js deleted file mode 100644 index ca149fe7a44..00000000000 --- a/test/spec/modules/realTimeModule_spec.js +++ /dev/null @@ -1,282 +0,0 @@ -import { - init, - requestBidsHook, - setTargetsAfterRequestBids, - deepMerge, - validateProviderDataForGPT -} from 'modules/rtdModule/index.js'; -import { - init as browsiInit, - addBrowsiTag, - isIdMatchingAdUnit, - setData, - getMacroId -} from 'modules/browsiRtdProvider.js'; -import { - init as audigentInit, - setData as setAudigentData -} from 'modules/audigentRtdProvider.js'; -import { config } from 'src/config.js'; -import { makeSlot } from '../integration/faker/googletag.js'; - -let expect = require('chai').expect; - -describe('Real time module', function () { - const conf = { - 'realTimeData': { - 'auctionDelay': 250, - dataProviders: [{ - 'name': 'browsi', - 'params': { - 'url': 'testUrl.com', - 'siteKey': 'testKey', - 'pubKey': 'testPub', - 'keyName': 'bv' - } - }, { - 'name': 'audigent' - }] - } - }; - - const predictions = { - p: { - 'browsiAd_2': { - 'w': [ - '/57778053/Browsi_Demo_Low', - '/57778053/Browsi_Demo_300x250' - ], - 'p': 0.07 - }, - 'browsiAd_1': { - 'w': [], - 'p': 0.06 - }, - 'browsiAd_3': { - 'w': [], - 'p': 0.53 - }, - 'browsiAd_4': { - 'w': [ - '/57778053/Browsi_Demo' - ], - 'p': 0.85 - } - } - }; - - const audigentSegments = { - audigent_segments: { 'a': 1, 'b': 2 } - } - - function getAdUnitMock(code = 'adUnit-code') { - return { - code, - mediaTypes: { banner: {}, native: {} }, - sizes: [[300, 200], [300, 600]], - bids: [{ bidder: 'sampleBidder', params: { placementId: 'banner-only-bidder' } }] - }; - } - - function createSlots() { - const slot1 = makeSlot({ code: '/57778053/Browsi_Demo_300x250', divId: 'browsiAd_1' }); - const slot2 = makeSlot({ code: '/57778053/Browsi', divId: 'browsiAd_1' }); - return [slot1, slot2]; - } - - describe('Real time module with browsi provider', function () { - afterEach(function () { - $$PREBID_GLOBAL$$.requestBids.removeAll(); - }); - - after(function () { - config.resetConfig(); - }); - - it('check module using bidsBackCallback', function () { - let adUnits1 = [getAdUnitMock('browsiAd_1')]; - let targeting = []; - init(config); - browsiInit(config); - config.setConfig(conf); - setData(predictions); - - // set slot - const slots = createSlots(); - window.googletag.pubads().setSlots(slots); - - function afterBidHook() { - slots.map(s => { - targeting = []; - s.getTargeting().map(value => { - targeting.push(Object.keys(value).toString()); - }); - }); - - expect(targeting.indexOf('bv')).to.be.greaterThan(-1); - } - setTargetsAfterRequestBids(afterBidHook, adUnits1, true); - }); - - it('check module using requestBidsHook', function () { - let adUnits1 = [getAdUnitMock('browsiAd_1')]; - let targeting = []; - let dataReceived = null; - - // set slot - const slotsB = createSlots(); - window.googletag.pubads().setSlots(slotsB); - - function afterBidHook(data) { - dataReceived = data; - slotsB.map(s => { - targeting = []; - s.getTargeting().map(value => { - targeting.push(Object.keys(value).toString()); - }); - }); - - expect(targeting.indexOf('bv')).to.be.greaterThan(-1); - dataReceived.adUnits.forEach(unit => { - unit.bids.forEach(bid => { - expect(bid.realTimeData).to.have.property('bv'); - }); - }); - } - requestBidsHook(afterBidHook, { adUnits: adUnits1 }); - }); - - it('check object deep merge', function () { - const obj1 = { - id1: { - key: 'value', - key2: 'value2' - }, - id2: { - k: 'v' - } - }; - const obj2 = { - id1: { - key3: 'value3' - } - }; - const obj3 = { - id3: { - key: 'value' - } - }; - const expected = { - id1: { - key: 'value', - key2: 'value2', - key3: 'value3' - }, - id2: { - k: 'v' - }, - id3: { - key: 'value' - } - }; - - const merged = deepMerge([obj1, obj2, obj3]); - assert.deepEqual(expected, merged); - }); - - it('check data validation for GPT targeting', function () { - // non strings values should be removed - const obj = { - valid: {'key': 'value'}, - invalid: {'key': ['value']}, - combine: { - 'a': 'value', - 'b': [] - } - }; - - const expected = { - valid: {'key': 'value'}, - invalid: {}, - combine: { - 'a': 'value', - } - }; - const validationResult = validateProviderDataForGPT(obj); - assert.deepEqual(expected, validationResult); - }); - - it('check browsi sub module', function () { - const script = addBrowsiTag('scriptUrl.com'); - expect(script.getAttribute('data-sitekey')).to.equal('testKey'); - expect(script.getAttribute('data-pubkey')).to.equal('testPub'); - expect(script.async).to.equal(true); - - const slots = createSlots(); - const test1 = isIdMatchingAdUnit(slots[0], ['/57778053/Browsi_Demo_300x250']); // true - const test2 = isIdMatchingAdUnit(slots[0], ['/57778053/Browsi_Demo_300x250', '/57778053/Browsi']); // true - const test3 = isIdMatchingAdUnit(slots[0], ['/57778053/Browsi_Demo_Low']); // false - const test4 = isIdMatchingAdUnit(slots[0], []); // true - - expect(test1).to.equal(true); - expect(test2).to.equal(true); - expect(test3).to.equal(false); - expect(test4).to.equal(true); - - // macro results - slots[0].setTargeting('test', ['test', 'value']); - // slot getTargeting doesn't act like GPT so we can't expect real value - const macroResult = getMacroId({p: '/'}, slots[0]); - expect(macroResult).to.equal('/57778053/Browsi_Demo_300x250/NA'); - - const macroResultB = getMacroId({}, slots[0]); - expect(macroResultB).to.equal('browsiAd_1'); - - const macroResultC = getMacroId({p: '', s: {s: 0, e: 1}}, slots[0]); - expect(macroResultC).to.equal('/'); - }) - }); - - describe('Real time module with Audigent provider', function () { - before(function () { - init(config); - audigentInit(config); - config.setConfig(conf); - setAudigentData(audigentSegments); - }); - - afterEach(function () { - $$PREBID_GLOBAL$$.requestBids.removeAll(); - config.resetConfig(); - }); - - it('check module using requestBidsHook', function () { - let adUnits1 = [getAdUnitMock('audigentAd_1')]; - let targeting = []; - let dataReceived = null; - - // set slot - const slotsB = createSlots(); - window.googletag.pubads().setSlots(slotsB); - - function afterBidHook(data) { - dataReceived = data; - slotsB.map(s => { - targeting = []; - s.getTargeting().map(value => { - targeting.push(Object.keys(value).toString()); - }); - }); - - dataReceived.adUnits.forEach(unit => { - unit.bids.forEach(bid => { - expect(bid.realTimeData).to.have.property('audigent_segments'); - expect(bid.realTimeData.audigent_segments).to.deep.equal(audigentSegments.audigent_segments); - }); - }); - } - - requestBidsHook(afterBidHook, { adUnits: adUnits1 }); - }); - }); -}); diff --git a/test/spec/modules/reconciliationRtdProvider_spec.js b/test/spec/modules/reconciliationRtdProvider_spec.js new file mode 100644 index 00000000000..6efe55ddf46 --- /dev/null +++ b/test/spec/modules/reconciliationRtdProvider_spec.js @@ -0,0 +1,221 @@ +import { + reconciliationSubmodule, + track, + getTopIFrameWin, + getSlotByWin +} from 'modules/reconciliationRtdProvider.js'; +import { makeSlot } from '../integration/faker/googletag.js'; +import * as utils from 'src/utils.js'; + +describe('Reconciliation Real time data submodule', function () { + const conf = { + dataProviders: [{ + 'name': 'reconciliation', + 'params': { + 'publisherMemberId': 'test_prebid_publisher' + }, + }] + }; + + let trackPostStub; + + beforeEach(function () { + trackPostStub = sinon.stub(track, 'trackPost'); + }); + + afterEach(function () { + trackPostStub.restore(); + }); + + describe('reconciliationSubmodule', function () { + describe('initialization', function () { + let utilsLogErrorSpy; + + before(function () { + utilsLogErrorSpy = sinon.spy(utils, 'logError'); + }); + + after(function () { + utils.logError.restore(); + }); + + it('successfully instantiates', function () { + expect(reconciliationSubmodule.init(conf.dataProviders[0])).to.equal(true); + }); + + it('should log error if initializied without parameters', function () { + expect(reconciliationSubmodule.init({'name': 'reconciliation', 'params': {}})).to.equal(true); + expect(utilsLogErrorSpy.calledOnce).to.be.true; + }); + }); + + describe('getData', function () { + it('should return data in proper format', function () { + makeSlot({code: '/reconciliationAdunit1', divId: 'reconciliationAd1'}); + + const targetingData = reconciliationSubmodule.getTargetingData(['/reconciliationAdunit1']); + expect(targetingData['/reconciliationAdunit1'].RSDK_AUID).to.eql('/reconciliationAdunit1'); + expect(targetingData['/reconciliationAdunit1'].RSDK_ADID).to.be.a('string'); + }); + + it('should return unit path if called with divId', function () { + makeSlot({code: '/reconciliationAdunit2', divId: 'reconciliationAd2'}); + + const targetingData = reconciliationSubmodule.getTargetingData(['reconciliationAd2']); + expect(targetingData['reconciliationAd2'].RSDK_AUID).to.eql('/reconciliationAdunit2'); + expect(targetingData['reconciliationAd2'].RSDK_ADID).to.be.a('string'); + }); + + it('should skip empty adUnit id', function () { + makeSlot({code: '/reconciliationAdunit3', divId: 'reconciliationAd3'}); + + const targetingData = reconciliationSubmodule.getTargetingData(['reconciliationAd3', '']); + expect(targetingData).to.have.all.keys('reconciliationAd3'); + }); + }); + + describe('track events', function () { + it('should track init event with data', function () { + const adUnit = { + code: '/adunit' + }; + + reconciliationSubmodule.getTargetingData([adUnit.code]); + + expect(trackPostStub.calledOnce).to.be.true; + expect(trackPostStub.getCalls()[0].args[0]).to.eql('https://confirm.fiduciadlt.com/init'); + expect(trackPostStub.getCalls()[0].args[1].adUnits[0].adUnitId).to.eql(adUnit.code); + expect(trackPostStub.getCalls()[0].args[1].adUnits[0].adDeliveryId).be.a('string'); + expect(trackPostStub.getCalls()[0].args[1].publisherMemberId).to.eql('test_prebid_publisher'); + }); + }); + + describe('get topmost iframe', function () { + /** + * - top + * -- iframe.window <-- top iframe window + * --- iframe.window + * ---- iframe.window <-- win + */ + const mockFrameWin = (topWin, parentWin) => { + return { + top: topWin, + parent: parentWin + } + } + + it('should return null if called with null', function() { + expect(getTopIFrameWin(null)).to.be.null; + }); + + it('should return null if there is an error in frames chain', function() { + const topWin = {}; + const iframe1Win = mockFrameWin(topWin, null); // break chain + const iframe2Win = mockFrameWin(topWin, iframe1Win); + + expect(getTopIFrameWin(iframe1Win, topWin)).to.be.null; + }); + + it('should get the topmost iframe', function () { + const topWin = {}; + const iframe1Win = mockFrameWin(topWin, topWin); + const iframe2Win = mockFrameWin(topWin, iframe1Win); + + expect(getTopIFrameWin(iframe2Win, topWin)).to.eql(iframe1Win); + }); + }); + + describe('get slot by nested iframe window', function () { + it('should return the slot', function () { + const adSlotElement = document.createElement('div'); + const adSlotIframe = document.createElement('iframe'); + + adSlotElement.id = 'reconciliationAd'; + adSlotElement.appendChild(adSlotIframe); + document.body.appendChild(adSlotElement); + + const adSlot = makeSlot({code: '/reconciliationAdunit', divId: adSlotElement.id}); + + expect(getSlotByWin(adSlotIframe.contentWindow)).to.eql(adSlot); + }); + + it('should return null if the slot is not found', function () { + const adSlotElement = document.createElement('div'); + const adSlotIframe = document.createElement('iframe'); + + adSlotElement.id = 'reconciliationAd'; + document.body.appendChild(adSlotElement); + document.body.appendChild(adSlotIframe); // iframe is not in ad slot + + const adSlot = makeSlot({code: '/reconciliationAdunit', divId: adSlotElement.id}); + + expect(getSlotByWin(adSlotIframe.contentWindow)).to.be.null; + }); + }); + + describe('handle postMessage from Reconciliation Tag in ad iframe', function () { + it('should track impression pixel with parameters', function (done) { + const adSlotElement = document.createElement('div'); + const adSlotIframe = document.createElement('iframe'); + + adSlotElement.id = 'reconciliationAdMessage'; + adSlotElement.appendChild(adSlotIframe); + document.body.appendChild(adSlotElement); + + const adSlot = makeSlot({code: '/reconciliationAdunit', divId: adSlotElement.id}); + // Fix targeting methods + adSlot.targeting = {}; + adSlot.setTargeting = function(key, value) { + this.targeting[key] = [value]; + }; + adSlot.getTargeting = function(key) { + return this.targeting[key]; + }; + + adSlot.setTargeting('RSDK_AUID', '/reconciliationAdunit'); + adSlot.setTargeting('RSDK_ADID', '12345'); + adSlotIframe.contentDocument.open(); + adSlotIframe.contentDocument.write(``); + adSlotIframe.contentDocument.close(); + + setTimeout(() => { + expect(trackPostStub.calledOnce).to.be.true; + expect(trackPostStub.getCalls()[0].args[0]).to.eql('https://confirm.fiduciadlt.com/pimp'); + expect(trackPostStub.getCalls()[0].args[1].adUnitId).to.eql('/reconciliationAdunit'); + expect(trackPostStub.getCalls()[0].args[1].adDeliveryId).to.eql('12345'); + expect(trackPostStub.getCalls()[0].args[1].tagOwnerMemberId).to.eql('test_member_id'); ; + expect(trackPostStub.getCalls()[0].args[1].dataSources.length).to.eql(1); + expect(trackPostStub.getCalls()[0].args[1].dataRecipients.length).to.eql(2); + expect(trackPostStub.getCalls()[0].args[1].publisherMemberId).to.eql('test_prebid_publisher'); + done(); + }, 100); + }); + }); + }); +}); diff --git a/test/spec/modules/relaidoBidAdapter_spec.js b/test/spec/modules/relaidoBidAdapter_spec.js index cd4918460db..91aa6b05e6e 100644 --- a/test/spec/modules/relaidoBidAdapter_spec.js +++ b/test/spec/modules/relaidoBidAdapter_spec.js @@ -1,16 +1,20 @@ import { expect } from 'chai'; import { spec } from 'modules/relaidoBidAdapter.js'; import * as utils from 'src/utils.js'; +import { getStorageManager } from '../../../src/storageManager.js'; const UUID_KEY = 'relaido_uuid'; const DEFAULT_USER_AGENT = window.navigator.userAgent; const MOBILE_USER_AGENT = 'Mozilla/5.0 (iPhone; CPU iPhone OS 13_3_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0.5 Mobile/15E148 Safari/604.1'; +const relaido_uuid = 'hogehoge'; const setUADefault = () => { window.navigator.__defineGetter__('userAgent', function () { return DEFAULT_USER_AGENT }) }; const setUAMobile = () => { window.navigator.__defineGetter__('userAgent', function () { return MOBILE_USER_AGENT }) }; +const storage = getStorageManager(); +storage.setCookie(UUID_KEY, relaido_uuid); + describe('RelaidoAdapter', function () { - const relaido_uuid = 'hogehoge'; let bidRequest; let bidderRequest; let serverResponse; @@ -65,7 +69,6 @@ describe('RelaidoAdapter', function () { height: bidRequest.mediaTypes.video.playerSize[0][1], mediaType: 'video', }; - localStorage.setItem(UUID_KEY, relaido_uuid); }); describe('spec.isBidRequestValid', function () { @@ -86,10 +89,58 @@ describe('RelaidoAdapter', function () { setUADefault(); }); - it('should return false when the uuid are missing', function () { - localStorage.removeItem(UUID_KEY); - const result = !!(utils.isSafariBrowser()); - expect(spec.isBidRequestValid(bidRequest)).to.equal(result); + it('should return false when missing 300x250 over and 1x1 by banner', function () { + setUAMobile(); + bidRequest.mediaTypes = { + banner: { + sizes: [ + [100, 100], + [300, 100] + ] + } + }; + expect(spec.isBidRequestValid(bidRequest)).to.equal(false); + setUADefault(); + }); + + it('should return true when 300x250 by banner', function () { + setUAMobile(); + bidRequest.mediaTypes = { + banner: { + sizes: [ + [300, 250] + ] + } + }; + expect(spec.isBidRequestValid(bidRequest)).to.equal(true); + setUADefault(); + }); + + it('should return true when 1x1 by banner', function () { + setUAMobile(); + bidRequest.mediaTypes = { + banner: { + sizes: [ + [1, 1] + ] + } + }; + expect(spec.isBidRequestValid(bidRequest)).to.equal(true); + setUADefault(); + }); + + it('should return true when 300x250 over by banner', function () { + setUAMobile(); + bidRequest.mediaTypes = { + banner: { + sizes: [ + [100, 100], + [300, 250] + ] + } + }; + expect(spec.isBidRequestValid(bidRequest)).to.equal(true); + setUADefault(); }); it('should return false when the placementId params are missing', function () { @@ -157,10 +208,18 @@ describe('RelaidoAdapter', function () { }); it('should build bid requests by banner', function () { + setUAMobile(); bidRequest.mediaTypes = { + video: { + context: 'outstream', + playerSize: [ + [320, 180] + ] + }, banner: { sizes: [ - [640, 360] + [640, 360], + [1, 1] ] } }; @@ -170,6 +229,31 @@ describe('RelaidoAdapter', function () { expect(request.mediaType).to.equal('banner'); }); + it('should take 1x1 size', function () { + setUAMobile(); + bidRequest.mediaTypes = { + video: { + context: 'outstream', + playerSize: [ + [320, 180] + ] + }, + banner: { + sizes: [ + [640, 360], + [1, 1] + ] + } + }; + const bidRequests = spec.buildRequests([bidRequest], bidderRequest); + expect(bidRequests).to.have.lengthOf(1); + const request = bidRequests[0]; + + // eslint-disable-next-line no-console + console.log(bidRequests); + expect(request.width).to.equal(1); + }); + it('The referrer should be the last', function () { const bidRequests = spec.buildRequests([bidRequest], bidderRequest); expect(bidRequests).to.have.lengthOf(1); 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/rhythmoneBidAdapter_spec.js b/test/spec/modules/rhythmoneBidAdapter_spec.js index d9342332e61..93735c019e1 100644 --- a/test/spec/modules/rhythmoneBidAdapter_spec.js +++ b/test/spec/modules/rhythmoneBidAdapter_spec.js @@ -157,6 +157,7 @@ describe('rhythmone adapter tests', function () { expect(bid.width).to.equal(800); expect(bid.height).to.equal(600); expect(bid.vastUrl).to.equal('https://testdomain/rmp/placementid/0/path?reqId=1636037'); + expect(bid.meta.advertiserDomains).to.deep.equal(['test.com']); expect(bid.mediaType).to.equal('video'); expect(bid.creativeId).to.equal('cr-vid'); expect(bid.currency).to.equal('USD'); diff --git a/test/spec/modules/richaudienceBidAdapter_spec.js b/test/spec/modules/richaudienceBidAdapter_spec.js index f18e67a3ac5..72410b71fb2 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: 'key1=value1;key2=value2' }, auctionId: '0cb3144c-d084-4686-b0d6-f5dbe917c563', bidRequestsCount: 1, @@ -75,26 +75,19 @@ describe('Richaudience adapter tests', function () { user: {} }]; - var DEFAULT_PARAMS_VIDEO_OUT_PARAMS = [{ + var DEFAULT_PARAMS_BANNER_OUTSTREAM = [{ adUnitCode: 'test-div', bidId: '2c7c8e9c900244', mediaTypes: { - video: { - context: 'outstream', - playerSize: [640, 480], - mimes: ['video/mp4'] + banner: { + sizes: [[300, 250], [600, 300]] } }, bidder: 'richaudience', params: { bidfloor: 0.5, pid: 'ADb1f40rmi', - supplyType: 'site', - player: { - init: 'close', - end: 'close', - skin: 'dark' - } + supplyType: 'site' }, auctionId: '0cb3144c-d084-4686-b0d6-f5dbe917c563', bidRequestsCount: 1, @@ -240,6 +233,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('key1=value1;key2=value2'); }) it('Verify build request to prebid video inestream', function() { @@ -259,8 +255,6 @@ describe('Richaudience adapter tests', function () { expect(requestContent).to.have.property('demand').and.to.equal('video'); expect(requestContent.videoData).to.have.property('format').and.to.equal('instream'); - // expect(requestContent.videoData.playerSize[0][0]).to.equal('640'); - // expect(requestContent.videoData.playerSize[0][0]).to.equal('480'); }) it('Verify build request to prebid video outstream', function() { @@ -278,6 +272,7 @@ describe('Richaudience adapter tests', function () { expect(request[0]).to.have.property('method').and.to.equal('POST'); const requestContent = JSON.parse(request[0].data); + expect(requestContent).to.have.property('demand').and.to.equal('video'); expect(requestContent.videoData).to.have.property('format').and.to.equal('outstream'); }) @@ -348,34 +343,49 @@ describe('Richaudience adapter tests', function () { }); describe('UID test', function () { - pbjs.setConfig({ + config.setConfig({ consentManagement: { cmpApi: 'iab', timeout: 5000, allowAuctionWithoutConsent: true + }, + userSync: { + userIds: [{ + 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 + }, + storage: { + 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 + } + }], + auctionDelay: 50 // 50ms maximum auction delay, applies to all userId modules } }); it('Verify build id5', function () { + var request; DEFAULT_PARAMS_WO_OPTIONAL[0].userId = {}; - DEFAULT_PARAMS_WO_OPTIONAL[0].userId.id5id = 'id5-user-id'; - - var request = spec.buildRequests(DEFAULT_PARAMS_WO_OPTIONAL, DEFAULT_PARAMS_GDPR); + DEFAULT_PARAMS_WO_OPTIONAL[0].userId.id5id = { uid: 1 }; + request = spec.buildRequests(DEFAULT_PARAMS_WO_OPTIONAL, DEFAULT_PARAMS_GDPR); var requestContent = JSON.parse(request[0].data); - expect(requestContent.user).to.deep.equal([{ - 'userId': 'id5-user-id', - 'source': 'id5-sync.com' - }]); + expect(requestContent.user.eids).to.equal(undefined); - var request; - DEFAULT_PARAMS_WO_OPTIONAL[0].userId = {}; - DEFAULT_PARAMS_WO_OPTIONAL[0].userId.id5id = 1; + DEFAULT_PARAMS_WO_OPTIONAL[0].userId.id5id = { uid: [] }; request = spec.buildRequests(DEFAULT_PARAMS_WO_OPTIONAL, DEFAULT_PARAMS_GDPR); - var requestContent = JSON.parse(request[0].data); + requestContent = JSON.parse(request[0].data); + expect(requestContent.user.eids).to.equal(undefined); + DEFAULT_PARAMS_WO_OPTIONAL[0].userId.id5id = { uid: null }; + request = spec.buildRequests(DEFAULT_PARAMS_WO_OPTIONAL, DEFAULT_PARAMS_GDPR); + requestContent = JSON.parse(request[0].data); expect(requestContent.user.eids).to.equal(undefined); - DEFAULT_PARAMS_WO_OPTIONAL[0].userId.id5id = []; + DEFAULT_PARAMS_WO_OPTIONAL[0].userId.id5id = { uid: {} }; request = spec.buildRequests(DEFAULT_PARAMS_WO_OPTIONAL, DEFAULT_PARAMS_GDPR); requestContent = JSON.parse(request[0].data); expect(requestContent.user.eids).to.equal(undefined); @@ -605,9 +615,19 @@ describe('Richaudience adapter tests', function () { }); const bids = spec.interpretResponse(BID_RESPONSE_VIDEO, request[0]); + expect(bids).to.have.lengthOf(1); const bid = bids[0]; + expect(bid.cpm).to.equal(1.50); expect(bid.mediaType).to.equal('video'); expect(bid.vastXml).to.equal(''); + expect(bid.cpm).to.equal(1.50); + expect(bid.width).to.equal(1); + expect(bid.height).to.equal(1); + expect(bid.creativeId).to.equal('189198063'); + expect(bid.netRevenue).to.equal(true); + expect(bid.currency).to.equal('USD'); + expect(bid.ttl).to.equal(300); + expect(bid.dealId).to.equal('dealId'); }); it('no banner media response outstream', function () { @@ -622,6 +642,35 @@ describe('Richaudience adapter tests', function () { } }); + const bids = spec.interpretResponse(BID_RESPONSE_VIDEO, request[0]); + expect(bids).to.have.lengthOf(1); + const bid = bids[0]; + expect(bid.cpm).to.equal(1.50); + expect(bid.mediaType).to.equal('video'); + expect(bid.vastXml).to.equal(''); + expect(bid.renderer.url).to.equal('https://cdn3.richaudience.com/prebidVideo/player.js'); + expect(bid.cpm).to.equal(1.50); + expect(bid.width).to.equal(1); + expect(bid.height).to.equal(1); + expect(bid.creativeId).to.equal('189198063'); + expect(bid.netRevenue).to.equal(true); + expect(bid.currency).to.equal('USD'); + expect(bid.ttl).to.equal(300); + expect(bid.dealId).to.equal('dealId'); + }); + + it('banner media and response VAST', function () { + const request = spec.buildRequests(DEFAULT_PARAMS_BANNER_OUTSTREAM, { + gdprConsent: { + consentString: 'BOZcQl_ObPFjWAeABAESCD-AAAAjx7_______9______9uz_Ov_v_f__33e8__9v_l_7_-___u_-33d4-_1vf99yfm1-7ftr3tp_87ues2_Xur__59__3z3_NohBgA', + gdprApplies: true + }, + refererInfo: { + referer: 'https://domain.com', + numIframes: 0 + } + }); + const bids = spec.interpretResponse(BID_RESPONSE_VIDEO, request[0]); const bid = bids[0]; expect(bid.mediaType).to.equal('video'); @@ -638,6 +687,16 @@ describe('Richaudience adapter tests', function () { expect(spec.aliases[0]).to.equal('ra'); }); + it('Verifies bidder gvlid', function () { + expect(spec.gvlid).to.equal(108); + }); + + it('Verifies bidder supportedMediaTypes', function () { + expect(spec.supportedMediaTypes).to.have.lengthOf(2); + expect(spec.supportedMediaTypes[0]).to.equal('banner'); + expect(spec.supportedMediaTypes[1]).to.equal('video'); + }); + it('Verifies if bid request is valid', function () { expect(spec.isBidRequestValid(DEFAULT_PARAMS_NEW_SIZES[0])).to.equal(true); expect(spec.isBidRequestValid(DEFAULT_PARAMS_WO_OPTIONAL[0])).to.equal(true); @@ -699,78 +758,373 @@ describe('Richaudience adapter tests', function () { bidfloor: 0.50, } })).to.equal(true); + expect(spec.isBidRequestValid({ + params: { + pid: ['1gCB5ZC4XL', '1a40xk8qSV'], + bidfloor: 0.50, + } + })).to.equal(false); + expect(spec.isBidRequestValid({ + params: { + pid: ['1gCB5ZC4XL', '1a40xk8qSV'], + supplyType: 'site', + bidfloor: 0.50, + } + })).to.equal(true); + expect(spec.isBidRequestValid({ + params: { + supplyType: 'site', + bidfloor: 0.50, + ifa: 'AAAAAAAAA-BBBB-CCCC-1111-222222220000', + } + })).to.equal(false); + expect(spec.isBidRequestValid({ + params: { + pid: ['1gCB5ZC4XL', '1a40xk8qSV'], + supplyType: 'site', + bidfloor: 0.50, + ifa: 'AAAAAAAAA-BBBB-CCCC-1111-222222220000', + } + })).to.equal(true); }); - it('Verifies user syncs iframe', function () { - var syncs = spec.getUserSyncs({ - iframeEnabled: true - }, [BID_RESPONSE], { - consentString: 'BOZcQl_ObPFjWAeABAESCD-AAAAjx7_______9______9uz_Ov_v_f__33e8__9v_l_7_-___u_-33d4-_1vf99yfm1-7ftr3tp_87ues2_Xur__59__3z3_NohBgA', - gdprApplies: true + describe('userSync', function () { + it('Verifies user syncs iframe include', function () { + config.setConfig({ + 'userSync': {filterSettings: {iframe: {bidders: '*', filter: 'include'}}} + }) + + var syncs = spec.getUserSyncs({ + iframeEnabled: true + }, [BID_RESPONSE], { + consentString: 'BOZcQl_ObPFjWAeABAESCD-AAAAjx7_______9______9uz_Ov_v_f__33e8__9v_l_7_-___u_-33d4-_1vf99yfm1-7ftr3tp_87ues2_Xur__59__3z3_NohBgA', + gdprApplies: true}, + ); + expect(syncs).to.have.lengthOf(1); + expect(syncs[0].type).to.equal('iframe'); + + syncs = spec.getUserSyncs({ + iframeEnabled: false, + }, [BID_RESPONSE], { + consentString: 'BOZcQl_ObPFjWAeABAESCD-AAAAjx7_______9______9uz_Ov_v_f__33e8__9v_l_7_-___u_-33d4-_1vf99yfm1-7ftr3tp_87ues2_Xur__59__3z3_NohBgA', + gdprApplies: true, + }); + expect(syncs).to.have.lengthOf(0); + + syncs = spec.getUserSyncs({ + iframeEnabled: false, + }, [BID_RESPONSE], { + consentString: 'BOZcQl_ObPFjWAeABAESCD-AAAAjx7_______9______9uz_Ov_v_f__33e8__9v_l_7_-___u_-33d4-_1vf99yfm1-7ftr3tp_87ues2_Xur__59__3z3_NohBgA', + gdprApplies: true + }); + expect(syncs).to.have.lengthOf(0); + + syncs = spec.getUserSyncs({ + iframeEnabled: true, + }, [], {consentString: '', gdprApplies: false}); + expect(syncs).to.have.lengthOf(1); + + syncs = spec.getUserSyncs({ + iframeEnabled: false, + }, [], {consentString: '', gdprApplies: true}); + expect(syncs).to.have.lengthOf(0); }); + it('Verifies user syncs iframe exclude', function () { + config.setConfig({ + 'userSync': {filterSettings: {iframe: {bidders: '*', filter: 'exclude'}}} + }) - expect(syncs).to.have.lengthOf(1); - expect(syncs[0].type).to.equal('iframe'); - syncs = spec.getUserSyncs({ - iframeEnabled: false - }, [BID_RESPONSE], { - consentString: 'BOZcQl_ObPFjWAeABAESCD-AAAAjx7_______9______9uz_Ov_v_f__33e8__9v_l_7_-___u_-33d4-_1vf99yfm1-7ftr3tp_87ues2_Xur__59__3z3_NohBgA', - gdprApplies: true + var syncs = spec.getUserSyncs({ + iframeEnabled: true + }, [BID_RESPONSE], { + consentString: 'BOZcQl_ObPFjWAeABAESCD-AAAAjx7_______9______9uz_Ov_v_f__33e8__9v_l_7_-___u_-33d4-_1vf99yfm1-7ftr3tp_87ues2_Xur__59__3z3_NohBgA', + gdprApplies: true}, + ); + expect(syncs).to.have.lengthOf(0); + + syncs = spec.getUserSyncs({ + iframeEnabled: false, + }, [BID_RESPONSE], { + consentString: 'BOZcQl_ObPFjWAeABAESCD-AAAAjx7_______9______9uz_Ov_v_f__33e8__9v_l_7_-___u_-33d4-_1vf99yfm1-7ftr3tp_87ues2_Xur__59__3z3_NohBgA', + gdprApplies: true, + }); + expect(syncs).to.have.lengthOf(0); + + syncs = spec.getUserSyncs({ + iframeEnabled: false, + }, [BID_RESPONSE], { + consentString: 'BOZcQl_ObPFjWAeABAESCD-AAAAjx7_______9______9uz_Ov_v_f__33e8__9v_l_7_-___u_-33d4-_1vf99yfm1-7ftr3tp_87ues2_Xur__59__3z3_NohBgA', + gdprApplies: true + }); + expect(syncs).to.have.lengthOf(0); + + syncs = spec.getUserSyncs({ + iframeEnabled: true, + }, [], {consentString: '', gdprApplies: false}); + expect(syncs).to.have.lengthOf(0); + + syncs = spec.getUserSyncs({ + iframeEnabled: false, + }, [], {consentString: '', gdprApplies: true}); + expect(syncs).to.have.lengthOf(0); }); - expect(syncs).to.have.lengthOf(0); - syncs = spec.getUserSyncs({ - iframeEnabled: true - }, [], {consentString: '', gdprApplies: false}); - expect(syncs).to.have.lengthOf(1); + it('Verifies user syncs image include', function () { + config.setConfig({ + 'userSync': {filterSettings: {image: {bidders: '*', filter: 'include'}}} + }) - syncs = spec.getUserSyncs({ - iframeEnabled: false - }, [], {consentString: '', gdprApplies: true}); - expect(syncs).to.have.lengthOf(0); + var syncs = spec.getUserSyncs({ + iframeEnabled: false, + pixelEnabled: true + }, [BID_RESPONSE], { + consentString: 'BOZcQl_ObPFjWAeABAESCD-AAAAjx7_______9______9uz_Ov_v_f__33e8__9v_l_7_-___u_-33d4-_1vf99yfm1-7ftr3tp_87ues2_Xur__59__3z3_NohBgA', + referer: 'http://domain.com', + gdprApplies: true + }) + expect(syncs).to.have.lengthOf(1); + expect(syncs[0].type).to.equal('image'); - pbjs.setConfig({ - consentManagement: { - cmpApi: 'iab', - timeout: 5000, - allowAuctionWithoutConsent: true, + syncs = spec.getUserSyncs({ + iframeEnabled: false, pixelEnabled: true - } + }, [BID_RESPONSE], { + consentString: '', + referer: 'http://domain.com', + gdprApplies: true + }) + expect(syncs).to.have.lengthOf(1); + expect(syncs[0].type).to.equal('image'); + + syncs = spec.getUserSyncs({ + iframeEnabled: false, + pixelEnabled: true + }, [], { + consentString: null, + referer: 'http://domain.com', + gdprApplies: true + }) + expect(syncs).to.have.lengthOf(1); + expect(syncs[0].type).to.equal('image'); }); - }); - it('Verifies user syncs image', function () { - var syncs = spec.getUserSyncs({ - iframeEnabled: false, - pixelEnabled: true - }, [BID_RESPONSE], { - consentString: 'BOZcQl_ObPFjWAeABAESCD-AAAAjx7_______9______9uz_Ov_v_f__33e8__9v_l_7_-___u_-33d4-_1vf99yfm1-7ftr3tp_87ues2_Xur__59__3z3_NohBgA', - referer: 'http://domain.com', - gdprApplies: true - }) - expect(syncs).to.have.lengthOf(1); - expect(syncs[0].type).to.equal('image'); - - syncs = spec.getUserSyncs({ - iframeEnabled: false, - pixelEnabled: true - }, [BID_RESPONSE], { - consentString: '', - referer: 'http://domain.com', - gdprApplies: true - }) - expect(syncs).to.have.lengthOf(1); - expect(syncs[0].type).to.equal('image'); - - syncs = spec.getUserSyncs({ - iframeEnabled: false, - pixelEnabled: true - }, [], { - consentString: null, - referer: 'http://domain.com', - gdprApplies: true - }) - expect(syncs).to.have.lengthOf(1); - expect(syncs[0].type).to.equal('image'); - }); + it('Verifies user syncs image exclude', function () { + config.setConfig({ + 'userSync': {filterSettings: {image: {bidders: '*', filter: 'exclude'}}} + }) + + var syncs = spec.getUserSyncs({ + iframeEnabled: false, + pixelEnabled: true + }, [BID_RESPONSE], { + consentString: 'BOZcQl_ObPFjWAeABAESCD-AAAAjx7_______9______9uz_Ov_v_f__33e8__9v_l_7_-___u_-33d4-_1vf99yfm1-7ftr3tp_87ues2_Xur__59__3z3_NohBgA', + referer: 'http://domain.com', + gdprApplies: true + }) + expect(syncs).to.have.lengthOf(0); + + syncs = spec.getUserSyncs({ + iframeEnabled: false, + pixelEnabled: true + }, [BID_RESPONSE], { + consentString: '', + referer: 'http://domain.com', + gdprApplies: true + }) + expect(syncs).to.have.lengthOf(0); + + syncs = spec.getUserSyncs({ + iframeEnabled: false, + pixelEnabled: true + }, [], { + consentString: null, + referer: 'http://domain.com', + gdprApplies: true + }) + expect(syncs).to.have.lengthOf(0); + }); + + it('Verifies user syncs iframe/image include', function () { + config.setConfig({ + 'userSync': {filterSettings: {iframe: {bidders: '*', filter: 'include'}, image: {bidders: '*', filter: 'include'}}} + }) + + var syncs = spec.getUserSyncs({ + iframeEnabled: true, + pixelEnabled: true + }, [BID_RESPONSE], { + consentString: 'BOZcQl_ObPFjWAeABAESCD-AAAAjx7_______9______9uz_Ov_v_f__33e8__9v_l_7_-___u_-33d4-_1vf99yfm1-7ftr3tp_87ues2_Xur__59__3z3_NohBgA', + gdprApplies: true}, + ); + expect(syncs).to.have.lengthOf(1); + expect(syncs[0].type).to.equal('iframe'); + + syncs = spec.getUserSyncs({ + iframeEnabled: false, + pixelEnabled: false + }, [BID_RESPONSE], { + consentString: 'BOZcQl_ObPFjWAeABAESCD-AAAAjx7_______9______9uz_Ov_v_f__33e8__9v_l_7_-___u_-33d4-_1vf99yfm1-7ftr3tp_87ues2_Xur__59__3z3_NohBgA', + gdprApplies: true, + }); + expect(syncs).to.have.lengthOf(0); + + syncs = spec.getUserSyncs({ + iframeEnabled: false, + pixelEnabled: false + }, [BID_RESPONSE], { + consentString: 'BOZcQl_ObPFjWAeABAESCD-AAAAjx7_______9______9uz_Ov_v_f__33e8__9v_l_7_-___u_-33d4-_1vf99yfm1-7ftr3tp_87ues2_Xur__59__3z3_NohBgA', + gdprApplies: true + }); + expect(syncs).to.have.lengthOf(0); + + syncs = spec.getUserSyncs({ + iframeEnabled: true, + pixelEnabled: true + }, [], {consentString: '', gdprApplies: false}); + expect(syncs).to.have.lengthOf(1); + + syncs = spec.getUserSyncs({ + iframeEnabled: false, + pixelEnabled: false + }, [], {consentString: '', gdprApplies: true}); + expect(syncs).to.have.lengthOf(0); + }); + + it('Verifies user syncs iframe/image exclude', function () { + config.setConfig({ + 'userSync': {filterSettings: {iframe: {bidders: '*', filter: 'exclude'}, image: {bidders: '*', filter: 'exclude'}}} + }) + + var syncs = spec.getUserSyncs({ + iframeEnabled: true, + pixelEnabled: true + }, [BID_RESPONSE], { + consentString: 'BOZcQl_ObPFjWAeABAESCD-AAAAjx7_______9______9uz_Ov_v_f__33e8__9v_l_7_-___u_-33d4-_1vf99yfm1-7ftr3tp_87ues2_Xur__59__3z3_NohBgA', + gdprApplies: true}, + ); + expect(syncs).to.have.lengthOf(0); + + syncs = spec.getUserSyncs({ + iframeEnabled: false, + pixelEnabled: false + }, [BID_RESPONSE], { + consentString: 'BOZcQl_ObPFjWAeABAESCD-AAAAjx7_______9______9uz_Ov_v_f__33e8__9v_l_7_-___u_-33d4-_1vf99yfm1-7ftr3tp_87ues2_Xur__59__3z3_NohBgA', + gdprApplies: true, + }); + expect(syncs).to.have.lengthOf(0); + + syncs = spec.getUserSyncs({ + iframeEnabled: false, + pixelEnabled: false + }, [BID_RESPONSE], { + consentString: 'BOZcQl_ObPFjWAeABAESCD-AAAAjx7_______9______9uz_Ov_v_f__33e8__9v_l_7_-___u_-33d4-_1vf99yfm1-7ftr3tp_87ues2_Xur__59__3z3_NohBgA', + gdprApplies: true + }); + expect(syncs).to.have.lengthOf(0); + + syncs = spec.getUserSyncs({ + iframeEnabled: true, + pixelEnabled: true + }, [], {consentString: '', gdprApplies: false}); + expect(syncs).to.have.lengthOf(0); + + syncs = spec.getUserSyncs({ + iframeEnabled: false, + pixelEnabled: false + }, [], {consentString: '', gdprApplies: true}); + expect(syncs).to.have.lengthOf(0); + }); + + it('Verifies user syncs iframe exclude / image include', function () { + config.setConfig({ + 'userSync': {filterSettings: {iframe: {bidders: '*', filter: 'exclude'}, image: {bidders: '*', filter: 'include'}}} + }) + + var syncs = spec.getUserSyncs({ + iframeEnabled: true, + pixelEnabled: true + }, [BID_RESPONSE], { + consentString: 'BOZcQl_ObPFjWAeABAESCD-AAAAjx7_______9______9uz_Ov_v_f__33e8__9v_l_7_-___u_-33d4-_1vf99yfm1-7ftr3tp_87ues2_Xur__59__3z3_NohBgA', + gdprApplies: true}, + ); + expect(syncs).to.have.lengthOf(1); + expect(syncs[0].type).to.equal('image'); + + syncs = spec.getUserSyncs({ + iframeEnabled: false, + pixelEnabled: false + }, [BID_RESPONSE], { + consentString: 'BOZcQl_ObPFjWAeABAESCD-AAAAjx7_______9______9uz_Ov_v_f__33e8__9v_l_7_-___u_-33d4-_1vf99yfm1-7ftr3tp_87ues2_Xur__59__3z3_NohBgA', + gdprApplies: true, + }); + expect(syncs).to.have.lengthOf(0); + + syncs = spec.getUserSyncs({ + iframeEnabled: false, + pixelEnabled: false + }, [BID_RESPONSE], { + consentString: 'BOZcQl_ObPFjWAeABAESCD-AAAAjx7_______9______9uz_Ov_v_f__33e8__9v_l_7_-___u_-33d4-_1vf99yfm1-7ftr3tp_87ues2_Xur__59__3z3_NohBgA', + gdprApplies: true + }); + expect(syncs).to.have.lengthOf(0); + + syncs = spec.getUserSyncs({ + iframeEnabled: true, + pixelEnabled: true + }, [], {consentString: '', gdprApplies: false}); + expect(syncs).to.have.lengthOf(1); + + syncs = spec.getUserSyncs({ + iframeEnabled: false, + pixelEnabled: false + }, [], {consentString: '', gdprApplies: true}); + expect(syncs).to.have.lengthOf(0); + }); + + it('Verifies user syncs iframe include / image exclude', function () { + config.setConfig({ + 'userSync': {filterSettings: {iframe: {bidders: '*', filter: 'include'}, image: {bidders: '*', filter: 'exclude'}}} + }) + + var syncs = spec.getUserSyncs({ + iframeEnabled: true, + pixelEnabled: true + }, [BID_RESPONSE], { + consentString: 'BOZcQl_ObPFjWAeABAESCD-AAAAjx7_______9______9uz_Ov_v_f__33e8__9v_l_7_-___u_-33d4-_1vf99yfm1-7ftr3tp_87ues2_Xur__59__3z3_NohBgA', + gdprApplies: true}, + ); + expect(syncs).to.have.lengthOf(1); + expect(syncs[0].type).to.equal('iframe'); + + syncs = spec.getUserSyncs({ + iframeEnabled: false, + pixelEnabled: false + }, [BID_RESPONSE], { + consentString: 'BOZcQl_ObPFjWAeABAESCD-AAAAjx7_______9______9uz_Ov_v_f__33e8__9v_l_7_-___u_-33d4-_1vf99yfm1-7ftr3tp_87ues2_Xur__59__3z3_NohBgA', + gdprApplies: true, + }); + expect(syncs).to.have.lengthOf(0); + + syncs = spec.getUserSyncs({ + iframeEnabled: false, + pixelEnabled: false + }, [BID_RESPONSE], { + consentString: 'BOZcQl_ObPFjWAeABAESCD-AAAAjx7_______9______9uz_Ov_v_f__33e8__9v_l_7_-___u_-33d4-_1vf99yfm1-7ftr3tp_87ues2_Xur__59__3z3_NohBgA', + gdprApplies: true + }); + expect(syncs).to.have.lengthOf(0); + + syncs = spec.getUserSyncs({ + iframeEnabled: true, + pixelEnabled: true + }, [], {consentString: '', gdprApplies: false}); + expect(syncs).to.have.lengthOf(1); + + syncs = spec.getUserSyncs({ + iframeEnabled: false, + pixelEnabled: false + }, [], {consentString: '', gdprApplies: true}); + expect(syncs).to.have.lengthOf(0); + }); + }) }); diff --git a/test/spec/modules/riseBidAdapter_spec.js b/test/spec/modules/riseBidAdapter_spec.js new file mode 100644 index 00000000000..b3257cbda9d --- /dev/null +++ b/test/spec/modules/riseBidAdapter_spec.js @@ -0,0 +1,381 @@ +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', + } + + const customSessionId = '12345678'; + + 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 the is_wrapper query param', function () { + bidRequests[0].params.isWrapper = true; + const requests = spec.buildRequests(bidRequests, bidderRequest); + for (const request of requests) { + expect(request.data.is_wrapper).to.equal(true); + } + }); + + it('sends the custom session id as a query param', function () { + bidRequests[0].params.sessionId = customSessionId; + const requests = spec.buildRequests(bidRequests, bidderRequest); + for (const request of requests) { + expect(request.data.session_id).to.equal(customSessionId); + } + }); + + 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/rtbhouseBidAdapter_spec.js b/test/spec/modules/rtbhouseBidAdapter_spec.js index efaed87dd15..d6bee26d73b 100644 --- a/test/spec/modules/rtbhouseBidAdapter_spec.js +++ b/test/spec/modules/rtbhouseBidAdapter_spec.js @@ -51,38 +51,7 @@ describe('RTBHouseAdapter', () => { }); describe('buildRequests', function () { - let bidRequests = [ - { - 'bidder': 'rtbhouse', - 'params': { - 'publisherId': 'PREBID_TEST', - 'region': 'prebid-eu', - 'test': 1 - }, - 'adUnitCode': 'adunit-code', - 'mediaTypes': { - 'banner': { - 'sizes': [[300, 250], [300, 600]], - } - }, - 'bidId': '30b31c1838de1e', - 'bidderRequestId': '22edbae2733bf6', - 'auctionId': '1d1a030790a475', - 'transactionId': 'example-transaction-id', - 'schain': { - 'ver': '1.0', - 'complete': 1, - 'nodes': [ - { - 'asi': 'directseller.com', - 'sid': '00001', - 'rid': 'BidRequest1', - 'hp': 1 - } - ] - } - } - ]; + let bidRequests; const bidderRequest = { 'refererInfo': { 'numIframes': 0, @@ -92,6 +61,41 @@ describe('RTBHouseAdapter', () => { } }; + beforeEach(() => { + bidRequests = [ + { + 'bidder': 'rtbhouse', + 'params': { + 'publisherId': 'PREBID_TEST', + 'region': 'prebid-eu', + 'test': 1 + }, + 'adUnitCode': 'adunit-code', + 'mediaTypes': { + 'banner': { + 'sizes': [[300, 250], [300, 600]], + } + }, + 'bidId': '30b31c1838de1e', + 'bidderRequestId': '22edbae2733bf6', + 'auctionId': '1d1a030790a475', + 'transactionId': 'example-transaction-id', + 'schain': { + 'ver': '1.0', + 'complete': 1, + 'nodes': [ + { + 'asi': 'directseller.com', + 'sid': '00001', + 'rid': 'BidRequest1', + 'hp': 1 + } + ] + } + } + ]; + }); + it('should build test param into the request', () => { let builtTestRequest = spec.buildRequests(bidRequests, bidderRequest).data; expect(JSON.parse(builtTestRequest).test).to.equal(1); @@ -177,6 +181,23 @@ describe('RTBHouseAdapter', () => { expect(data.source.tid).to.equal('example-transaction-id'); }); + it('should include bidfloor from floor module if avaiable', () => { + const bidRequest = Object.assign([], bidRequests); + bidRequest[0].getFloor = () => ({floor: 1.22}); + const request = spec.buildRequests(bidRequest, bidderRequest); + const data = JSON.parse(request.data); + expect(data.imp[0].bidfloor).to.equal(1.22) + }); + + it('should use bidfloor from floor module if both floor module and bid floor avaiable', () => { + const bidRequest = Object.assign([], bidRequests); + bidRequest[0].getFloor = () => ({floor: 1.22}); + bidRequest[0].params.bidfloor = 0.01; + const request = spec.buildRequests(bidRequest, bidderRequest); + const data = JSON.parse(request.data); + expect(data.imp[0].bidfloor).to.equal(1.22) + }); + it('should include bidfloor in request if available', () => { const bidRequest = Object.assign([], bidRequests); bidRequest[0].params.bidfloor = 0.01; @@ -185,11 +206,11 @@ describe('RTBHouseAdapter', () => { expect(data.imp[0].bidfloor).to.equal(0.01) }); - it('should include source.ext.schain in request', () => { + it('should include schain in request', () => { const bidRequest = Object.assign([], bidRequests); const request = spec.buildRequests(bidRequest, bidderRequest); const data = JSON.parse(request.data); - expect(data.source.ext.schain).to.deep.equal({ + expect(data.ext.schain).to.deep.equal({ 'ver': '1.0', 'complete': 1, 'nodes': [ @@ -203,6 +224,13 @@ describe('RTBHouseAdapter', () => { }); }); + it('should include source.tid in request', () => { + const bidRequest = Object.assign([], bidRequests); + const request = spec.buildRequests(bidRequest, bidderRequest); + const data = JSON.parse(request.data); + expect(data.source).to.have.deep.property('tid'); + }); + it('should not include invalid schain', () => { const bidRequest = Object.assign([], bidRequests); bidRequest[0].schain = { @@ -424,6 +452,7 @@ describe('RTBHouseAdapter', () => { 'mediaType': 'banner', 'currency': 'USD', 'ttl': 300, + 'meta': { advertiserDomains: ['rtbhouse.com'] }, 'netRevenue': true } ]; @@ -486,6 +515,7 @@ describe('RTBHouseAdapter', () => { it('should contain native assets in valid format', () => { const bids = spec.interpretResponse({body: response}, {}); + expect(bids[0].meta.advertiserDomains).to.deep.equal(['rtbhouse.com']); expect(bids[0].native).to.deep.equal({ title: 'Title text', clickUrl: encodeURIComponent('https://example.com'), diff --git a/test/spec/modules/rubiconAnalyticsAdapter_spec.js b/test/spec/modules/rubiconAnalyticsAdapter_spec.js index 0c2c83a4b37..df8bf03b56a 100644 --- a/test/spec/modules/rubiconAnalyticsAdapter_spec.js +++ b/test/spec/modules/rubiconAnalyticsAdapter_spec.js @@ -2,16 +2,17 @@ import rubiconAnalyticsAdapter, { SEND_TIMEOUT, parseBidResponse, getHostNameFromReferer, + storage, + rubiConf, } from 'modules/rubiconAnalyticsAdapter.js'; import CONSTANTS from 'src/constants.json'; import { config } from 'src/config.js'; import { server } from 'test/mocks/xhr.js'; - +import * as mockGpt from '../integration/faker/googletag.js'; import { setConfig, addBidResponseHook, } from 'modules/currency.js'; - let Ajv = require('ajv'); let schema = require('./rubiconAnalyticsSchema.json'); let ajv = new Ajv({ @@ -110,10 +111,85 @@ const BID2 = Object.assign({}, BID, { } }); +const BID3 = Object.assign({}, BID, { + adUnitCode: '/19968336/siderail-tag1', + bidId: '5fg6hyy4r879f0', + adId: 'fake_ad_id', + requestId: '5fg6hyy4r879f0', + width: 300, + height: 250, + mediaType: 'banner', + cpm: 2.01, + source: 'server', + seatBidId: 'aaaa-bbbb-cccc-dddd', + rubiconTargeting: { + 'rpfl_elemid': '/19968336/siderail-tag1', + 'rpfl_14062': '15_tier0200' + }, + adserverTargeting: { + 'hb_bidder': 'rubicon', + 'hb_adid': '5fg6hyy4r879f0', + 'hb_pb': '2.00', + 'hb_size': '300x250', + 'hb_source': 'server' + } +}); + +const BID4 = Object.assign({}, BID, { + adUnitCode: '/19968336/header-bid-tag1', + bidId: '3bd4ebb1c900e2', + adId: 'fake_ad_id', + requestId: '3bd4ebb1c900e2', + width: 728, + height: 90, + mediaType: 'banner', + cpm: 1.52, + source: 'server', + pbsBidId: 'zzzz-yyyy-xxxx-wwww', + seatBidId: 'aaaa-bbbb-cccc-dddd', + rubiconTargeting: { + 'rpfl_elemid': '/19968336/header-bid-tag1', + 'rpfl_14062': '2_tier0100' + }, + adserverTargeting: { + 'hb_bidder': 'rubicon', + 'hb_adid': '3bd4ebb1c900e2', + 'hb_pb': '1.500', + 'hb_size': '728x90', + 'hb_source': 'server' + } +}); + +const floorMinRequest = { + 'bidder': 'rubicon', + 'params': { + 'accountId': '14062', + 'siteId': '70608', + 'zoneId': '335918', + 'userId': '12346', + 'keywords': ['a', 'b', 'c'], + 'inventory': {'rating': '4-star', 'prodtype': 'tech'}, + 'visitor': {'ucat': 'new', 'lastsearch': 'iphone'}, + 'position': 'atf' + }, + 'mediaTypes': { + 'banner': { + 'sizes': [[300, 250]] + } + }, + 'adUnitCode': '/19968336/siderail-tag1', + 'transactionId': 'c435626g-9e3f-401a-bee1-d56aec29a1d4', + 'sizes': [[300, 250]], + 'bidId': '5fg6hyy4r879f0', + 'bidderRequestId': '1be65d7958826a', + 'auctionId': '25c6d7f5-699a-4bfc-87c9-996f915341fa' +}; + const MOCK = { SET_TARGETING: { [BID.adUnitCode]: BID.adserverTargeting, - [BID2.adUnitCode]: BID2.adserverTargeting + [BID2.adUnitCode]: BID2.adserverTargeting, + [BID3.adUnitCode]: BID3.adserverTargeting }, AUCTION_INIT: { 'auctionId': '25c6d7f5-699a-4bfc-87c9-996f915341fa', @@ -204,7 +280,8 @@ const MOCK = { 'sizes': [[640, 480]], 'bidId': '2ecff0db240757', 'bidderRequestId': '1be65d7958826a', - 'auctionId': '25c6d7f5-699a-4bfc-87c9-996f915341fa' + 'auctionId': '25c6d7f5-699a-4bfc-87c9-996f915341fa', + 'src': 'client' }, { 'bidder': 'rubicon', @@ -228,7 +305,8 @@ const MOCK = { 'sizes': [[1000, 300], [970, 250], [728, 90]], 'bidId': '3bd4ebb1c900e2', 'bidderRequestId': '1be65d7958826a', - 'auctionId': '25c6d7f5-699a-4bfc-87c9-996f915341fa' + 'auctionId': '25c6d7f5-699a-4bfc-87c9-996f915341fa', + 'src': 's2s' } ], 'auctionStart': 1519149536560, @@ -240,7 +318,8 @@ const MOCK = { }, BID_RESPONSE: [ BID, - BID2 + BID2, + BID3 ], AUCTION_END: { 'auctionId': '25c6d7f5-699a-4bfc-87c9-996f915341fa' @@ -251,14 +330,21 @@ const MOCK = { }), Object.assign({}, BID2, { 'status': 'rendered' + }), + Object.assign({}, BID3, { + 'status': 'rendered' }) ], BIDDER_DONE: { 'bidderCode': 'rubicon', + 'serverResponseTimeMs': 42, 'bids': [ BID, Object.assign({}, BID2, { 'serverResponseTimeMs': 42, + }), + Object.assign({}, BID3, { + 'serverResponseTimeMs': 55, }) ] }, @@ -272,14 +358,23 @@ const MOCK = { ] }; +const STUBBED_UUID = '12345678-1234-1234-1234-123456789abc'; + const ANALYTICS_MESSAGE = { + 'channel': 'web', 'eventTimeMillis': 1519767013781, 'integration': 'pbjs', 'version': '$prebid.version$', 'referrerUri': 'http://www.test.com/page.html', + 'session': { + 'expires': 1519788613781, + 'id': STUBBED_UUID, + 'start': 1519767013781 + }, 'referrerHostname': 'www.test.com', 'auctions': [ { + 'requestId': '25c6d7f5-699a-4bfc-87c9-996f915341fa', 'clientTimeoutMillis': 3000, 'serverTimeoutMillis': 1000, 'accountId': 1001, @@ -463,16 +558,23 @@ const ANALYTICS_MESSAGE = { 'bidwonStatus': 'success' } ], - 'wrapperName': '10000_fakewrapper_test' + 'wrapper': { + 'name': '10000_fakewrapper_test' + } }; -function performStandardAuction() { +function performStandardAuction(gptEvents) { events.emit(AUCTION_INIT, MOCK.AUCTION_INIT); events.emit(BID_REQUESTED, MOCK.BID_REQUESTED); events.emit(BID_RESPONSE, MOCK.BID_RESPONSE[0]); events.emit(BID_RESPONSE, MOCK.BID_RESPONSE[1]); events.emit(BIDDER_DONE, MOCK.BIDDER_DONE); events.emit(AUCTION_END, MOCK.AUCTION_END); + + if (gptEvents && gptEvents.length) { + gptEvents.forEach(gptEvent => mockGpt.emitEvent(gptEvent.eventName, gptEvent.params)); + } + events.emit(SET_TARGETING, MOCK.SET_TARGETING); events.emit(BID_WON, MOCK.BID_WON[0]); events.emit(BID_WON, MOCK.BID_WON[1]); @@ -481,12 +583,20 @@ function performStandardAuction() { describe('rubicon analytics adapter', function () { let sandbox; let clock; - + let getDataFromLocalStorageStub, setDataInLocalStorageStub, localStorageIsEnabledStub; beforeEach(function () { + getDataFromLocalStorageStub = sinon.stub(storage, 'getDataFromLocalStorage'); + setDataInLocalStorageStub = sinon.stub(storage, 'setDataInLocalStorage'); + localStorageIsEnabledStub = sinon.stub(storage, 'localStorageIsEnabled'); + mockGpt.disable(); sandbox = sinon.sandbox.create(); + localStorageIsEnabledStub.returns(true); + sandbox.stub(events, 'getEvents').returns([]); + sandbox.stub(utils, 'generateUUID').returns(STUBBED_UUID); + clock = sandbox.useFakeTimers(1519767013781); rubiconAnalyticsAdapter.referrerHostname = ''; @@ -505,6 +615,10 @@ describe('rubicon analytics adapter', function () { afterEach(function () { sandbox.restore(); config.resetConfig(); + mockGpt.enable(); + getDataFromLocalStorageStub.restore(); + setDataInLocalStorageStub.restore(); + localStorageIsEnabledStub.restore(); }); it('should require accountId', function () { @@ -531,6 +645,76 @@ describe('rubicon analytics adapter', function () { expect(utils.logError.called).to.equal(true); }); + describe('config subscribe', function() { + it('should update the pvid if user asks', function () { + expect(utils.generateUUID.called).to.equal(false); + config.setConfig({rubicon: {updatePageView: true}}); + expect(utils.generateUUID.called).to.equal(true); + }); + it('should merge in and preserve older set configs', function () { + config.setConfig({ + rubicon: { + wrapperName: '1001_general', + int_type: 'dmpbjs', + fpkvs: { + source: 'fb' + } + } + }); + expect(rubiConf).to.deep.equal({ + analyticsEventDelay: 0, + pvid: '12345678', + wrapperName: '1001_general', + int_type: 'dmpbjs', + fpkvs: { + source: 'fb' + }, + updatePageView: true + }); + + // update it with stuff + config.setConfig({ + rubicon: { + fpkvs: { + link: 'email' + } + } + }); + expect(rubiConf).to.deep.equal({ + analyticsEventDelay: 0, + pvid: '12345678', + wrapperName: '1001_general', + int_type: 'dmpbjs', + fpkvs: { + source: 'fb', + link: 'email' + }, + updatePageView: true + }); + + // overwriting specific edge keys should update them + config.setConfig({ + rubicon: { + fpkvs: { + link: 'iMessage', + source: 'twitter' + } + } + }); + expect(rubiConf).to.deep.equal({ + analyticsEventDelay: 0, + pvid: '12345678', + wrapperName: '1001_general', + int_type: 'dmpbjs', + fpkvs: { + link: 'iMessage', + source: 'twitter' + }, + updatePageView: true + }); + }); + }); + describe('sampling', function () { beforeEach(function () { sandbox.stub(Math, 'random').returns(0.08); @@ -659,11 +843,111 @@ describe('rubicon analytics adapter', function () { expect(message).to.deep.equal(ANALYTICS_MESSAGE); }); + it('should handle bidResponse dimensions correctly', function () { + events.emit(AUCTION_INIT, MOCK.AUCTION_INIT); + events.emit(BID_REQUESTED, MOCK.BID_REQUESTED); + + // mock bid response with playerWidth and playerHeight (NO width and height) + let bidResponse1 = utils.deepClone(MOCK.BID_RESPONSE[0]); + delete bidResponse1.width; + delete bidResponse1.height; + bidResponse1.playerWidth = 640; + bidResponse1.playerHeight = 480; + + // mock bid response with no width height or playerwidth playerheight + let bidResponse2 = utils.deepClone(MOCK.BID_RESPONSE[1]); + delete bidResponse2.width; + delete bidResponse2.height; + delete bidResponse2.playerWidth; + delete bidResponse2.playerHeight; + + events.emit(BID_RESPONSE, bidResponse1); + events.emit(BID_RESPONSE, bidResponse2); + events.emit(BIDDER_DONE, MOCK.BIDDER_DONE); + events.emit(AUCTION_END, MOCK.AUCTION_END); + events.emit(SET_TARGETING, MOCK.SET_TARGETING); + events.emit(BID_WON, MOCK.BID_WON[0]); + events.emit(BID_WON, MOCK.BID_WON[1]); + + let message = JSON.parse(server.requests[0].requestBody); + validate(message); + expect(message.auctions[0].adUnits[0].bids[0].bidResponse.dimensions).to.deep.equal({ + width: 640, + height: 480 + }); + expect(message.auctions[0].adUnits[1].bids[0].bidResponse.dimensions).to.equal(undefined); + }); + + it('should pass along adomians correctly', function () { + events.emit(AUCTION_INIT, MOCK.AUCTION_INIT); + events.emit(BID_REQUESTED, MOCK.BID_REQUESTED); + + // 1 adomains + let bidResponse1 = utils.deepClone(MOCK.BID_RESPONSE[0]); + bidResponse1.meta = { + advertiserDomains: ['magnite.com'] + } + + // two adomains + let bidResponse2 = utils.deepClone(MOCK.BID_RESPONSE[1]); + bidResponse2.meta = { + advertiserDomains: ['prebid.org', 'magnite.com'] + } + + // make sure we only pass max 10 adomains + bidResponse2.meta.advertiserDomains = [...bidResponse2.meta.advertiserDomains, ...bidResponse2.meta.advertiserDomains, ...bidResponse2.meta.advertiserDomains, ...bidResponse2.meta.advertiserDomains, ...bidResponse2.meta.advertiserDomains, ...bidResponse2.meta.advertiserDomains, ...bidResponse2.meta.advertiserDomains] + + events.emit(BID_RESPONSE, bidResponse1); + events.emit(BID_RESPONSE, bidResponse2); + events.emit(BIDDER_DONE, MOCK.BIDDER_DONE); + events.emit(AUCTION_END, MOCK.AUCTION_END); + events.emit(SET_TARGETING, MOCK.SET_TARGETING); + events.emit(BID_WON, MOCK.BID_WON[0]); + events.emit(BID_WON, MOCK.BID_WON[1]); + + let message = JSON.parse(server.requests[0].requestBody); + validate(message); + expect(message.auctions[0].adUnits[0].bids[0].bidResponse.adomains).to.deep.equal(['magnite.com']); + expect(message.auctions[0].adUnits[1].bids[0].bidResponse.adomains).to.deep.equal(['prebid.org', 'magnite.com', 'prebid.org', 'magnite.com', 'prebid.org', 'magnite.com', 'prebid.org', 'magnite.com', 'prebid.org', 'magnite.com']); + }); + + it('should NOT pass along adomians correctly when edge cases', function () { + events.emit(AUCTION_INIT, MOCK.AUCTION_INIT); + events.emit(BID_REQUESTED, MOCK.BID_REQUESTED); + + // empty => nothing + let bidResponse1 = utils.deepClone(MOCK.BID_RESPONSE[0]); + bidResponse1.meta = { + advertiserDomains: [] + } + + // not array => nothing + let bidResponse2 = utils.deepClone(MOCK.BID_RESPONSE[1]); + bidResponse2.meta = { + advertiserDomains: 'prebid.org' + } + + events.emit(BID_RESPONSE, bidResponse1); + events.emit(BID_RESPONSE, bidResponse2); + events.emit(BIDDER_DONE, MOCK.BIDDER_DONE); + events.emit(AUCTION_END, MOCK.AUCTION_END); + events.emit(SET_TARGETING, MOCK.SET_TARGETING); + events.emit(BID_WON, MOCK.BID_WON[0]); + events.emit(BID_WON, MOCK.BID_WON[1]); + + let message = JSON.parse(server.requests[0].requestBody); + validate(message); + expect(message.auctions[0].adUnits[0].bids[0].bidResponse.adomains).to.be.undefined; + expect(message.auctions[0].adUnits[1].bids[0].bidResponse.adomains).to.be.undefined; + }); + function performFloorAuction(provider) { let auctionInit = utils.deepClone(MOCK.AUCTION_INIT); auctionInit.bidderRequests[0].bids[0].floorData = { skipped: false, modelVersion: 'someModelName', + modelWeight: 10, + modelTimestamp: 1606772895, location: 'setConfig', skipRate: 15, fetchStatus: 'error', @@ -714,14 +998,40 @@ describe('rubicon analytics adapter', function () { } }; + let floorMinResponse = { + ...BID3, + floorData: { + floorValue: 1.5, + floorRuleValue: 1, + floorRule: '12345/entertainment|banner', + floorCurrency: 'USD', + cpmAfterAdjustments: 2.00, + enforcements: { + enforceJS: true, + enforcePBS: false, + floorDeals: false, + bidAdjustment: true + }, + matchedFields: { + gptSlot: '12345/entertainment', + mediaType: 'banner' + } + } + }; + + let bidRequest = utils.deepClone(MOCK.BID_REQUESTED); + bidRequest.bids.push(floorMinRequest) + // spoof the auction with just our duplicates events.emit(AUCTION_INIT, auctionInit); - events.emit(BID_REQUESTED, MOCK.BID_REQUESTED); + events.emit(BID_REQUESTED, bidRequest); events.emit(BID_RESPONSE, flooredResponse); events.emit(BID_RESPONSE, notFlooredResponse); + events.emit(BID_RESPONSE, floorMinResponse); events.emit(AUCTION_END, MOCK.AUCTION_END); events.emit(SET_TARGETING, MOCK.SET_TARGETING); events.emit(BID_WON, MOCK.BID_WON[1]); + events.emit(BID_WON, MOCK.BID_WON[2]); clock.tick(SEND_TIMEOUT + 1000); expect(server.requests.length).to.equal(1); @@ -732,13 +1042,15 @@ describe('rubicon analytics adapter', function () { } it('should capture price floor information correctly', function () { - let message = performFloorAuction('rubicon') + let message = performFloorAuction('rubicon'); // verify our floor stuff is passed // top level floor info expect(message.auctions[0].floors).to.deep.equal({ location: 'setConfig', modelName: 'someModelName', + modelWeight: 10, + modelTimestamp: 1606772895, skipped: false, enforcement: true, dealsEnforced: false, @@ -747,33 +1059,45 @@ describe('rubicon analytics adapter', function () { provider: 'rubicon' }); // first adUnit's adSlot - expect(message.auctions[0].adUnits[0].adSlot).to.equal('12345/sports'); + expect(message.auctions[0].adUnits[0].gam.adSlot).to.equal('12345/sports'); // since no other bids, we set adUnit status to no-bid expect(message.auctions[0].adUnits[0].status).to.equal('no-bid'); // first adUnits bid is rejected - expect(message.auctions[0].adUnits[0].bids[0].status).to.equal('rejected'); + expect(message.auctions[0].adUnits[0].bids[0].status).to.equal('rejected-ipf'); expect(message.auctions[0].adUnits[0].bids[0].bidResponse.floorValue).to.equal(4); // if bid rejected should take cpmAfterAdjustments val expect(message.auctions[0].adUnits[0].bids[0].bidResponse.bidPriceUSD).to.equal(2.1); // second adUnit's adSlot - expect(message.auctions[0].adUnits[1].adSlot).to.equal('12345/news'); + expect(message.auctions[0].adUnits[1].gam.adSlot).to.equal('12345/news'); // top level adUnit status is success expect(message.auctions[0].adUnits[1].status).to.equal('success'); // second adUnits bid is success expect(message.auctions[0].adUnits[1].bids[0].status).to.equal('success'); expect(message.auctions[0].adUnits[1].bids[0].bidResponse.floorValue).to.equal(1); expect(message.auctions[0].adUnits[1].bids[0].bidResponse.bidPriceUSD).to.equal(1.52); + + // second adUnit's adSlot + expect(message.auctions[0].adUnits[2].gam.adSlot).to.equal('12345/entertainment'); + // top level adUnit status is success + expect(message.auctions[0].adUnits[2].status).to.equal('success'); + // second adUnits bid is success + expect(message.auctions[0].adUnits[2].bids[0].status).to.equal('success'); + expect(message.auctions[0].adUnits[2].bids[0].bidResponse.floorValue).to.equal(1.5); + expect(message.auctions[0].adUnits[2].bids[0].bidResponse.floorRuleValue).to.equal(1); + expect(message.auctions[0].adUnits[2].bids[0].bidResponse.bidPriceUSD).to.equal(2.01); }); it('should still send floor info if provider is not rubicon', function () { - let message = performFloorAuction('randomProvider') + let message = performFloorAuction('randomProvider'); // verify our floor stuff is passed // top level floor info expect(message.auctions[0].floors).to.deep.equal({ location: 'setConfig', modelName: 'someModelName', + modelWeight: 10, + modelTimestamp: 1606772895, skipped: false, enforcement: true, dealsEnforced: false, @@ -782,23 +1106,605 @@ describe('rubicon analytics adapter', function () { provider: 'randomProvider' }); // first adUnit's adSlot - expect(message.auctions[0].adUnits[0].adSlot).to.equal('12345/sports'); + expect(message.auctions[0].adUnits[0].gam.adSlot).to.equal('12345/sports'); // since no other bids, we set adUnit status to no-bid expect(message.auctions[0].adUnits[0].status).to.equal('no-bid'); // first adUnits bid is rejected - expect(message.auctions[0].adUnits[0].bids[0].status).to.equal('rejected'); + expect(message.auctions[0].adUnits[0].bids[0].status).to.equal('rejected-ipf'); expect(message.auctions[0].adUnits[0].bids[0].bidResponse.floorValue).to.equal(4); // if bid rejected should take cpmAfterAdjustments val expect(message.auctions[0].adUnits[0].bids[0].bidResponse.bidPriceUSD).to.equal(2.1); // second adUnit's adSlot - expect(message.auctions[0].adUnits[1].adSlot).to.equal('12345/news'); + expect(message.auctions[0].adUnits[1].gam.adSlot).to.equal('12345/news'); // top level adUnit status is success expect(message.auctions[0].adUnits[1].status).to.equal('success'); // second adUnits bid is success expect(message.auctions[0].adUnits[1].bids[0].status).to.equal('success'); expect(message.auctions[0].adUnits[1].bids[0].bidResponse.floorValue).to.equal(1); expect(message.auctions[0].adUnits[1].bids[0].bidResponse.bidPriceUSD).to.equal(1.52); + + // second adUnit's adSlot + expect(message.auctions[0].adUnits[2].gam.adSlot).to.equal('12345/entertainment'); + // top level adUnit status is success + expect(message.auctions[0].adUnits[2].status).to.equal('success'); + // second adUnits bid is success + expect(message.auctions[0].adUnits[2].bids[0].status).to.equal('success'); + expect(message.auctions[0].adUnits[2].bids[0].bidResponse.floorValue).to.equal(1.5); + expect(message.auctions[0].adUnits[2].bids[0].bidResponse.floorRuleValue).to.equal(1); + expect(message.auctions[0].adUnits[2].bids[0].bidResponse.bidPriceUSD).to.equal(2.01); + }); + + describe('with session handling', function () { + const expectedPvid = STUBBED_UUID.slice(0, 8); + beforeEach(function () { + config.setConfig({rubicon: {updatePageView: true}}); + }); + + it('should not log any session data if local storage is not enabled', function () { + localStorageIsEnabledStub.returns(false); + + let expectedMessage = utils.deepClone(ANALYTICS_MESSAGE); + delete expectedMessage.session; + delete expectedMessage.fpkvs; + + performStandardAuction(); + + expect(server.requests.length).to.equal(1); + let request = server.requests[0]; + + expect(request.url).to.equal('//localhost:9999/event'); + + let message = JSON.parse(request.requestBody); + validate(message); + + expect(message).to.deep.equal(expectedMessage); + }); + + it('should should pass along custom rubicon kv and pvid when defined', function () { + config.setConfig({rubicon: { + fpkvs: { + source: 'fb', + link: 'email' + } + }}); + 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: 'source', value: 'fb'}, + {key: 'link', value: 'email'} + ] + 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'}); + + config.setConfig({rubicon: { + fpkvs: { + source: 'fb', + link: 'email' + } + }}); + 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: 'source', value: 'other'}, + {key: 'link', value: 'email'} + ] + + message.fpkvs.sort((left, right) => left.key < right.key); + expectedMessage.fpkvs.sort((left, right) => left.key < right.key); + + expect(message).to.deep.equal(expectedMessage); + }); + + it('should pick up existing localStorage and use its values', function () { + // set some localStorage + let inputlocalStorage = { + id: '987654', + start: 1519766113781, // 15 mins before "now" + expires: 1519787713781, // six hours later + lastSeen: 1519766113781, + fpkvs: { source: 'tw' } + }; + getDataFromLocalStorageStub.withArgs('rpaSession').returns(btoa(JSON.stringify(inputlocalStorage))); + + config.setConfig({rubicon: { + fpkvs: { + link: 'email' // should merge this with what is in the localStorage! + } + }}); + 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 = { + id: '987654', + start: 1519766113781, + expires: 1519787713781, + pvid: expectedPvid + } + expectedMessage.fpkvs = [ + {key: 'source', value: 'tw'}, + {key: 'link', value: 'email'} + ] + expect(message).to.deep.equal(expectedMessage); + + let calledWith; + try { + calledWith = JSON.parse(atob(setDataInLocalStorageStub.getCall(0).args[1])); + } catch (e) { + calledWith = {}; + } + + expect(calledWith).to.deep.equal({ + id: '987654', // should have stayed same + start: 1519766113781, // should have stayed same + expires: 1519787713781, // should have stayed same + lastSeen: 1519767013781, // lastSeen updated to our "now" + fpkvs: { source: 'tw', link: 'email' }, // link merged in + pvid: expectedPvid // new pvid stored + }); + }); + + it('should overwrite matching localstorge value and use its remaining values', function () { + sandbox.stub(utils, 'getWindowLocation').returns({'search': '?utm_source=fb&utm_click=dog'}); + + // set some localStorage + let inputlocalStorage = { + id: '987654', + start: 1519766113781, // 15 mins before "now" + expires: 1519787713781, // six hours later + lastSeen: 1519766113781, + fpkvs: { source: 'tw', link: 'email' } + }; + getDataFromLocalStorageStub.withArgs('rpaSession').returns(btoa(JSON.stringify(inputlocalStorage))); + + config.setConfig({rubicon: { + fpkvs: { + link: 'email' // should merge this with what is in the localStorage! + } + }}); + 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 = { + id: '987654', + start: 1519766113781, + expires: 1519787713781, + pvid: expectedPvid + } + expectedMessage.fpkvs = [ + {key: 'source', value: 'fb'}, + {key: 'link', value: 'email'}, + {key: 'click', value: 'dog'} + ] + + message.fpkvs.sort((left, right) => left.key < right.key); + expectedMessage.fpkvs.sort((left, right) => left.key < right.key); + + expect(message).to.deep.equal(expectedMessage); + + let calledWith; + try { + calledWith = JSON.parse(atob(setDataInLocalStorageStub.getCall(0).args[1])); + } catch (e) { + calledWith = {}; + } + + expect(calledWith).to.deep.equal({ + id: '987654', // should have stayed same + start: 1519766113781, // should have stayed same + expires: 1519787713781, // should have stayed same + lastSeen: 1519767013781, // lastSeen updated to our "now" + fpkvs: { source: 'fb', link: 'email', click: 'dog' }, // link merged in + pvid: expectedPvid // new pvid stored + }); + }); + + it('should throw out session if lastSeen > 30 mins ago and create new one', function () { + // set some localStorage + let inputlocalStorage = { + id: '987654', + start: 1519764313781, // 45 mins before "now" + expires: 1519785913781, // six hours later + lastSeen: 1519764313781, // 45 mins before "now" + fpkvs: { source: 'tw' } + }; + getDataFromLocalStorageStub.withArgs('rpaSession').returns(btoa(JSON.stringify(inputlocalStorage))); + + config.setConfig({rubicon: { + fpkvs: { + link: 'email' // should merge this with what is in the localStorage! + } + }}); + + 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); + // session should match what is already in ANALYTICS_MESSAGE, just need to add pvid + expectedMessage.session.pvid = expectedPvid; + + // the saved fpkvs should have been thrown out since session expired + expectedMessage.fpkvs = [ + {key: 'link', value: 'email'} + ] + expect(message).to.deep.equal(expectedMessage); + + let calledWith; + try { + calledWith = JSON.parse(atob(setDataInLocalStorageStub.getCall(0).args[1])); + } catch (e) { + calledWith = {}; + } + + expect(calledWith).to.deep.equal({ + id: STUBBED_UUID, // should have stayed same + start: 1519767013781, // should have stayed same + expires: 1519788613781, // should have stayed same + lastSeen: 1519767013781, // lastSeen updated to our "now" + fpkvs: { link: 'email' }, // link merged in + pvid: expectedPvid // new pvid stored + }); + }); + + it('should throw out session if past expires time and create new one', function () { + // set some localStorage + let inputlocalStorage = { + id: '987654', + start: 1519745353781, // 6 hours before "expires" + expires: 1519766953781, // little more than six hours ago + lastSeen: 1519767008781, // 5 seconds ago + fpkvs: { source: 'tw' } + }; + getDataFromLocalStorageStub.withArgs('rpaSession').returns(btoa(JSON.stringify(inputlocalStorage))); + + config.setConfig({rubicon: { + fpkvs: { + link: 'email' // should merge this with what is in the localStorage! + } + }}); + + 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); + // session should match what is already in ANALYTICS_MESSAGE, just need to add pvid + expectedMessage.session.pvid = expectedPvid; + + // the saved fpkvs should have been thrown out since session expired + expectedMessage.fpkvs = [ + {key: 'link', value: 'email'} + ] + expect(message).to.deep.equal(expectedMessage); + + let calledWith; + try { + calledWith = JSON.parse(atob(setDataInLocalStorageStub.getCall(0).args[1])); + } catch (e) { + calledWith = {}; + } + + expect(calledWith).to.deep.equal({ + id: STUBBED_UUID, // should have stayed same + start: 1519767013781, // should have stayed same + expires: 1519788613781, // should have stayed same + lastSeen: 1519767013781, // lastSeen updated to our "now" + fpkvs: { link: 'email' }, // link merged in + pvid: expectedPvid // new pvid stored + }); + }); + }); + describe('with googletag enabled', function () { + let gptSlot0, gptSlot1; + let gptSlotRenderEnded0, gptSlotRenderEnded1; + beforeEach(function () { + mockGpt.enable(); + gptSlot0 = mockGpt.makeSlot({code: '/19968336/header-bid-tag-0'}); + gptSlotRenderEnded0 = { + eventName: 'slotRenderEnded', + params: { + slot: gptSlot0, + isEmpty: false, + advertiserId: 1111, + sourceAgnosticCreativeId: 2222, + sourceAgnosticLineItemId: 3333 + } + }; + + gptSlot1 = mockGpt.makeSlot({code: '/19968336/header-bid-tag1'}); + gptSlotRenderEnded1 = { + eventName: 'slotRenderEnded', + params: { + slot: gptSlot1, + isEmpty: false, + advertiserId: 4444, + sourceAgnosticCreativeId: 5555, + sourceAgnosticLineItemId: 6666 + } + }; + }); + + afterEach(function () { + mockGpt.disable(); + }); + + it('should add necessary gam information if gpt is enabled and slotRender event emmited', 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 handle empty gam renders', function () { + performStandardAuction([gptSlotRenderEnded0, { + eventName: 'slotRenderEnded', + params: { + slot: gptSlot1, + isEmpty: true + } + }]); + 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 = { + isSlotEmpty: true, + adSlot: '/19968336/header-bid-tag1' + }; + expect(message).to.deep.equal(expectedMessage); + }); + + it('should still add gam ids if falsy', function () { + performStandardAuction([gptSlotRenderEnded0, { + eventName: 'slotRenderEnded', + params: { + slot: gptSlot1, + isEmpty: false, + advertiserId: 0, + sourceAgnosticCreativeId: 0, + sourceAgnosticLineItemId: 0 + } + }]); + 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: 0, + creativeId: 0, + lineItemId: 0, + adSlot: '/19968336/header-bid-tag1' + }; + expect(message).to.deep.equal(expectedMessage); + }); + + it('should pick backup Ids if no sourceAgnostic available first', function () { + performStandardAuction([gptSlotRenderEnded0, { + eventName: 'slotRenderEnded', + params: { + slot: gptSlot1, + isEmpty: false, + advertiserId: 0, + lineItemId: 1234, + creativeId: 5678 + } + }]); + 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: 0, + creativeId: 5678, + lineItemId: 1234, + adSlot: '/19968336/header-bid-tag1' + }; + expect(message).to.deep.equal(expectedMessage); + }); + + 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, + 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 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 () { @@ -835,6 +1741,73 @@ describe('rubicon analytics adapter', function () { expect(message.bidsWon[0].bidId).to.equal('abc-123-do-re-me'); }); + it('should correctly overwrite bidId if pbsBidId is on the bidResponse', function () { + // Only want one bid request in our mock auction + let bidRequested = utils.deepClone(MOCK.BID_REQUESTED); + bidRequested.bids.shift(); + let auctionInit = utils.deepClone(MOCK.AUCTION_INIT); + auctionInit.adUnits.shift(); + + // clone the mock bidResponse and duplicate + let seatBidResponse = utils.deepClone(BID4); + + const setTargeting = { + [seatBidResponse.adUnitCode]: seatBidResponse.adserverTargeting + }; + + const bidWon = Object.assign({}, seatBidResponse, { + 'status': 'rendered' + }); + + // spoof the auction with just our duplicates + events.emit(AUCTION_INIT, auctionInit); + events.emit(BID_REQUESTED, bidRequested); + events.emit(BID_RESPONSE, seatBidResponse); + events.emit(AUCTION_END, MOCK.AUCTION_END); + events.emit(SET_TARGETING, setTargeting); + events.emit(BID_WON, bidWon); + + let message = JSON.parse(server.requests[0].requestBody); + + validate(message); + expect(message.auctions[0].adUnits[0].bids[0].bidId).to.equal('zzzz-yyyy-xxxx-wwww'); + expect(message.bidsWon[0].bidId).to.equal('zzzz-yyyy-xxxx-wwww'); + }); + + it('should correctly generate new bidId if it is 0', function () { + // Only want one bid request in our mock auction + let bidRequested = utils.deepClone(MOCK.BID_REQUESTED); + bidRequested.bids.shift(); + let auctionInit = utils.deepClone(MOCK.AUCTION_INIT); + auctionInit.adUnits.shift(); + + // clone the mock bidResponse and duplicate + let seatBidResponse = utils.deepClone(BID4); + seatBidResponse.pbsBidId = '0'; + + const setTargeting = { + [seatBidResponse.adUnitCode]: seatBidResponse.adserverTargeting + }; + + const bidWon = Object.assign({}, seatBidResponse, { + 'status': 'rendered' + }); + + // spoof the auction with just our duplicates + events.emit(AUCTION_INIT, auctionInit); + events.emit(BID_REQUESTED, bidRequested); + events.emit(BID_RESPONSE, seatBidResponse); + events.emit(AUCTION_END, MOCK.AUCTION_END); + events.emit(SET_TARGETING, setTargeting); + events.emit(BID_WON, bidWon); + + let message = JSON.parse(server.requests[0].requestBody); + + validate(message); + expect(message.auctions[0].adUnits[0].bids[0].bidId).to.equal(STUBBED_UUID); + expect(message.bidsWon[0].bidId).to.equal(STUBBED_UUID); + }); + it('should pick the highest cpm bid if more than one bid per bidRequestId', function () { // Only want one bid request in our mock auction let bidRequested = utils.deepClone(MOCK.BID_REQUESTED); @@ -932,6 +1905,65 @@ 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].ortb2Imp = { + ext: { + data: { + aupname: '1234/mycoolsite/*&gpt_leaderboard&deviceType=mobile' + } + } + }; + bidRequest.bids[1].ortb2Imp = { + ext: { + data: { + 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 pass bidderDetail for multibid auctions', function () { + let bidResponse = utils.deepClone(MOCK.BID_RESPONSE[1]); + bidResponse.targetingBidder = 'rubi2'; + bidResponse.originalRequestId = bidResponse.requestId; + bidResponse.requestId = '1a2b3c4d5e6f7g8h9'; + + 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, bidResponse); + events.emit(BIDDER_DONE, MOCK.BIDDER_DONE); + events.emit(AUCTION_END, MOCK.AUCTION_END); + events.emit(SET_TARGETING, MOCK.SET_TARGETING); + events.emit(BID_WON, MOCK.BID_WON[0]); + + clock.tick(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[1].bids[1].bidder).to.equal('rubicon'); + expect(message.auctions[0].adUnits[1].bids[1].bidderDetail).to.equal('rubi2'); + }); + it('should successfully convert bid price to USD in parseBidResponse', function () { // Set the rates setConfig({ @@ -963,12 +1995,9 @@ describe('rubicon analytics adapter', function () { describe('config with integration type', () => { it('should use the integration type provided in the config instead of the default', () => { - sandbox.stub(config, 'getConfig').callsFake(function (key) { - const config = { - 'rubicon.int_type': 'testType' - }; - return config[key]; - }); + config.setConfig({rubicon: { + int_type: 'testType' + }}) rubiconAnalyticsAdapter.enableAnalytics({ options: { @@ -988,6 +2017,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/rubiconAnalyticsSchema.json b/test/spec/modules/rubiconAnalyticsSchema.json index 16cca629d8c..13d39e46cd0 100644 --- a/test/spec/modules/rubiconAnalyticsSchema.json +++ b/test/spec/modules/rubiconAnalyticsSchema.json @@ -34,6 +34,53 @@ "type": "string", "description": "Version of Prebid.js responsible for the auctions contained within." }, + "fpkvs": { + "type": "array", + "description": "List of any dynamic key value pairs set by publisher.", + "minItems": 1, + "items": { + "type": "object", + "required": [ + "key", + "value" + ], + "properties": { + "key": { + "type": "string" + }, + "value": { + "type": "string" + } + } + } + }, + "session": { + "type": "object", + "description": "The session information for a given event", + "required": [ + "id", + "start", + "expires" + ], + "properties": { + "id": { + "type": "string", + "description": "UUID of session." + }, + "start": { + "type": "integer", + "description": "Unix timestamp of time of creation for this session in milliseconds." + }, + "expires": { + "type": "integer", + "description": "Unix timestamp of the maximum allowed time in milliseconds of the session." + }, + "pvid": { + "type": "string", + "description": "id to track page view." + } + } + }, "auctions": { "type": "array", "minItems": 1, @@ -125,6 +172,9 @@ "zoneId": { "type": "number", "description": "The Rubicon zoneId associated with this adUnit - Removed if null" + }, + "gam": { + "$ref": "#/definitions/gam" } } } @@ -197,6 +247,31 @@ } }, "definitions": { + "gam": { + "type": "object", + "description": "The gam information for a given ad unit", + "required": [ + "adSlot" + ], + "properties": { + "adSlot": { + "type": "string" + }, + "advertiserId": { + "type": "integer" + }, + "creativeId": { + "type": "integer" + }, + "LineItemId": { + "type": "integer" + }, + "isSlotEmpty": { + "type": "boolean", + "enum": [true] + } + } + }, "adserverTargeting": { "type": "object", "description": "The adserverTargeting key/value pairs", @@ -293,7 +368,8 @@ "success", "no-bid", "error", - "rejected" + "rejected-gdpr", + "rejected-ipf" ] }, "error": { @@ -333,7 +409,6 @@ "bidResponse": { "type": "object", "required": [ - "dimensions", "mediaType", "bidPriceUSD" ], diff --git a/test/spec/modules/rubiconBidAdapter_spec.js b/test/spec/modules/rubiconBidAdapter_spec.js index c21a9b879f1..1964d0d6ecc 100644 --- a/test/spec/modules/rubiconBidAdapter_spec.js +++ b/test/spec/modules/rubiconBidAdapter_spec.js @@ -1,10 +1,17 @@ import {expect} from 'chai'; -import adapterManager from 'src/adapterManager.js'; -import {spec, getPriceGranularity, masSizeOrdering, resetUserSync, hasVideoMediaType, FASTLANE_ENDPOINT} from 'modules/rubiconBidAdapter.js'; +import { + spec, + getPriceGranularity, + masSizeOrdering, + resetUserSync, + hasVideoMediaType, + resetRubiConf +} from 'modules/rubiconBidAdapter.js'; import {parse as parseQuery} from 'querystring'; import {config} from 'src/config.js'; import * as utils from 'src/utils.js'; import find from 'core-js-pure/features/array/find.js'; +import {createEidsArray} from 'modules/userId/eids.js'; const INTEGRATION = `pbjs_lite_v$prebid.version$`; // $prebid.version$ will be substituted in by gulp in built prebid const PBS_INTEGRATION = 'pbjs'; @@ -13,7 +20,8 @@ describe('the rubicon adapter', function () { let sandbox, bidderRequest, sizeMap, - getFloorResponse; + getFloorResponse, + logErrorSpy; /** * @typedef {Object} sizeMapConverted @@ -136,7 +144,7 @@ describe('the rubicon adapter', function () { 'targeting': [ { 'key': getProp('targeting_key', `rpfl_${i}`), - 'values': [ '43_tier_all_test' ] + 'values': ['43_tier_all_test'] } ] }; @@ -219,12 +227,28 @@ describe('the rubicon adapter', function () { 'size_id': 201, }; bid.userId = { - lipb: { - lipbid: '0000-1111-2222-3333', - segments: ['segA', 'segB'] - }, - idl_env: '1111-2222-3333-4444' + lipb: {lipbid: '0000-1111-2222-3333', segments: ['segA', 'segB']}, + idl_env: '1111-2222-3333-4444', + sharedid: {id: '1111', third: '2222'}, + tdid: '3000', + pubcid: '4000', + pubProvidedId: [{ + source: 'example.com', + uids: [{ + id: '333333', + ext: { + stype: 'ppuid' + } + }] + }, { + source: 'id-partner.com', + uids: [{ + id: '4444444' + }] + }], + criteoId: '1111', }; + bid.userIdAsEids = createEidsArray(bid.userId); bid.storedAuctionResponse = 11111; } @@ -272,6 +296,7 @@ describe('the rubicon adapter', function () { beforeEach(function () { sandbox = sinon.sandbox.create(); + logErrorSpy = sinon.spy(utils, 'logError'); getFloorResponse = {}; bidderRequest = { bidderCode: 'rubicon', @@ -343,6 +368,10 @@ describe('the rubicon adapter', function () { afterEach(function () { sandbox.restore(); + utils.logError.restore(); + config.resetConfig(); + resetRubiConf(); + delete $$PREBID_GLOBAL$$.installedModules; }); describe('MAS mapping / ordering', function () { @@ -368,7 +397,10 @@ describe('the rubicon adapter', function () { describe('to fastlane', function () { it('should make a well-formed request object', function () { sandbox.stub(Math, 'random').callsFake(() => 0.1); - let [request] = spec.buildRequests(bidderRequest.bids, bidderRequest); + let duplicate = Object.assign(bidderRequest); + duplicate.bids[0].params.floor = 0.01; + + let [request] = spec.buildRequests(duplicate.bids, duplicate); let data = parseQuery(request.data); expect(request.url).to.equal('https://fastlane.rubiconproject.com/a/api/fastlane.json'); @@ -412,7 +444,18 @@ describe('the rubicon adapter', function () { it('should correctly send hard floors when getFloor function is present and returns valid floor', function () { // default getFloor response is empty object so should not break and not send hard_floor bidderRequest.bids[0].getFloor = () => getFloorResponse; + sinon.spy(bidderRequest.bids[0], 'getFloor'); let [request] = spec.buildRequests(bidderRequest.bids, bidderRequest); + + // make sure banner bid called with right stuff + expect( + bidderRequest.bids[0].getFloor.calledWith({ + currency: 'USD', + mediaType: 'banner', + size: '*' + }) + ).to.be.true; + let data = parseQuery(request.data); expect(data.rp_hard_floor).to.be.undefined; @@ -446,35 +489,41 @@ describe('the rubicon adapter', function () { data = parseQuery(request.data); expect(data.rp_hard_floor).to.equal('1.23'); }); - it('should not send p_pos to AE if not params.position specified', function() { - var noposRequest = utils.deepClone(bidderRequest); - delete noposRequest.bids[0].params.position; - let [request] = spec.buildRequests(noposRequest.bids, noposRequest); - let data = parseQuery(request.data); + it('should send rp_maxbids to AE if rubicon multibid config exists', function () { + var multibidRequest = utils.deepClone(bidderRequest); + multibidRequest.bidLimit = 5; + + let [request] = spec.buildRequests(multibidRequest.bids, multibidRequest); + let data = parseQuery(request.data); + + expect(data['rp_maxbids']).to.equal('5'); + }); + + it('should not send p_pos to AE if not params.position specified', function () { + var noposRequest = utils.deepClone(bidderRequest); + delete noposRequest.bids[0].params.position; - expect(data['site_id']).to.equal('70608'); - expect(data['p_pos']).to.equal(undefined); + let [request] = spec.buildRequests(noposRequest.bids, noposRequest); + let data = parseQuery(request.data); + + expect(data['site_id']).to.equal('70608'); + expect(data['p_pos']).to.equal(undefined); }); - it('should not send p_pos to AE if not params.position is invalid', function() { - var badposRequest = utils.deepClone(bidderRequest); - badposRequest.bids[0].params.position = 'bad'; + it('should not send p_pos to AE if not params.position is invalid', function () { + var badposRequest = utils.deepClone(bidderRequest); + badposRequest.bids[0].params.position = 'bad'; - let [request] = spec.buildRequests(badposRequest.bids, badposRequest); - let data = parseQuery(request.data); + let [request] = spec.buildRequests(badposRequest.bids, badposRequest); + let data = parseQuery(request.data); - expect(data['site_id']).to.equal('70608'); - expect(data['p_pos']).to.equal(undefined); + expect(data['site_id']).to.equal('70608'); + expect(data['p_pos']).to.equal(undefined); }); it('should correctly send p_pos in sra fashion', function() { - sandbox.stub(config, 'getConfig').callsFake((key) => { - const config = { - 'rubicon.singleRequest': true - }; - return config[key]; - }); + config.setConfig({rubicon: {singleRequest: true}}); // first one is atf var sraPosRequest = utils.deepClone(bidderRequest); @@ -504,11 +553,11 @@ describe('the rubicon adapter', function () { expect(data['p_pos']).to.equal('atf;;btf;;'); }); - it('should not send x_source.pchain to AE if params.pchain is not specified', function() { - var noPchainRequest = utils.deepClone(bidderRequest); - delete noPchainRequest.bids[0].params.pchain; + it('should not send x_source.pchain to AE if params.pchain is not specified', function () { + var noPchainRequest = utils.deepClone(bidderRequest); + delete noPchainRequest.bids[0].params.pchain; - let [request] = spec.buildRequests(noPchainRequest.bids, noPchainRequest); + let [request] = spec.buildRequests(noPchainRequest.bids, noPchainRequest); expect(request.data).to.contain('&site_id=70608&'); expect(request.data).to.not.contain('x_source.pchain'); }); @@ -517,7 +566,7 @@ describe('the rubicon adapter', function () { sandbox.stub(Math, 'random').callsFake(() => 0.1); let [request] = spec.buildRequests(bidderRequest.bids, bidderRequest); - const referenceOrdering = ['account_id', 'site_id', 'zone_id', 'size_id', 'alt_size_ids', 'p_pos', 'rf', 'p_geo.latitude', 'p_geo.longitude', 'kw', 'tg_v.ucat', 'tg_v.lastsearch', 'tg_v.likes', 'tg_i.rating', 'tg_i.prodtype', 'tk_flint', 'x_source.tid', 'x_source.pchain', 'p_screen_res', 'rp_floor', 'rp_secure', 'tk_user_key', 'tg_fl.eid', 'slots', 'rand']; + const referenceOrdering = ['account_id', 'site_id', 'zone_id', 'size_id', 'alt_size_ids', 'p_pos', 'rf', 'p_geo.latitude', 'p_geo.longitude', 'kw', 'tg_v.ucat', 'tg_v.lastsearch', 'tg_v.likes', 'tg_i.rating', 'tg_i.prodtype', 'tk_flint', 'x_source.tid', 'x_source.pchain', 'p_screen_res', 'rp_secure', 'tk_user_key', 'tg_fl.eid', 'rp_maxbids', 'slots', 'rand']; request.data.split('&').forEach((item, i) => { expect(item.split('=')[0]).to.equal(referenceOrdering[i]); @@ -532,7 +581,6 @@ describe('the rubicon adapter', function () { 'size_id': '15', 'alt_size_ids': '43', 'p_pos': 'atf', - 'rp_floor': '0.01', 'rp_secure': /[01]/, 'rand': '0.1', 'tk_flint': INTEGRATION, @@ -609,7 +657,7 @@ describe('the rubicon adapter', function () { expect(parseQuery(request.data).rf).to.equal('localhost'); delete bidderRequest.bids[0].params.referrer; - let refererInfo = { referer: 'https://www.prebid.org' }; + let refererInfo = {referer: 'https://www.prebid.org'}; bidderRequest = Object.assign({refererInfo}, bidderRequest); [request] = spec.buildRequests(bidderRequest.bids, bidderRequest); expect(parseQuery(request.data).rf).to.equal('https://www.prebid.org'); @@ -668,233 +716,6 @@ describe('the rubicon adapter', function () { expect(data['rp_floor']).to.equal('2'); }); - it('should send digitrust params', function () { - window.DigiTrust = { - getUser: function () { - } - }; - sandbox.stub(window.DigiTrust, 'getUser').callsFake(() => - ({ - success: true, - identity: { - privacy: {optout: false}, - id: 'testId', - keyv: 'testKeyV' - } - }) - ); - - let [request] = spec.buildRequests(bidderRequest.bids, bidderRequest); - let data = parseQuery(request.data); - - let expectedQuery = { - 'dt.id': 'testId', - 'dt.keyv': 'testKeyV', - 'dt.pref': '0' - }; - - // test that all values above are both present and correct - Object.keys(expectedQuery).forEach(key => { - let value = expectedQuery[key]; - expect(data[key]).to.equal(value); - }); - - delete window.DigiTrust; - }); - - it('should not send digitrust params when DigiTrust not loaded', function () { - let [request] = spec.buildRequests(bidderRequest.bids, bidderRequest); - let data = parseQuery(request.data); - - let undefinedKeys = ['dt.id', 'dt.keyv']; - - // Test that none of the DigiTrust keys are part of the query - undefinedKeys.forEach(key => { - expect(typeof data[key]).to.equal('undefined'); - }); - }); - - it('should not send digitrust params due to optout', function () { - window.DigiTrust = { - getUser: function () { - } - }; - sandbox.stub(window.DigiTrust, 'getUser').callsFake(() => - ({ - success: true, - identity: { - privacy: {optout: true}, - id: 'testId', - keyv: 'testKeyV' - } - }) - ); - - let [request] = spec.buildRequests(bidderRequest.bids, bidderRequest); - let data = parseQuery(request.data); - - let undefinedKeys = ['dt.id', 'dt.keyv']; - - // Test that none of the DigiTrust keys are part of the query - undefinedKeys.forEach(key => { - expect(typeof data[key]).to.equal('undefined'); - }); - - delete window.DigiTrust; - }); - - it('should not send digitrust params due to failure', function () { - window.DigiTrust = { - getUser: function () { - } - }; - sandbox.stub(window.DigiTrust, 'getUser').callsFake(() => - ({ - success: false, - identity: { - privacy: {optout: false}, - id: 'testId', - keyv: 'testKeyV' - } - }) - ); - - let [request] = spec.buildRequests(bidderRequest.bids, bidderRequest); - let data = parseQuery(request.data); - - let undefinedKeys = ['dt.id', 'dt.keyv']; - - // Test that none of the DigiTrust keys are part of the query - undefinedKeys.forEach(key => { - expect(typeof data[key]).to.equal('undefined'); - }); - - delete window.DigiTrust; - }); - - describe('digiTrustId config', function () { - beforeEach(function () { - window.DigiTrust = { - getUser: sandbox.spy() - }; - }); - - afterEach(function () { - delete window.DigiTrust; - }); - - it('should send digiTrustId config params', function () { - sandbox.stub(config, 'getConfig').callsFake((key) => { - var config = { - digiTrustId: { - success: true, - identity: { - privacy: {optout: false}, - id: 'testId', - keyv: 'testKeyV' - } - } - }; - return config[key]; - }); - - let [request] = spec.buildRequests(bidderRequest.bids, bidderRequest); - let data = parseQuery(request.data); - - let expectedQuery = { - 'dt.id': 'testId', - 'dt.keyv': 'testKeyV' - }; - - // test that all values above are both present and correct - Object.keys(expectedQuery).forEach(key => { - let value = expectedQuery[key]; - expect(data[key]).to.equal(value); - }); - - // should not have called DigiTrust.getUser() - expect(window.DigiTrust.getUser.notCalled).to.equal(true); - }); - - it('should not send digiTrustId config params due to optout', function () { - sandbox.stub(config, 'getConfig').callsFake((key) => { - var config = { - digiTrustId: { - success: true, - identity: { - privacy: {optout: true}, - id: 'testId', - keyv: 'testKeyV' - } - } - } - return config[key]; - }); - - let [request] = spec.buildRequests(bidderRequest.bids, bidderRequest); - let data = parseQuery(request.data); - - let undefinedKeys = ['dt.id', 'dt.keyv']; - - // Test that none of the DigiTrust keys are part of the query - undefinedKeys.forEach(key => { - expect(typeof data[key]).to.equal('undefined'); - }); - - // should not have called DigiTrust.getUser() - expect(window.DigiTrust.getUser.notCalled).to.equal(true); - }); - - it('should not send digiTrustId config params due to failure', function () { - sandbox.stub(config, 'getConfig').callsFake((key) => { - var config = { - digiTrustId: { - success: false, - identity: { - privacy: {optout: false}, - id: 'testId', - keyv: 'testKeyV' - } - } - } - return config[key]; - }); - - let [request] = spec.buildRequests(bidderRequest.bids, bidderRequest); - let data = parseQuery(request.data); - - let undefinedKeys = ['dt.id', 'dt.keyv']; - - // Test that none of the DigiTrust keys are part of the query - undefinedKeys.forEach(key => { - expect(typeof data[key]).to.equal('undefined'); - }); - - // should not have called DigiTrust.getUser() - expect(window.DigiTrust.getUser.notCalled).to.equal(true); - }); - - it('should not send digiTrustId config params if they do not exist', function () { - sandbox.stub(config, 'getConfig').callsFake((key) => { - var config = {}; - return config[key]; - }); - - let [request] = spec.buildRequests(bidderRequest.bids, bidderRequest); - let data = parseQuery(request.data); - - let undefinedKeys = ['dt.id', 'dt.keyv']; - - // Test that none of the DigiTrust keys are part of the query - undefinedKeys.forEach(key => { - expect(typeof data[key]).to.equal('undefined'); - }); - - // should have called DigiTrust.getUser() once - expect(window.DigiTrust.getUser.calledOnce).to.equal(true); - }); - }); - describe('GDPR consent config', function () { it('should send "gdpr" and "gdpr_consent", when gdprConsent defines consentString and gdprApplies', function () { createGdprBidderRequest(true); @@ -1017,22 +838,55 @@ describe('the rubicon adapter', function () { }); }); - it('should use first party data from getConfig over the bid params, if present', () => { - const context = { - keywords: ['e', 'f'], - rating: '4-star' + it('should merge first party data from getConfig with the bid params, if present', () => { + const site = { + keywords: 'e,f', + rating: '4-star', + ext: { + data: { + page: 'home' + } + }, + content: { + data: [{ + 'name': 'www.dataprovider1.com', + 'ext': { 'segtax': 1 }, + 'segment': [ + { 'id': '987' } + ] + }, { + 'name': 'www.dataprovider1.com', + 'ext': { 'segtax': 2 }, + 'segment': [ + { 'id': '432' } + ] + }] + } }; const user = { - keywords: ['d'], + data: [{ + 'name': 'www.dataprovider1.com', + 'ext': { 'segtax': 3 }, + 'segment': [ + { 'id': '687' }, + { 'id': '123' } + ] + }], gender: 'M', yob: '1984', - geo: { country: 'ca' } + geo: {country: 'ca'}, + keywords: 'd', + ext: { + data: { + age: 40 + } + } }; sandbox.stub(config, 'getConfig').callsFake(key => { const config = { - fpd: { - context, + ortb2: { + site, user } }; @@ -1040,14 +894,17 @@ 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.iab': '687,123', + 'tg_i.iab': '987,432', 'tg_v.yob': '1984', - 'tg_v.geo': '{"country":"ca"}', - 'tg_i.rating': '4-star', + 'tg_i.rating': '4-star,5-star', + 'tg_i.page': 'home', 'tg_i.prodtype': 'tech,mobile', }; @@ -1067,12 +924,7 @@ describe('the rubicon adapter', function () { it('should group all bid requests with the same site id', function () { sandbox.stub(Math, 'random').callsFake(() => 0.1); - sandbox.stub(config, 'getConfig').callsFake((key) => { - const config = { - 'rubicon.singleRequest': true - }; - return config[key]; - }); + config.setConfig({rubicon: {singleRequest: true}}); const expectedQuery = { 'account_id': '14062', @@ -1081,7 +933,6 @@ describe('the rubicon adapter', function () { 'size_id': '15', 'alt_size_ids': '43', 'p_pos': 'atf', - 'rp_floor': '0.01', 'rp_secure': /[01]/, 'rand': '0.1', 'tk_flint': INTEGRATION, @@ -1180,13 +1031,7 @@ describe('the rubicon adapter', function () { }); it('should not send more than 10 bids in a request (split into separate requests with <= 10 bids each)', function () { - sandbox.stub(config, 'getConfig').callsFake((key) => { - const config = { - 'rubicon.singleRequest': true - }; - return config[key]; - }); - + config.setConfig({rubicon: {singleRequest: true}}); let serverRequests; let data; @@ -1228,12 +1073,7 @@ describe('the rubicon adapter', function () { }); it('should not group bid requests if singleRequest does not equal true', function () { - sandbox.stub(config, 'getConfig').callsFake((key) => { - const config = { - 'rubicon.singleRequest': false - }; - return config[key]; - }); + config.setConfig({rubicon: {singleRequest: false}}); const bidCopy = utils.deepClone(bidderRequest.bids[0]); bidderRequest.bids.push(bidCopy); @@ -1251,12 +1091,7 @@ describe('the rubicon adapter', function () { }); it('should not group video bid requests', function () { - sandbox.stub(config, 'getConfig').callsFake((key) => { - const config = { - 'rubicon.singleRequest': true - }; - return config[key]; - }); + config.setConfig({rubicon: {singleRequest: true}}); const bidCopy = utils.deepClone(bidderRequest.bids[0]); bidderRequest.bids.push(bidCopy); @@ -1300,33 +1135,39 @@ describe('the rubicon adapter', function () { }); }); - describe('user id config', function() { - it('should send tpid_tdid when userId defines tdid', function () { + describe('user id config', function () { + it('should send tpid_tdid when userIdAsEids contains unifiedId', function () { const clonedBid = utils.deepClone(bidderRequest.bids[0]); clonedBid.userId = { tdid: 'abcd-efgh-ijkl-mnop-1234' }; + clonedBid.userIdAsEids = createEidsArray(clonedBid.userId); let [request] = spec.buildRequests([clonedBid], bidderRequest); let data = parseQuery(request.data); expect(data['tpid_tdid']).to.equal('abcd-efgh-ijkl-mnop-1234'); + expect(data['eid_adserver.org']).to.equal('abcd-efgh-ijkl-mnop-1234'); }); describe('LiveIntent support', function () { - it('should send tpid_liveintent.com when userId defines lipd', function () { + it('should send tpid_liveintent.com when userIdAsEids contains liveintentId', function () { const clonedBid = utils.deepClone(bidderRequest.bids[0]); clonedBid.userId = { lipb: { - lipbid: '0000-1111-2222-3333' + lipbid: '0000-1111-2222-3333', + segments: ['segA', 'segB'] } }; + clonedBid.userIdAsEids = createEidsArray(clonedBid.userId); let [request] = spec.buildRequests([clonedBid], bidderRequest); let data = parseQuery(request.data); expect(data['tpid_liveintent.com']).to.equal('0000-1111-2222-3333'); + expect(data['eid_liveintent.com']).to.equal('0000-1111-2222-3333'); + expect(data['tg_v.LIseg']).to.equal('segA,segB'); }); - it('should send tg_v.LIseg when userId defines lipd.segments', function () { + it('should send tg_v.LIseg when userIdAsEids contains liveintentId with ext.segments as array', function () { const clonedBid = utils.deepClone(bidderRequest.bids[0]); clonedBid.userId = { lipb: { @@ -1334,6 +1175,7 @@ describe('the rubicon adapter', function () { segments: ['segD', 'segE'] } }; + clonedBid.userIdAsEids = createEidsArray(clonedBid.userId); let [request] = spec.buildRequests([clonedBid], bidderRequest); const unescapedData = unescape(request.data); @@ -1343,28 +1185,155 @@ describe('the rubicon adapter', function () { }); describe('LiveRamp support', function () { - it('should send tpid_liveramp.com when userId defines idl_env', function () { + it('should send x_liverampidl when userIdAsEids contains liverampId', function () { const clonedBid = utils.deepClone(bidderRequest.bids[0]); clonedBid.userId = { idl_env: '1111-2222-3333-4444' }; + clonedBid.userIdAsEids = createEidsArray(clonedBid.userId); let [request] = spec.buildRequests([clonedBid], bidderRequest); let data = parseQuery(request.data); - expect(data['tpid_liveramp.com']).to.equal('1111-2222-3333-4444'); + expect(data['x_liverampidl']).to.equal('1111-2222-3333-4444'); + }); + }); + + describe('pubcid support', function () { + it('should send eid_pubcid.org when userIdAsEids contains pubcid', function () { + const clonedBid = utils.deepClone(bidderRequest.bids[0]); + clonedBid.userId = { + pubcid: '1111' + }; + clonedBid.userIdAsEids = createEidsArray(clonedBid.userId); + let [request] = spec.buildRequests([clonedBid], bidderRequest); + let data = parseQuery(request.data); + + expect(data['eid_pubcid.org']).to.equal('1111^1'); + }); + }); + + describe('Criteo support', function () { + it('should send eid_criteo.com when userIdAsEids contains criteo', function () { + const clonedBid = utils.deepClone(bidderRequest.bids[0]); + clonedBid.userId = { + criteoId: '1111' + }; + clonedBid.userIdAsEids = createEidsArray(clonedBid.userId); + let [request] = spec.buildRequests([clonedBid], bidderRequest); + let data = parseQuery(request.data); + + expect(data['eid_criteo.com']).to.equal('1111^1'); + }); + }); + + describe('SharedID support', function () { + it('should send sharedid when userIdAsEids contains sharedId', function () { + const clonedBid = utils.deepClone(bidderRequest.bids[0]); + clonedBid.userId = { + sharedid: { + id: '1111', + third: '2222' + } + }; + clonedBid.userIdAsEids = createEidsArray(clonedBid.userId); + let [request] = spec.buildRequests([clonedBid], bidderRequest); + let data = parseQuery(request.data); + + expect(data['eid_sharedid.org']).to.equal('1111^1^2222'); }); }); - }) + + describe('pubProvidedId support', function () { + it('should send pubProvidedId when userIdAsEids contains pubProvidedId ids', function () { + const clonedBid = utils.deepClone(bidderRequest.bids[0]); + clonedBid.userId = { + pubProvidedId: [{ + source: 'example.com', + uids: [{ + id: '11111', + ext: { + stype: 'ppuid' + } + }] + }, { + source: 'id-partner.com', + uids: [{ + id: '222222' + }] + }] + }; + clonedBid.userIdAsEids = createEidsArray(clonedBid.userId); + let [request] = spec.buildRequests([clonedBid], bidderRequest); + let data = parseQuery(request.data); + + expect(data['ppuid']).to.equal('11111'); + }); + }); + + describe('ID5 support', function () { + it('should send ID5 id when userIdAsEids contains ID5', function () { + const clonedBid = utils.deepClone(bidderRequest.bids[0]); + clonedBid.userId = { + id5id: { + uid: '11111', + ext: { + linkType: '22222' + } + } + }; + clonedBid.userIdAsEids = createEidsArray(clonedBid.userId); + let [request] = spec.buildRequests([clonedBid], bidderRequest); + let data = parseQuery(request.data); + + expect(data['eid_id5-sync.com']).to.equal('11111^1^22222'); + }); + }); + + describe('UserID catchall support', function () { + it('should send user id with generic format', function () { + const clonedBid = utils.deepClone(bidderRequest.bids[0]); + // Hardcoding userIdAsEids since createEidsArray returns empty array if source not found in eids.js + clonedBid.userIdAsEids = [{ + source: 'catchall', + uids: [{ + id: '11111', + atype: 2 + }] + }] + let [request] = spec.buildRequests([clonedBid], bidderRequest); + let data = parseQuery(request.data); + + expect(data['eid_catchall']).to.equal('11111^2'); + }); + }); + + describe('Config user.id support', function () { + it('should send ppuid when config defines user.id', function () { + config.setConfig({user: {id: '123'}}); + const clonedBid = utils.deepClone(bidderRequest.bids[0]); + clonedBid.userId = { + sharedid: { + id: '1111', + third: '2222' + } + }; + let [request] = spec.buildRequests([clonedBid], bidderRequest); + let data = parseQuery(request.data); + + expect(data['ppuid']).to.equal('123'); + }); + }); + }); describe('Prebid AdSlot', function () { beforeEach(function () { // enforce that the bid at 0 does not have a 'context' property - if (bidderRequest.bids[0].hasOwnProperty('fpd')) { - delete bidderRequest.bids[0].fpd; + if (bidderRequest.bids[0].hasOwnProperty('ortb2Imp')) { + delete bidderRequest.bids[0].ortb2Imp; } }); - it('should not send \"tg_i.pbadslot’\" if \"fpd.context\" object is not valid', function () { + it('should not send \"tg_i.pbadslot’\" if \"ortb2Imp.ext.data\" object is not valid', function () { const [request] = spec.buildRequests(bidderRequest.bids, bidderRequest); const data = parseQuery(request.data); @@ -1372,8 +1341,8 @@ describe('the rubicon adapter', function () { expect(data).to.not.have.property('tg_i.pbadslot’'); }); - it('should not send \"tg_i.pbadslot’\" if \"fpd.context.pbAdSlot\" is undefined', function () { - bidderRequest.bids[0].fpd = {}; + it('should not send \"tg_i.pbadslot’\" if \"ortb2Imp.ext.data.pbadslot\" is undefined', function () { + bidderRequest.bids[0].ortb2Imp = {}; const [request] = spec.buildRequests(bidderRequest.bids, bidderRequest); const data = parseQuery(request.data); @@ -1382,10 +1351,12 @@ describe('the rubicon adapter', function () { expect(data).to.not.have.property('tg_i.pbadslot’'); }); - it('should not send \"tg_i.pbadslot’\" if \"fpd.context.pbAdSlot\" value is an empty string', function () { - bidderRequest.bids[0].fpd = { - context: { - pbAdSlot: '' + it('should not send \"tg_i.pbadslot’\" if \"ortb2Imp.ext.data.pbadslot\" value is an empty string', function () { + bidderRequest.bids[0].ortb2Imp = { + ext: { + data: { + pbadslot: '' + } } }; @@ -1396,10 +1367,12 @@ describe('the rubicon adapter', function () { expect(data).to.not.have.property('tg_i.pbadslot'); }); - it('should send \"tg_i.pbadslot\" if \"fpd.context.pbAdSlot\" value is a valid string', function () { - bidderRequest.bids[0].fpd = { - context: { - pbAdSlot: 'abc' + it('should send \"tg_i.pbadslot\" if \"ortb2Imp.ext.data.pbadslot\" value is a valid string', function () { + bidderRequest.bids[0].ortb2Imp = { + ext: { + data: { + pbadslot: 'abc' + } } } @@ -1411,12 +1384,14 @@ describe('the rubicon adapter', function () { expect(data['tg_i.pbadslot']).to.equal('abc'); }); - it('should send \"tg_i.pbadslot\" if \"fpd.context.pbAdSlot\" value is a valid string, but all leading slash characters should be removed', function () { - bidderRequest.bids[0].fpd = { - context: { - pbAdSlot: '/a/b/c' + it('should send \"tg_i.pbadslot\" if \"ortb2Imp.ext.data.pbadslot\" value is a valid string, but all leading slash characters should be removed', function () { + bidderRequest.bids[0].ortb2Imp = { + ext: { + data: { + pbadslot: '/a/b/c' + } } - }; + } const [request] = spec.buildRequests(bidderRequest.bids, bidderRequest); const data = parseQuery(request.data); @@ -1430,12 +1405,12 @@ describe('the rubicon adapter', function () { describe('GAM ad unit', function () { beforeEach(function () { // enforce that the bid at 0 does not have a 'context' property - if (bidderRequest.bids[0].hasOwnProperty('fpd')) { - delete bidderRequest.bids[0].fpd; + if (bidderRequest.bids[0].hasOwnProperty('ortb2Imp')) { + delete bidderRequest.bids[0].ortb2Imp; } }); - it('should not send \"tg_i.dfp_ad_unit_code’\" if \"fpd.context\" object is not valid', function () { + it('should not send \"tg_i.dfp_ad_unit_code’\" if \"ortb2Imp.ext.data\" object is not valid', function () { const [request] = spec.buildRequests(bidderRequest.bids, bidderRequest); const data = parseQuery(request.data); @@ -1443,8 +1418,8 @@ describe('the rubicon adapter', function () { expect(data).to.not.have.property('tg_i.dfp_ad_unit_code’'); }); - it('should not send \"tg_i.dfp_ad_unit_code’\" if \"fpd.context.adServer.adSlot\" is undefined', function () { - bidderRequest.bids[0].fpd = {}; + it('should not send \"tg_i.dfp_ad_unit_code’\" if \"ortb2Imp.ext.data.adServer.adslot\" is undefined', function () { + bidderRequest.bids[0].ortb2Imp = {}; const [request] = spec.buildRequests(bidderRequest.bids, bidderRequest); const data = parseQuery(request.data); @@ -1453,11 +1428,13 @@ describe('the rubicon adapter', function () { expect(data).to.not.have.property('tg_i.dfp_ad_unit_code’'); }); - it('should not send \"tg_i.dfp_ad_unit_code’\" if \"fpd.context.adServer.adSlot\" value is an empty string', function () { - bidderRequest.bids[0].fpd = { - context: { - adServer: { - adSlot: '' + it('should not send \"tg_i.dfp_ad_unit_code’\" if \"ortb2Imp.ext.data.adServer.adslot\" value is an empty string', function () { + bidderRequest.bids[0].ortb2Imp = { + ext: { + data: { + adserver: { + adslot: '' + } } } }; @@ -1469,11 +1446,13 @@ describe('the rubicon adapter', function () { expect(data).to.not.have.property('tg_i.dfp_ad_unit_code'); }); - it('should send \"tg_i.dfp_ad_unit_code\" if \"fpd.context.adServer.adSlot\" value is a valid string', function () { - bidderRequest.bids[0].fpd = { - context: { - adServer: { - adSlot: 'abc' + it('should send \"tg_i.dfp_ad_unit_code\" if \"ortb2Imp.ext.data.adServer.adslot\" value is a valid string', function () { + bidderRequest.bids[0].ortb2Imp = { + ext: { + data: { + adserver: { + adslot: 'abc' + } } } } @@ -1486,11 +1465,13 @@ describe('the rubicon adapter', function () { expect(data['tg_i.dfp_ad_unit_code']).to.equal('abc'); }); - it('should send \"tg_i.dfp_ad_unit_code\" if \"fpd.context.adServer.adSlot\" value is a valid string, but all leading slash characters should be removed', function () { - bidderRequest.bids[0].fpd = { - context: { - adServer: { - adSlot: 'a/b/c' + it('should send \"tg_i.dfp_ad_unit_code\" if \"ortb2Imp.ext.data.adServer.adslot\" value is a valid string, but all leading slash characters should be removed', function () { + bidderRequest.bids[0].ortb2Imp = { + ext: { + data: { + adserver: { + adslot: 'a/b/c' + } } } }; @@ -1516,11 +1497,11 @@ describe('the rubicon adapter', function () { let [request] = spec.buildRequests(bidderRequest.bids, bidderRequest); let post = request.data; - expect(post).to.have.property('imp') + expect(post).to.have.property('imp'); // .with.length.of(1); let imp = post.imp[0]; expect(imp.id).to.equal(bidderRequest.bids[0].adUnitCode); - expect(imp.exp).to.equal(300); + expect(imp.exp).to.equal(undefined); // now undefined expect(imp.video.w).to.equal(640); expect(imp.video.h).to.equal(480); expect(imp.video.pos).to.equal(1); @@ -1539,20 +1520,47 @@ describe('the rubicon adapter', function () { expect(imp.ext.rubicon.video.skip).to.equal(1); expect(imp.ext.rubicon.video.skipafter).to.equal(15); expect(imp.ext.prebid.auctiontimestamp).to.equal(1472239426000); + // should contain version + expect(post.ext.prebid.channel).to.deep.equal({name: 'pbjs', version: 'v$prebid.version$'}); expect(post.user.ext.consent).to.equal('BOJ/P2HOJ/P2HABABMAAAAAZ+A=='); + // EIDs should exist + expect(post.user.ext).to.have.property('eids').that.is.an('array'); + // LiveIntent should exist expect(post.user.ext.eids[0].source).to.equal('liveintent.com'); expect(post.user.ext.eids[0].uids[0].id).to.equal('0000-1111-2222-3333'); - expect(post.user.ext.tpid).that.is.an('object'); - expect(post.user.ext.tpid.source).to.equal('liveintent.com'); - expect(post.user.ext.tpid.uid).to.equal('0000-1111-2222-3333'); + expect(post.user.ext.eids[0].uids[0].atype).to.equal(3); + expect(post.user.ext.eids[0]).to.have.property('ext').that.is.an('object'); + expect(post.user.ext.eids[0].ext).to.have.property('segments').that.is.an('array'); + expect(post.user.ext.eids[0].ext.segments[0]).to.equal('segA'); + expect(post.user.ext.eids[0].ext.segments[1]).to.equal('segB'); // LiveRamp should exist expect(post.user.ext.eids[1].source).to.equal('liveramp.com'); expect(post.user.ext.eids[1].uids[0].id).to.equal('1111-2222-3333-4444'); - expect(post.rp).that.is.an('object'); - expect(post.rp.target).that.is.an('object'); - expect(post.rp.target.LIseg).that.is.an('array'); - expect(post.rp.target.LIseg[0]).to.equal('segA'); - expect(post.rp.target.LIseg[1]).to.equal('segB'); + expect(post.user.ext.eids[1].uids[0].atype).to.equal(3); + // SharedId should exist + expect(post.user.ext.eids[2].source).to.equal('sharedid.org'); + expect(post.user.ext.eids[2].uids[0].id).to.equal('1111'); + expect(post.user.ext.eids[2].uids[0].atype).to.equal(1); + expect(post.user.ext.eids[2].uids[0].ext.third).to.equal('2222'); + // UnifiedId should exist + expect(post.user.ext.eids[3].source).to.equal('adserver.org'); + expect(post.user.ext.eids[3].uids[0].atype).to.equal(1); + expect(post.user.ext.eids[3].uids[0].id).to.equal('3000'); + // PubCommonId should exist + expect(post.user.ext.eids[4].source).to.equal('pubcid.org'); + expect(post.user.ext.eids[4].uids[0].atype).to.equal(1); + expect(post.user.ext.eids[4].uids[0].id).to.equal('4000'); + // example should exist + expect(post.user.ext.eids[5].source).to.equal('example.com'); + expect(post.user.ext.eids[5].uids[0].id).to.equal('333333'); + // id-partner.com + expect(post.user.ext.eids[6].source).to.equal('id-partner.com'); + expect(post.user.ext.eids[6].uids[0].id).to.equal('4444444'); + // CriteoId should exist + expect(post.user.ext.eids[7].source).to.equal('criteo.com'); + expect(post.user.ext.eids[7].uids[0].id).to.equal('1111'); + expect(post.user.ext.eids[7].uids[0].atype).to.equal(1); + expect(post.regs.ext.gdpr).to.equal(1); expect(post.regs.ext.us_privacy).to.equal('1NYN'); expect(post).to.have.property('ext').that.is.an('object'); @@ -1568,12 +1576,23 @@ describe('the rubicon adapter', function () { createVideoBidderRequest(); // default getFloor response is empty object so should not break and not send hard_floor bidderRequest.bids[0].getFloor = () => getFloorResponse; + sinon.spy(bidderRequest.bids[0], 'getFloor'); + sandbox.stub(Date, 'now').callsFake(() => bidderRequest.auctionStart + 100 ); let [request] = spec.buildRequests(bidderRequest.bids, bidderRequest); + // make sure banner bid called with right stuff + expect( + bidderRequest.bids[0].getFloor.calledWith({ + currency: 'USD', + mediaType: 'video', + size: [640, 480] + }) + ).to.be.true; + // not an object should work and not send expect(request.data.imp[0].bidfloor).to.be.undefined; @@ -1597,23 +1616,133 @@ describe('the rubicon adapter', function () { [request] = spec.buildRequests(bidderRequest.bids, bidderRequest); expect(request.data.imp[0].bidfloor).to.equal(1.23); }); - it('should add alias name to PBS Request', function() { + + it('should continue with auction and log error if getFloor throws one', function () { createVideoBidderRequest(); + // default getFloor response is empty object so should not break and not send hard_floor + bidderRequest.bids[0].getFloor = () => { + throw new Error('An exception!'); + }; + sandbox.stub(Date, 'now').callsFake(() => + bidderRequest.auctionStart + 100 + ); - bidderRequest.bidderCode = 'superRubicon'; - bidderRequest.bids[0].bidder = 'superRubicon'; let [request] = spec.buildRequests(bidderRequest.bids, bidderRequest); - // should have the aliases object sent to PBS - expect(request.data.ext.prebid).to.haveOwnProperty('aliases'); - expect(request.data.ext.prebid.aliases).to.deep.equal({superRubicon: 'rubicon'}); + // log error called + expect(logErrorSpy.calledOnce).to.equal(true); - // should have the imp ext bidder params be under the alias name not rubicon superRubicon + // should have an imp + expect(request.data.imp).to.exist.and.to.be.a('array'); + expect(request.data.imp).to.have.lengthOf(1); + + // should be NO bidFloor + expect(request.data.imp[0].bidfloor).to.be.undefined; + }); + + it('should add alias name to PBS Request', function () { + createVideoBidderRequest(); + + bidderRequest.bidderCode = 'superRubicon'; + bidderRequest.bids[0].bidder = 'superRubicon'; + let [request] = spec.buildRequests(bidderRequest.bids, bidderRequest); + + // should have the aliases object sent to PBS + expect(request.data.ext.prebid).to.haveOwnProperty('aliases'); + expect(request.data.ext.prebid.aliases).to.deep.equal({superRubicon: 'rubicon'}); + + // should have the imp ext bidder params be under the alias name not rubicon superRubicon expect(request.data.imp[0].ext).to.have.property('superRubicon').that.is.an('object'); expect(request.data.imp[0].ext).to.not.haveOwnProperty('rubicon'); }); - it('should send correct bidfloor to PBS', function() { + it('should add multibid configuration to PBS Request', function () { + createVideoBidderRequest(); + + const multibid = [{ + bidder: 'bidderA', + maxBids: 2 + }, { + bidder: 'bidderB', + maxBids: 2 + }]; + const expected = [{ + bidder: 'bidderA', + maxbids: 2 + }, { + bidder: 'bidderB', + maxbids: 2 + }]; + + config.setConfig({multibid: multibid}); + + let [request] = spec.buildRequests(bidderRequest.bids, bidderRequest); + + // should have the aliases object sent to PBS + expect(request.data.ext.prebid).to.haveOwnProperty('multibid'); + expect(request.data.ext.prebid.multibid).to.deep.equal(expected); + }); + + it('should pass client analytics to PBS endpoint if all modules included', function () { + createVideoBidderRequest(); + $$PREBID_GLOBAL$$.installedModules = []; + let [request] = spec.buildRequests(bidderRequest.bids, bidderRequest); + let payload = request.data; + + expect(payload.ext.prebid.analytics).to.not.be.undefined; + expect(payload.ext.prebid.analytics).to.deep.equal({'rubicon': {'client-analytics': true}}); + }); + + it('should pass client analytics to PBS endpoint if rubicon analytics adapter is included', function () { + createVideoBidderRequest(); + $$PREBID_GLOBAL$$.installedModules = ['rubiconBidAdapter', 'rubiconAnalyticsAdapter']; + let [request] = spec.buildRequests(bidderRequest.bids, bidderRequest); + let payload = request.data; + + expect(payload.ext.prebid.analytics).to.not.be.undefined; + expect(payload.ext.prebid.analytics).to.deep.equal({'rubicon': {'client-analytics': true}}); + }); + + it('should not pass client analytics to PBS endpoint if rubicon analytics adapter is not included', function () { + createVideoBidderRequest(); + $$PREBID_GLOBAL$$.installedModules = ['rubiconBidAdapter']; + let [request] = spec.buildRequests(bidderRequest.bids, bidderRequest); + let payload = request.data; + + expect(payload.ext.prebid.analytics).to.be.undefined; + }); + + it('should send video exp param correctly when set', function () { + createVideoBidderRequest(); + config.setConfig({s2sConfig: {defaultTtl: 600}}); + let [request] = spec.buildRequests(bidderRequest.bids, bidderRequest); + let post = request.data; + + // should exp set to the right value according to config + let imp = post.imp[0]; + expect(imp.exp).to.equal(600); + }); + + it('should not send video exp at all if not set in s2sConfig config', function () { + createVideoBidderRequest(); + let [request] = spec.buildRequests(bidderRequest.bids, bidderRequest); + let post = request.data; + + // should exp set to the right value according to config + let imp = post.imp[0]; + // bidderFactory stringifies request body before sending so removes undefined attributes: + expect(imp.exp).to.equal(undefined); + }); + + it('should send tmax as the bidderRequest timeout value', function () { + createVideoBidderRequest(); + bidderRequest.timeout = 3333; + let [request] = spec.buildRequests(bidderRequest.bids, bidderRequest); + let post = request.data; + expect(post.tmax).to.equal(3333); + }); + + it('should send correct bidfloor to PBS', function () { createVideoBidderRequest(); bidderRequest.bids[0].params.floor = 0.1; @@ -1740,16 +1869,6 @@ describe('the rubicon adapter', function () { delete bidderRequest.bids[0].mediaTypes.video.protocols; expect(spec.isBidRequestValid(bidderRequest.bids[0])).to.equal(false); - // change maxduration to an string, no good - createVideoBidderRequest(); - bidderRequest.bids[0].mediaTypes.video.maxduration = 'string'; - expect(spec.isBidRequestValid(bidderRequest.bids[0])).to.equal(false); - - // delete maxduration, no good - createVideoBidderRequest(); - delete bidderRequest.bids[0].mediaTypes.video.maxduration; - expect(spec.isBidRequestValid(bidderRequest.bids[0])).to.equal(false); - // change linearity to an string, no good createVideoBidderRequest(); bidderRequest.bids[0].mediaTypes.video.linearity = 'string'; @@ -1803,14 +1922,14 @@ describe('the rubicon adapter', function () { let requests = spec.buildRequests(bidderRequest.bids, bidderRequest); expect(requests.length).to.equal(1); - expect(requests[0].url).to.equal(FASTLANE_ENDPOINT); + expect(requests[0].url).to.equal('https://fastlane.rubiconproject.com/a/api/fastlane.json'); bidderRequest.mediaTypes.video.context = 'instream'; requests = spec.buildRequests(bidderRequest.bids, bidderRequest); expect(requests.length).to.equal(1); - expect(requests[0].url).to.equal(FASTLANE_ENDPOINT); + expect(requests[0].url).to.equal('https://fastlane.rubiconproject.com/a/api/fastlane.json'); }); it('should send request as banner when invalid video bid in multiple mediaType bidRequest', function () { @@ -1829,7 +1948,7 @@ describe('the rubicon adapter', function () { let requests = spec.buildRequests(bidRequestCopy.bids, bidRequestCopy); expect(requests.length).to.equal(1); - expect(requests[0].url).to.equal(FASTLANE_ENDPOINT); + expect(requests[0].url).to.equal('https://fastlane.rubiconproject.com/a/api/fastlane.json'); }); it('should include coppa flag in video bid request', () => { @@ -1853,39 +1972,58 @@ describe('the rubicon adapter', function () { it('should include first party data', () => { createVideoBidderRequest(); - const context = { - keywords: ['e', 'f'], - rating: '4-star' + const site = { + ext: { + data: { + page: 'home' + } + }, + content: { + data: [{foo: 'bar'}] + }, + keywords: 'e,f', + rating: '4-star', + data: [{foo: 'bar'}] }; const user = { - keywords: ['d'], + ext: { + data: { + age: 31 + } + }, + keywords: 'd', gender: 'M', yob: '1984', - geo: { country: 'ca' } + geo: {country: 'ca'}, + data: [{foo: 'bar'}] }; sandbox.stub(config, 'getConfig').callsFake(key => { const config = { - fpd: { - context, + ortb2: { + site, user } }; 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({}, site, {keywords: bidderRequest.bids[0].params.keywords.join(',')}), + user: Object.assign({}, user), + siteData: Object.assign({}, site.ext.data, bidderRequest.bids[0].params.inventory), + userData: Object.assign({}, user.ext.data, bidderRequest.bids[0].params.visitor), + }; + + delete request.data.site.page; + delete request.data.site.content.language; + + 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.siteData); + expect(request.data.user.ext.data).to.deep.equal(expected.userData); }); it('should include storedAuctionResponse in video bid request', function () { @@ -1904,28 +2042,33 @@ describe('the rubicon adapter', function () { expect(request.data.imp[0].ext.prebid.storedauctionresponse.id).to.equal('11111'); }); - it('should include pbAdSlot in bid request', function () { + it('should include pbadslot in bid request', function () { createVideoBidderRequest(); - bidderRequest.bids[0].fpd = { - context: { - pbAdSlot: '1234567890' + bidderRequest.bids[0].ortb2Imp = { + ext: { + data: { + pbadslot: '1234567890' + } } - }; + } sandbox.stub(Date, 'now').callsFake(() => bidderRequest.auctionStart + 100 ); const [request] = spec.buildRequests(bidderRequest.bids, bidderRequest); - expect(request.data.imp[0].ext.context.data.pbadslot).to.equal('1234567890'); + expect(request.data.imp[0].ext.data.pbadslot).to.equal('1234567890'); }); it('should include GAM ad unit in bid request', function () { createVideoBidderRequest(); - bidderRequest.bids[0].fpd = { - context: { - adServer: { - adSlot: '1234567890' + bidderRequest.bids[0].ortb2Imp = { + ext: { + data: { + adserver: { + adslot: '1234567890', + name: 'adServerName1' + } } } }; @@ -1935,27 +2078,77 @@ describe('the rubicon adapter', function () { ); const [request] = spec.buildRequests(bidderRequest.bids, bidderRequest); - expect(request.data.imp[0].ext.context.data.adslot).to.equal('1234567890'); + expect(request.data.imp[0].ext.data.adserver.adslot).to.equal('1234567890'); + expect(request.data.imp[0].ext.data.adserver.name).to.equal('adServerName1'); }); it('should use the integration type provided in the config instead of the default', () => { createVideoBidderRequest(); - sandbox.stub(config, 'getConfig').callsFake(function (key) { - const config = { - 'rubicon.int_type': 'testType' - }; - return config[key]; - }); + config.setConfig({rubicon: {int_type: 'testType'}}); const [request] = spec.buildRequests(bidderRequest.bids, bidderRequest); expect(request.data.ext.prebid.bidders.rubicon.integration).to.equal('testType'); }); + + it('should pass the user.id provided in the config', function () { + config.setConfig({user: {id: '123'}}); + createVideoBidderRequest(); + + sandbox.stub(Date, 'now').callsFake(() => + bidderRequest.auctionStart + 100 + ); + + let [request] = spec.buildRequests(bidderRequest.bids, bidderRequest); + let post = request.data; + + expect(post).to.have.property('imp') + // .with.length.of(1); + let imp = post.imp[0]; + expect(imp.id).to.equal(bidderRequest.bids[0].adUnitCode); + expect(imp.exp).to.equal(undefined); + expect(imp.video.w).to.equal(640); + expect(imp.video.h).to.equal(480); + expect(imp.video.pos).to.equal(1); + expect(imp.video.context).to.equal('instream'); + expect(imp.video.minduration).to.equal(15); + expect(imp.video.maxduration).to.equal(30); + expect(imp.video.startdelay).to.equal(0); + expect(imp.video.skip).to.equal(1); + expect(imp.video.skipafter).to.equal(15); + expect(imp.ext.rubicon.video.playerWidth).to.equal(640); + expect(imp.ext.rubicon.video.playerHeight).to.equal(480); + expect(imp.ext.rubicon.video.size_id).to.equal(201); + expect(imp.ext.rubicon.video.language).to.equal('en'); + // Also want it to be in post.site.content.language + expect(post.site.content.language).to.equal('en'); + expect(imp.ext.rubicon.video.skip).to.equal(1); + expect(imp.ext.rubicon.video.skipafter).to.equal(15); + expect(imp.ext.prebid.auctiontimestamp).to.equal(1472239426000); + expect(post.user.ext.consent).to.equal('BOJ/P2HOJ/P2HABABMAAAAAZ+A=='); + + // Config user.id + expect(post.user.id).to.equal('123'); + + expect(post.regs.ext.gdpr).to.equal(1); + expect(post.regs.ext.us_privacy).to.equal('1NYN'); + expect(post).to.have.property('ext').that.is.an('object'); + expect(post.ext.prebid.targeting.includewinners).to.equal(true); + expect(post.ext.prebid).to.have.property('cache').that.is.an('object'); + expect(post.ext.prebid.cache).to.have.property('vastxml').that.is.an('object'); + expect(post.ext.prebid.cache.vastxml).to.have.property('returnCreative').that.is.an('boolean'); + expect(post.ext.prebid.cache.vastxml.returnCreative).to.equal(false); + expect(post.ext.prebid.bidders.rubicon.integration).to.equal(PBS_INTEGRATION); + }) }); describe('combineSlotUrlParams', function () { it('should combine an array of slot url params', function () { expect(spec.combineSlotUrlParams([])).to.deep.equal({}); - expect(spec.combineSlotUrlParams([{p1: 'foo', p2: 'test', p3: ''}])).to.deep.equal({p1: 'foo', p2: 'test', p3: ''}); + expect(spec.combineSlotUrlParams([{p1: 'foo', p2: 'test', p3: ''}])).to.deep.equal({ + p1: 'foo', + p2: 'test', + p3: '' + }); expect(spec.combineSlotUrlParams([{}, {p1: 'foo', p2: 'test'}])).to.deep.equal({p1: ';foo', p2: ';test'}); @@ -1986,7 +2179,6 @@ describe('the rubicon adapter', function () { 'size_id': 15, 'alt_size_ids': '43', 'p_pos': 'atf', - 'rp_floor': 0.01, 'rp_secure': /[01]/, 'tk_flint': INTEGRATION, 'x_source.tid': 'd45dd707-a418-42ec-b8a7-b70a6c6fab0b', @@ -2018,7 +2210,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'); }); }); @@ -2097,6 +2289,7 @@ describe('the rubicon adapter', function () { 'impression_id': '153dc240-8229-4604-b8f5-256933b9374c', 'size_id': '15', 'ad_id': '6', + 'adomain': ['test.com'], 'advertiser': 7, 'network': 8, 'creative_id': 'crid-9', @@ -2118,6 +2311,7 @@ describe('the rubicon adapter', function () { 'impression_id': '153dc240-8229-4604-b8f5-256933b9374d', 'size_id': '43', 'ad_id': '7', + 'adomain': ['test.com'], 'advertiser': 7, 'network': 8, 'creative_id': 'crid-9', @@ -2152,6 +2346,8 @@ describe('the rubicon adapter', function () { expect(bids[0].rubicon.networkId).to.equal(8); expect(bids[0].creativeId).to.equal('crid-9'); expect(bids[0].currency).to.equal('USD'); + expect(bids[0].meta.mediaType).to.equal('banner'); + expect(String(bids[0].meta.advertiserDomains)).to.equal('test.com'); expect(bids[0].ad).to.contain(`alert('foo')`) .and.to.contain(``) .and.to.contain(`
`); @@ -2436,6 +2632,146 @@ describe('the rubicon adapter', function () { expect(bids[0].cpm).to.be.equal(0); }); + it('should create bids with matching requestIds if imp id matches', function () { + let bidRequests = [{ + 'bidder': 'rubicon', + 'params': { + 'accountId': 1001, + 'siteId': 12345, + 'zoneId': 67890, + 'floor': null + }, + 'mediaTypes': { + 'banner': { + 'sizes': [[300, 250]] + } + }, + 'adUnitCode': 'div-gpt-ad-1460505748561-0', + 'transactionId': '404a7b28-f276-41cc-a5cf-c1d3dc5671f9', + 'sizes': [[300, 250]], + 'bidId': '557ba307cef098', + 'bidderRequestId': '46a00704ffeb7', + 'auctionId': '3fdc6494-da94-44a0-a292-b55a90b08b2c', + 'src': 'client', + 'bidRequestsCount': 1, + 'bidderRequestsCount': 1, + 'bidderWinsCount': 0, + 'startTime': 1615412098213 + }, { + 'bidder': 'rubicon', + 'params': { + 'accountId': 1001, + 'siteId': 12345, + 'zoneId': 67890, + 'floor': null + }, + 'mediaTypes': { + 'banner': { + 'sizes': [[300, 250]] + } + }, + 'adUnitCode': 'div-gpt-ad-1460505748561-1', + 'transactionId': '404a7b28-f276-41cc-a5cf-c1d3dc5671f9', + 'sizes': [[300, 250]], + 'bidId': '456gt123jkl098', + 'bidderRequestId': '46a00704ffeb7', + 'auctionId': '3fdc6494-da94-44a0-a292-b55a90b08b2c', + 'src': 'client', + 'bidRequestsCount': 1, + 'bidderRequestsCount': 1, + 'bidderWinsCount': 0, + 'startTime': 1615412098213 + }]; + + let response = { + 'status': 'ok', + 'account_id': 14062, + 'site_id': 70608, + 'zone_id': 530022, + 'size_id': 15, + 'alt_size_ids': [ + 43 + ], + 'tracking': '', + 'inventory': {}, + 'ads': [ + { + 'status': 'ok', + 'impression_id': '153dc240-8229-4604-b8f5-256933b9374c', + 'size_id': '15', + 'ad_id': '6', + 'advertiser': 7, + 'network': 8, + 'creative_id': 'crid-9', + 'type': 'script', + 'script': 'alert(\'foo\')', + 'campaign_id': 10, + 'cpm': 0.811, + 'targeting': [ + { + 'key': 'rpfl_14062', + 'values': [ + '15_tier_all_test' + ] + } + ] + }, + { + 'status': 'ok', + 'impression_id': '153dc240-8229-4604-b8f5-256933b9374c', + 'size_id': '15', + 'ad_id': '7', + 'advertiser': 7, + 'network': 8, + 'creative_id': 'crid-9', + 'type': 'script', + 'script': 'alert(\'foo\')', + 'campaign_id': 10, + 'cpm': 0.911, + 'targeting': [ + { + 'key': 'rpfl_14062', + 'values': [ + '43_tier_all_test' + ] + } + ] + }, + { + 'status': 'ok', + 'impression_id': '153dc240-8229-4604-b8f5-256933b9374d', + 'size_id': '43', + 'ad_id': '7', + 'advertiser': 7, + 'network': 8, + 'creative_id': 'crid-9', + 'type': 'script', + 'script': 'alert(\'foo\')', + 'campaign_id': 10, + 'cpm': 1.911, + 'targeting': [ + { + 'key': 'rpfl_14062', + 'values': [ + '43_tier_all_test' + ] + } + ] + } + ] + }; + + config.setConfig({ multibid: [{bidder: 'rubicon', maxbids: 2, targetbiddercodeprefix: 'rubi'}] }); + + let bids = spec.interpretResponse({body: response}, { + bidRequest: bidRequests + }); + + expect(bids).to.be.lengthOf(3); + expect(bids[0].requestId).to.not.equal(bids[1].requestId); + expect(bids[1].requestId).to.equal(bids[2].requestId); + }); + it('should handle an error with no ads returned', function () { let response = { 'status': 'ok', @@ -2511,7 +2847,7 @@ describe('the rubicon adapter', function () { }] }; - let bids = spec.interpretResponse({ body: response }, { + let bids = spec.interpretResponse({body: response}, { bidRequest: [utils.deepClone(bidderRequest.bids[0])] }); @@ -2522,7 +2858,7 @@ describe('the rubicon adapter', function () { describe('singleRequest enabled', function () { it('handles bidRequest of type Array and returns associated adUnits', function () { const overrideMap = []; - overrideMap[0] = { impression_id: '1' }; + overrideMap[0] = {impression_id: '1'}; const stubAds = []; for (let i = 0; i < 10; i++) { @@ -2544,7 +2880,8 @@ describe('the rubicon adapter', function () { 'tracking': '', 'inventory': {}, 'ads': stubAds - }}, { bidRequest: stubBids }); + } + }, {bidRequest: stubBids}); expect(bids).to.be.a('array').with.lengthOf(10); bids.forEach((bid) => { @@ -2577,7 +2914,7 @@ describe('the rubicon adapter', function () { it('handles incorrect adUnits length by returning all bids with matching ads', function () { const overrideMap = []; - overrideMap[0] = { impression_id: '1' }; + overrideMap[0] = {impression_id: '1'}; const stubAds = []; for (let i = 0; i < 6; i++) { @@ -2599,7 +2936,8 @@ describe('the rubicon adapter', function () { 'tracking': '', 'inventory': {}, 'ads': stubAds - }}, { bidRequest: stubBids }); + } + }, {bidRequest: stubBids}); // no bids expected because response didn't match requested bid number expect(bids).to.be.a('array').with.lengthOf(6); @@ -2610,11 +2948,11 @@ describe('the rubicon adapter', function () { // Create overrides to break associations between bids and ads // Each override should cause one less bid to be returned by interpretResponse const overrideMap = []; - overrideMap[0] = { impression_id: '1' }; - overrideMap[2] = { status: 'error' }; - overrideMap[4] = { status: 'error' }; - overrideMap[7] = { status: 'error' }; - overrideMap[8] = { status: 'error' }; + overrideMap[0] = {impression_id: '1'}; + overrideMap[2] = {status: 'error'}; + overrideMap[4] = {status: 'error'}; + overrideMap[7] = {status: 'error'}; + overrideMap[8] = {status: 'error'}; for (let i = 0; i < 10; i++) { stubAds.push(createResponseAdByIndex(i, sizeMap[i].sizeId, overrideMap)); @@ -2635,7 +2973,8 @@ describe('the rubicon adapter', function () { 'tracking': '', 'inventory': {}, 'ads': stubAds - }}, { bidRequest: stubBids }); + } + }, {bidRequest: stubBids}); expect(bids).to.be.a('array').with.lengthOf(6); bids.forEach((bid) => { @@ -2682,13 +3021,15 @@ describe('the rubicon adapter', function () { bid: [{ id: '0', impid: 'instream_video1', + adomain: ['test.com'], price: 2, crid: '4259970', ext: { bidder: { rp: { mime: 'application/javascript', - size_id: 201 + size_id: 201, + advid: 12345 } }, prebid: { @@ -2717,6 +3058,9 @@ describe('the rubicon adapter', function () { expect(bids[0].netRevenue).to.equal(true); expect(bids[0].adserverTargeting).to.deep.equal({hb_uuid: '0c498f63-5111-4bed-98e2-9be7cb932a64'}); expect(bids[0].mediaType).to.equal('video'); + expect(bids[0].meta.mediaType).to.equal('video'); + expect(String(bids[0].meta.advertiserDomains)).to.equal('test.com'); + expect(bids[0].meta.advertiserId).to.equal(12345); expect(bids[0].bidderCode).to.equal('rubicon'); expect(bids[0].currency).to.equal('USD'); expect(bids[0].width).to.equal(640); @@ -2724,14 +3068,156 @@ describe('the rubicon adapter', function () { }); }); + describe('for outstream video', function () { + const sandbox = sinon.createSandbox(); + beforeEach(function () { + createVideoBidderRequestOutstream(); + config.setConfig({rubicon: { + rendererConfig: { + align: 'left', + closeButton: true + }, + rendererUrl: 'https://example.test/renderer.js' + }}); + window.MagniteApex = { + renderAd: function() { + return null; + } + } + }); + + afterEach(function () { + sandbox.restore(); + delete window.MagniteApex; + }); + + it('should register a successful bid', function () { + let response = { + cur: 'USD', + seatbid: [{ + bid: [{ + id: '0', + impid: 'outstream_video1', + adomain: ['test.com'], + price: 2, + crid: '4259970', + ext: { + bidder: { + rp: { + mime: 'application/javascript', + size_id: 201, + advid: 12345 + } + }, + prebid: { + targeting: { + hb_uuid: '0c498f63-5111-4bed-98e2-9be7cb932a64' + }, + type: 'video' + } + } + }], + group: 0, + seat: 'rubicon' + }], + }; + + let bids = spec.interpretResponse({body: response}, { + bidRequest: bidderRequest.bids[0] + }); + + expect(bids).to.be.lengthOf(1); + + expect(bids[0].seatBidId).to.equal('0'); + expect(bids[0].creativeId).to.equal('4259970'); + expect(bids[0].cpm).to.equal(2); + expect(bids[0].ttl).to.equal(300); + expect(bids[0].netRevenue).to.equal(true); + expect(bids[0].adserverTargeting).to.deep.equal({hb_uuid: '0c498f63-5111-4bed-98e2-9be7cb932a64'}); + expect(bids[0].mediaType).to.equal('video'); + expect(bids[0].meta.mediaType).to.equal('video'); + expect(String(bids[0].meta.advertiserDomains)).to.equal('test.com'); + expect(bids[0].meta.advertiserId).to.equal(12345); + expect(bids[0].bidderCode).to.equal('rubicon'); + expect(bids[0].currency).to.equal('USD'); + expect(bids[0].width).to.equal(640); + expect(bids[0].height).to.equal(320); + // check custom renderer + expect(typeof bids[0].renderer).to.equal('object'); + expect(bids[0].renderer.getConfig()).to.deep.equal({ + align: 'left', + closeButton: true + }); + expect(bids[0].renderer.url).to.equal('https://example.test/renderer.js'); + }); + + it('should render ad with Magnite renderer', function () { + let response = { + cur: 'USD', + seatbid: [{ + bid: [{ + id: '0', + impid: 'outstream_video1', + adomain: ['test.com'], + price: 2, + crid: '4259970', + ext: { + bidder: { + rp: { + mime: 'application/javascript', + size_id: 201, + advid: 12345 + } + }, + prebid: { + targeting: { + hb_uuid: '0c498f63-5111-4bed-98e2-9be7cb932a64' + }, + type: 'video' + } + }, + nurl: 'https://test.com/vast.xml' + }], + group: 0, + seat: 'rubicon' + }], + }; + + sinon.spy(window.MagniteApex, 'renderAd'); + + let bids = spec.interpretResponse({body: response}, { + bidRequest: bidderRequest.bids[0] + }); + const bid = bids[0]; + bid.adUnitCode = 'outstream_video1_placement'; + const adUnit = document.createElement('div'); + adUnit.id = bid.adUnitCode; + document.body.appendChild(adUnit); + + bid.renderer.render(bid); + + const renderCall = window.MagniteApex.renderAd.getCall(0); + expect(renderCall.args[0]).to.deep.equal({ + closeButton: true, + collapse: true, + height: 320, + label: undefined, + placement: { + align: 'left', + attachTo: '#outstream_video1_placement', + position: 'append', + }, + vastUrl: 'https://test.com/vast.xml', + width: 640 + }); + // cleanup + adUnit.remove(); + }); + }); + describe('config with integration type', () => { it('should use the integration type provided in the config instead of the default', () => { - sandbox.stub(config, 'getConfig').callsFake(function (key) { - const config = { - 'rubicon.int_type': 'testType' - }; - return config[key]; - }); + config.setConfig({rubicon: {int_type: 'testType'}}); const [request] = spec.buildRequests(bidderRequest.bids, bidderRequest); expect(parseQuery(request.data).tk_flint).to.equal('testType_v$prebid.version$'); }); @@ -2766,7 +3252,7 @@ describe('the rubicon adapter', function () { }); it('should pass gdpr params if consent is true', function () { - expect(spec.getUserSyncs({ iframeEnabled: true }, {}, { + expect(spec.getUserSyncs({iframeEnabled: true}, {}, { gdprApplies: true, consentString: 'foo' })).to.deep.equal({ type: 'iframe', url: `${emilyUrl}?gdpr=1&gdpr_consent=foo` @@ -2774,7 +3260,7 @@ describe('the rubicon adapter', function () { }); it('should pass gdpr params if consent is false', function () { - expect(spec.getUserSyncs({ iframeEnabled: true }, {}, { + expect(spec.getUserSyncs({iframeEnabled: true}, {}, { gdprApplies: false, consentString: 'foo' })).to.deep.equal({ type: 'iframe', url: `${emilyUrl}?gdpr=0&gdpr_consent=foo` @@ -2782,7 +3268,7 @@ describe('the rubicon adapter', function () { }); it('should pass gdpr param gdpr_consent only when gdprApplies is undefined', function () { - expect(spec.getUserSyncs({ iframeEnabled: true }, {}, { + expect(spec.getUserSyncs({iframeEnabled: true}, {}, { consentString: 'foo' })).to.deep.equal({ type: 'iframe', url: `${emilyUrl}?gdpr_consent=foo` @@ -2790,13 +3276,13 @@ describe('the rubicon adapter', function () { }); it('should pass no params if gdpr consentString is not defined', function () { - expect(spec.getUserSyncs({ iframeEnabled: true }, {}, {})).to.deep.equal({ + expect(spec.getUserSyncs({iframeEnabled: true}, {}, {})).to.deep.equal({ type: 'iframe', url: `${emilyUrl}` }); }); it('should pass no params if gdpr consentString is a number', function () { - expect(spec.getUserSyncs({ iframeEnabled: true }, {}, { + expect(spec.getUserSyncs({iframeEnabled: true}, {}, { consentString: 0 })).to.deep.equal({ type: 'iframe', url: `${emilyUrl}` @@ -2804,7 +3290,7 @@ describe('the rubicon adapter', function () { }); it('should pass no params if gdpr consentString is null', function () { - expect(spec.getUserSyncs({ iframeEnabled: true }, {}, { + expect(spec.getUserSyncs({iframeEnabled: true}, {}, { consentString: null })).to.deep.equal({ type: 'iframe', url: `${emilyUrl}` @@ -2812,7 +3298,7 @@ describe('the rubicon adapter', function () { }); it('should pass no params if gdpr consentString is a object', function () { - expect(spec.getUserSyncs({ iframeEnabled: true }, {}, { + expect(spec.getUserSyncs({iframeEnabled: true}, {}, { consentString: {} })).to.deep.equal({ type: 'iframe', url: `${emilyUrl}` @@ -2820,19 +3306,19 @@ describe('the rubicon adapter', function () { }); it('should pass no params if gdpr is not defined', function () { - expect(spec.getUserSyncs({ iframeEnabled: true }, {}, undefined)).to.deep.equal({ + expect(spec.getUserSyncs({iframeEnabled: true}, {}, undefined)).to.deep.equal({ type: 'iframe', url: `${emilyUrl}` }); }); it('should pass us_privacy if uspConsent is defined', function () { - expect(spec.getUserSyncs({ iframeEnabled: true }, {}, undefined, '1NYN')).to.deep.equal({ + expect(spec.getUserSyncs({iframeEnabled: true}, {}, undefined, '1NYN')).to.deep.equal({ type: 'iframe', url: `${emilyUrl}?us_privacy=1NYN` }); }); it('should pass us_privacy after gdpr if both are present', function () { - expect(spec.getUserSyncs({ iframeEnabled: true }, {}, { + expect(spec.getUserSyncs({iframeEnabled: true}, {}, { consentString: 'foo' }, '1NYN')).to.deep.equal({ type: 'iframe', url: `${emilyUrl}?gdpr_consent=foo&us_privacy=1NYN` @@ -2840,8 +3326,8 @@ describe('the rubicon adapter', function () { }); }); - describe('get price granularity', function() { - it('should return correct buckets for all price granularity values', function() { + describe('get price granularity', function () { + it('should return correct buckets for all price granularity values', function () { const CUSTOM_PRICE_BUCKET_ITEM = {max: 5, increment: 0.5}; const mockConfig = { @@ -2874,7 +3360,7 @@ describe('the rubicon adapter', function () { }); }); - describe('Supply Chain Support', function() { + describe('Supply Chain Support', function () { const nodePropsOrder = ['asi', 'sid', 'hp', 'rid', 'name', 'domain']; let bidRequests; let schainConfig; @@ -2967,4 +3453,51 @@ describe('the rubicon adapter', function () { expect(request[0].data.source.ext.schain).to.deep.equal(schain); }); }); + + describe('configurable settings', function() { + afterEach(() => { + config.setConfig({ + rubicon: { + bannerHost: 'rubicon', + videoHost: 'prebid-server', + syncHost: 'eus', + returnVast: false + } + }); + config.resetConfig(); + }); + + beforeEach(function () { + resetUserSync(); + }); + + it('should update fastlane endpoint if', function () { + config.setConfig({ + rubicon: { + bannerHost: 'fastlane-qa', + videoHost: 'prebid-server-qa', + syncHost: 'eus-qa', + returnVast: true + } + }); + + // banner + let [bannerRequest] = spec.buildRequests(bidderRequest.bids, bidderRequest); + expect(bannerRequest.url).to.equal('https://fastlane-qa.rubiconproject.com/a/api/fastlane.json'); + + // video and returnVast + createVideoBidderRequest(); + let [videoRequest] = spec.buildRequests(bidderRequest.bids, bidderRequest); + let post = videoRequest.data; + expect(videoRequest.url).to.equal('https://prebid-server-qa.rubiconproject.com/openrtb2/auction'); + expect(post.ext.prebid.cache.vastxml).to.have.property('returnCreative').that.is.an('boolean'); + expect(post.ext.prebid.cache.vastxml.returnCreative).to.equal(true); + + // user sync + let syncs = spec.getUserSyncs({ + iframeEnabled: true + }); + expect(syncs).to.deep.equal({type: 'iframe', url: 'https://eus-qa.rubiconproject.com/usync.html'}); + }); + }); }); 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/seedtagBidAdapter_spec.js b/test/spec/modules/seedtagBidAdapter_spec.js index 5c8c58196f7..c82c8300d2e 100644 --- a/test/spec/modules/seedtagBidAdapter_spec.js +++ b/test/spec/modules/seedtagBidAdapter_spec.js @@ -1,5 +1,9 @@ import { expect } from 'chai' import { spec, getTimeoutUrl } from 'modules/seedtagBidAdapter.js' +import * as utils from 'src/utils.js' + +const PUBLISHER_ID = '0000-0000-01' +const ADUNIT_ID = '000000' function getSlotConfigs(mediaTypes, params) { return { @@ -16,10 +20,16 @@ function getSlotConfigs(mediaTypes, params) { } } +function createVideoSlotConfig(mediaType) { + return getSlotConfigs(mediaType, { + publisherId: PUBLISHER_ID, + adUnitId: ADUNIT_ID, + placement: 'video' + }) +} + describe('Seedtag Adapter', function() { describe('isBidRequestValid method', function() { - const PUBLISHER_ID = '0000-0000-01' - const ADUNIT_ID = '000000' describe('returns true', function() { describe('when banner slot config has all mandatory params', () => { describe('and placement has the correct value', function() { @@ -33,7 +43,7 @@ describe('Seedtag Adapter', function() { } ) } - const placements = ['banner', 'video', 'inImage', 'inScreen'] + const placements = ['banner', 'video', 'inImage', 'inScreen', 'inArticle'] placements.forEach(placement => { it('should be ' + placement, function() { const isBidRequestValid = spec.isBidRequestValid( @@ -44,7 +54,7 @@ describe('Seedtag Adapter', function() { }) }) }) - describe('when video slot has all mandatory params.', function() { + describe('when video slot has all mandatory params', function() { it('should return true, when video mediatype object are correct.', function() { const slotConfig = getSlotConfigs( { @@ -66,13 +76,13 @@ describe('Seedtag Adapter', function() { }) describe('returns false', function() { describe('when params are not correct', function() { - function createSlotconfig(params) { + function createSlotConfig(params) { return getSlotConfigs({ banner: {} }, params) } it('does not have the PublisherToken.', function() { const isBidRequestValid = spec.isBidRequestValid( - createSlotconfig({ - adUnitId: '000000', + createSlotConfig({ + adUnitId: ADUNIT_ID, placement: 'banner' }) ) @@ -80,8 +90,8 @@ describe('Seedtag Adapter', function() { }) it('does not have the AdUnitId.', function() { const isBidRequestValid = spec.isBidRequestValid( - createSlotconfig({ - publisherId: '0000-0000-01', + createSlotConfig({ + publisherId: PUBLISHER_ID, placement: 'banner' }) ) @@ -89,25 +99,25 @@ describe('Seedtag Adapter', function() { }) it('does not have the placement.', function() { const isBidRequestValid = spec.isBidRequestValid( - createSlotconfig({ - publisherId: '0000-0000-01', - adUnitId: '000000' + createSlotConfig({ + publisherId: PUBLISHER_ID, + adUnitId: ADUNIT_ID }) ) expect(isBidRequestValid).to.equal(false) }) it('does not have a the correct placement.', function() { const isBidRequestValid = spec.isBidRequestValid( - createSlotconfig({ - publisherId: '0000-0000-01', - adUnitId: '000000', + createSlotConfig({ + publisherId: PUBLISHER_ID, + adUnitId: ADUNIT_ID, placement: 'another_thing' }) ) expect(isBidRequestValid).to.equal(false) }) }) - describe('when video mediaType object is not correct.', function() { + describe('when video mediaType object is not correct', function() { function createVideoSlotconfig(mediaType) { return getSlotConfigs(mediaType, { publisherId: PUBLISHER_ID, @@ -117,19 +127,19 @@ describe('Seedtag Adapter', function() { } it('is a void object', function() { const isBidRequestValid = spec.isBidRequestValid( - createVideoSlotconfig({ video: {} }) + createVideoSlotConfig({ video: {} }) ) expect(isBidRequestValid).to.equal(false) }) it('does not have playerSize.', function() { const isBidRequestValid = spec.isBidRequestValid( - createVideoSlotconfig({ video: { context: 'instream' } }) + createVideoSlotConfig({ video: { context: 'instream' } }) ) expect(isBidRequestValid).to.equal(false) }) it('is not instream ', function() { const isBidRequestValid = spec.isBidRequestValid( - createVideoSlotconfig({ + createVideoSlotConfig({ video: { context: 'outstream', playerSize: [[600, 200]] @@ -138,6 +148,20 @@ describe('Seedtag Adapter', function() { ) expect(isBidRequestValid).to.equal(false) }) + describe('order does not matter', function() { + it('when video is not the first slot', function() { + const isBidRequestValid = spec.isBidRequestValid( + createVideoSlotConfig({ banner: {}, video: {} }) + ) + expect(isBidRequestValid).to.equal(false) + }) + it('when video is the first slot', function() { + const isBidRequestValid = spec.isBidRequestValid( + createVideoSlotConfig({ video: {}, banner: {} }) + ) + expect(isBidRequestValid).to.equal(false) + }) + }) }) }) }) @@ -148,8 +172,8 @@ describe('Seedtag Adapter', function() { timeout: 1000 } const mandatoryParams = { - publisherId: '0000-0000-01', - adUnitId: '000000', + publisherId: PUBLISHER_ID, + adUnitId: ADUNIT_ID, placement: 'banner' } const inStreamParams = Object.assign({}, mandatoryParams, { @@ -176,6 +200,7 @@ describe('Seedtag Adapter', function() { expect(data.url).to.equal('referer') expect(data.publisherToken).to.equal('0000-0000-01') expect(typeof data.version).to.equal('string') + expect(['fixed', 'mobile', 'unknown'].indexOf(data.connectionType)).to.be.above(-1) }) describe('adPosition param', function() { @@ -278,7 +303,7 @@ describe('Seedtag Adapter', function() { 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() { + it('should return a void array, when the server response have no bids.', function() { const request = { data: JSON.stringify({}) } const serverResponse = { body: { bids: [] } } const bids = spec.interpretResponse(serverResponse, request) @@ -300,7 +325,8 @@ describe('Seedtag Adapter', function() { width: 728, height: 90, mediaType: 'display', - ttl: 360 + ttl: 360, + nurl: 'testurl.com/nurl' } ], cookieSync: { url: '' } @@ -315,6 +341,7 @@ describe('Seedtag Adapter', function() { expect(bids[0].currency).to.equal('USD') expect(bids[0].netRevenue).to.equal(true) expect(bids[0].ad).to.equal('content') + expect(bids[0].nurl).to.equal('testurl.com/nurl') }) }) describe('the bid is a video', function() { @@ -331,7 +358,8 @@ describe('Seedtag Adapter', function() { width: 728, height: 90, mediaType: 'video', - ttl: 360 + ttl: 360, + nurl: undefined } ], cookieSync: { url: '' } @@ -381,6 +409,14 @@ describe('Seedtag Adapter', function() { }) describe('onTimeout', function () { + beforeEach(function() { + sinon.stub(utils, 'triggerPixel') + }) + + afterEach(function() { + utils.triggerPixel.restore() + }) + it('should return the correct endpoint', function () { const params = { publisherId: '0000', adUnitId: '11111' } const timeoutData = [{ params: [ params ] }]; @@ -392,5 +428,44 @@ describe('Seedtag Adapter', function() { params.adUnitId ) }) + + it('should set the timeout pixel', function() { + const params = { publisherId: '0000', adUnitId: '11111' } + const timeoutData = [{ params: [ params ] }]; + spec.onTimeout(timeoutData) + expect(utils.triggerPixel.calledWith('https://s.seedtag.com/se/hb/timeout?publisherToken=' + + params.publisherId + + '&adUnitId=' + + params.adUnitId)).to.equal(true); + }) + }) + + describe('onBidWon', function () { + beforeEach(function() { + sinon.stub(utils, 'triggerPixel') + }) + + afterEach(function() { + utils.triggerPixel.restore() + }) + + describe('without nurl', function() { + const bid = {} + + it('does not create pixel ', function() { + spec.onBidWon(bid) + expect(utils.triggerPixel.called).to.equal(false); + }) + }) + + describe('with nurl', function () { + const nurl = 'http://seedtag_domain/won' + const bid = { nurl } + + it('creates nurl pixel if bid nurl', function() { + spec.onBidWon({ nurl }) + expect(utils.triggerPixel.calledWith(nurl)).to.equal(true); + }) + }) }) }) diff --git a/test/spec/modules/shareUserIds_spec.js b/test/spec/modules/shareUserIds_spec.js index 451892919cb..67e39533fc7 100644 --- a/test/spec/modules/shareUserIds_spec.js +++ b/test/spec/modules/shareUserIds_spec.js @@ -50,6 +50,16 @@ describe('#userIdTargeting', function() { pubads.setTargeting('test', ['TEST']); config.GAM_KEYS.tdid = ''; userIdTargeting(userIds, config); + expect(pubads.getTargeting('tdid')).to.be.an('array').that.is.empty; expect(pubads.getTargeting('test')).to.deep.equal(['TEST']); }); + + it('User Id Targeting is added to googletag queue when GPT is not ready', function() { + let pubads = window.googletag.pubads; + delete window.googletag.pubads; + userIdTargeting(userIds, config); + window.googletag.pubads = pubads; + window.googletag.cmd.map(command => command()); + expect(window.googletag.pubads().getTargeting('TD_ID')).to.deep.equal(['my-tdid']); + }); }); diff --git a/test/spec/modules/sharedIdSystem_spec.js b/test/spec/modules/sharedIdSystem_spec.js new file mode 100644 index 00000000000..ad51fe81cde --- /dev/null +++ b/test/spec/modules/sharedIdSystem_spec.js @@ -0,0 +1,77 @@ +import { + sharedIdSubmodule, +} from 'modules/sharedIdSystem.js'; +import { server } from 'test/mocks/xhr.js'; +import {uspDataHandler} from 'src/adapterManager'; + +let expect = require('chai').expect; + +describe('SharedId System', function() { + const SHAREDID_RESPONSE = {sharedId: 'testsharedid'}; + let uspConsentDataStub; + describe('Xhr Requests from getId()', function() { + let callbackSpy = sinon.spy(); + + beforeEach(function() { + callbackSpy.resetHistory(); + uspConsentDataStub = sinon.stub(uspDataHandler, 'getConsentData'); + }); + + afterEach(function () { + uspConsentDataStub.restore(); + }); + + 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); + }); + + it('should call shared id endpoint with usp consent data and handle a valid response', function () { + uspConsentDataStub.returns('1YYY'); + 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?us_privacy=1YYY&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 92f9fd11eeb..5c8e01536dd 100644 --- a/test/spec/modules/sharethroughBidAdapter_spec.js +++ b/test/spec/modules/sharethroughBidAdapter_spec.js @@ -1,6 +1,8 @@ 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'; +import { config } from 'src/config'; const spec = newBidder(sharethroughAdapterSpec).getSpec(); const bidRequests = [ @@ -12,7 +14,27 @@ const bidRequests = [ params: { pkey: 'aaaa1111' }, - userId: { tdid: 'fake-tdid' } + userId: { + tdid: 'fake-tdid', + 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' + } }, { bidder: 'sharethrough', @@ -33,13 +55,33 @@ const bidRequests = [ pkey: 'cccc3333', 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', @@ -51,7 +93,7 @@ const prebidRequests = [ } }, { - method: 'GET', + method: 'POST', url: 'https://btlr.sharethrough.com/WYu2BXv1/v1', data: { bidId: 'bidId', @@ -63,7 +105,7 @@ const prebidRequests = [ } }, { - method: 'GET', + method: 'POST', url: 'https://btlr.sharethrough.com/WYu2BXv1/v1', data: { bidId: 'bidId', @@ -76,7 +118,7 @@ const prebidRequests = [ } }, { - method: 'GET', + method: 'POST', url: 'https://btlr.sharethrough.com/WYu2BXv1/v1', data: { bidId: 'bidId', @@ -88,7 +130,7 @@ const prebidRequests = [ } }, { - method: 'GET', + method: 'POST', url: 'https://btlr.sharethrough.com/WYu2BXv1/v1', data: { bidId: 'bidId', @@ -98,7 +140,7 @@ const prebidRequests = [ skipIframeBusting: false, sizes: [[300, 250], [300, 300], [250, 250], [600, 50]] } - }, + } ]; const bidderResponse = { @@ -121,27 +163,31 @@ const bidderResponse = { }; const setUserAgent = (uaString) => { - window.navigator['__defineGetter__']('userAgent', function () { + window.navigator['__defineGetter__']('userAgent', function() { return uaString; }); }; -describe('sharethrough internal spec', function () { - let windowSpy, windowTopSpy; - +describe('sharethrough internal spec', function() { + let windowStub, windowTopStub; + let stubbedReturn = [{ + appendChild: () => undefined + }] beforeEach(function() { - windowSpy = sinon.spy(window.document, 'getElementsByTagName'); - windowTopSpy = sinon.spy(window.top.document, 'getElementsByTagName'); + windowStub = sinon.stub(window.document, 'getElementsByTagName'); + windowTopStub = sinon.stub(window.top.document, 'getElementsByTagName'); + windowStub.withArgs('body').returns(stubbedReturn); + windowTopStub.withArgs('body').returns(stubbedReturn); }); afterEach(function() { - windowSpy.restore(); - windowTopSpy.restore(); + windowStub.restore(); + windowTopStub.restore(); window.STR = undefined; window.top.STR = undefined; }); - describe('we cannot access top level document', function () { + describe('we cannot access top level document', function() { beforeEach(function() { window.lockedInFrame = true; }); @@ -150,44 +196,44 @@ describe('sharethrough internal spec', function () { window.lockedInFrame = false; }); - it('appends sfp.js to the safeframe', function () { + it('appends sfp.js to the safeframe', function() { sharethroughInternal.handleIframe(); - expect(windowSpy.calledOnce).to.be.true; + expect(windowStub.calledOnce).to.be.true; }); - it('does not append anything if sfp.js is already loaded in the safeframe', function () { + it('does not append anything if sfp.js is already loaded in the safeframe', function() { window.STR = { Tag: true }; sharethroughInternal.handleIframe(); - expect(windowSpy.notCalled).to.be.true; - expect(windowTopSpy.notCalled).to.be.true; + expect(windowStub.notCalled).to.be.true; + expect(windowTopStub.notCalled).to.be.true; }); }); - describe('we are able to bust out of the iframe', function () { - it('appends sfp.js to window.top', function () { + describe('we are able to bust out of the iframe', function() { + it('appends sfp.js to window.top', function() { sharethroughInternal.handleIframe(); - expect(windowSpy.calledOnce).to.be.true; - expect(windowTopSpy.calledOnce).to.be.true; + expect(windowStub.calledOnce).to.be.true; + expect(windowTopStub.calledOnce).to.be.true; }); - it('only appends sfp-set-targeting.js if sfp.js is already loaded on the page', function () { + it('only appends sfp-set-targeting.js if sfp.js is already loaded on the page', function() { window.top.STR = { Tag: true }; sharethroughInternal.handleIframe(); - expect(windowSpy.calledOnce).to.be.true; - expect(windowTopSpy.notCalled).to.be.true; + expect(windowStub.calledOnce).to.be.true; + expect(windowTopStub.notCalled).to.be.true; }); }); }); -describe('sharethrough adapter spec', function () { - describe('.code', function () { - it('should return a bidder code of sharethrough', function () { +describe('sharethrough adapter spec', function() { + describe('.code', function() { + it('should return a bidder code of sharethrough', function() { expect(spec.code).to.eql('sharethrough'); }); }); - describe('.isBidRequestValid', function () { - it('should return false if req has no pkey', function () { + describe('.isBidRequestValid', function() { + it('should return false if req has no pkey', function() { const invalidBidRequest = { bidder: 'sharethrough', params: { @@ -197,7 +243,7 @@ describe('sharethrough adapter spec', function () { expect(spec.isBidRequestValid(invalidBidRequest)).to.eql(false); }); - it('should return false if req has wrong bidder code', function () { + it('should return false if req has wrong bidder code', function() { const invalidBidRequest = { bidder: 'notSharethrough', params: { @@ -207,22 +253,22 @@ describe('sharethrough adapter spec', function () { expect(spec.isBidRequestValid(invalidBidRequest)).to.eql(false); }); - it('should return true if req is correct', function () { + it('should return true if req is correct', function() { expect(spec.isBidRequestValid(bidRequests[0])).to.eq(true); expect(spec.isBidRequestValid(bidRequests[1])).to.eq(true); - }) + }); }); - describe('.buildRequests', function () { - it('should return an array of requests', function () { + describe('.buildRequests', function() { + it('should return an array of requests', function() { const builtBidRequests = spec.buildRequests(bidRequests); 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 () { + it('should set the instant_play_capable parameter correctly based on browser userAgent string', function() { setUserAgent('Android Chrome/60'); let builtBidRequests = spec.buildRequests(bidRequests); expect(builtBidRequests[0].data.instant_play_capable).to.be.true; @@ -252,31 +298,31 @@ describe('sharethrough adapter spec', function () { const stub = sinon.stub(sharethroughInternal, 'getProtocol').returns('http:'); const bidRequest = spec.buildRequests(bidRequests, null)[0]; expect(bidRequest.data.secure).to.be.false; - stub.restore() + stub.restore(); }); it('should set the secure parameter to true when the protocol is https', function() { const stub = sinon.stub(sharethroughInternal, 'getProtocol').returns('https:'); const bidRequest = spec.buildRequests(bidRequests, null)[0]; expect(bidRequest.data.secure).to.be.true; - stub.restore() + stub.restore(); }); it('should set the secure parameter to true when the protocol is neither http or https', function() { const stub = sinon.stub(sharethroughInternal, 'getProtocol').returns('about:'); const bidRequest = spec.buildRequests(bidRequests, null)[0]; expect(bidRequest.data.secure).to.be.true; - stub.restore() + stub.restore(); }); - it('should add ccpa parameter if uspConsent is present', function () { + it('should add ccpa parameter if uspConsent is present', function() { const uspConsent = '1YNN'; const bidderRequest = { uspConsent: uspConsent }; const bidRequest = spec.buildRequests(bidRequests, bidderRequest)[0]; expect(bidRequest.data.us_privacy).to.eq(uspConsent); }); - it('should add consent parameters if gdprConsent is present', function () { + it('should add consent parameters if gdprConsent is present', function() { const gdprConsent = { consentString: 'consent_string123', gdprApplies: true }; const bidderRequest = { gdprConsent: gdprConsent }; const bidRequest = spec.buildRequests(bidRequests, bidderRequest)[0]; @@ -284,19 +330,63 @@ describe('sharethrough adapter spec', function () { expect(bidRequest.data.consent_string).to.eq('consent_string123'); }); - it('should handle gdprConsent is present but values are undefined case', function () { + it('should handle gdprConsent is present but values are undefined case', function() { const gdprConsent = { consent_string: undefined, gdprApplies: undefined }; const bidderRequest = { gdprConsent: gdprConsent }; const bidRequest = spec.buildRequests(bidRequests, bidderRequest)[0]; - expect(bidRequest.data).to.not.include.any.keys('consent_string') + expect(bidRequest.data).to.not.include.any.keys('consent_string'); }); - it('should add the ttduid parameter if a bid request contains a value for Unified ID from The Trade Desk', function () { + it('should add the ttduid parameter if a bid request contains a value for Unified ID from The Trade Desk', function() { const bidRequest = spec.buildRequests(bidRequests)[0]; expect(bidRequest.data.ttduid).to.eq('fake-tdid'); }); - it('should add Sharethrough specific parameters', function () { + it('should add the pubcid parameter if a bid request contains a value for the Publisher Common ID Module in the' + + ' userId object of the bidrequest', function() { + const bidRequest = spec.buildRequests(bidRequests)[0]; + 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]; + delete bidRequest.userId; + 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({ strData: { @@ -327,6 +417,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'); @@ -344,11 +446,34 @@ describe('sharethrough adapter spec', function () { const builtBidRequest = spec.buildRequests([bidRequest])[0]; expect(builtBidRequest.data).to.not.include.any.keys('bidfloor'); }); + + describe('coppa', function() { + it('should add coppa to request if enabled', function() { + config.setConfig({coppa: true}); + const bidRequest = Object.assign({}, bidRequests[0]); + const builtBidRequest = spec.buildRequests([bidRequest])[0]; + expect(builtBidRequest.data.coppa).to.eq(true); + }); + + it('should not add coppa to request if disabled', function() { + config.setConfig({coppa: false}); + const bidRequest = Object.assign({}, bidRequests[0]); + const builtBidRequest = spec.buildRequests([bidRequest])[0]; + expect(builtBidRequest.data.coppa).to.be.undefined; + }); + + it('should not add coppa to request if unknown value', function() { + config.setConfig({coppa: 'something'}); + const bidRequest = Object.assign({}, bidRequests[0]); + const builtBidRequest = spec.buildRequests([bidRequest])[0]; + expect(builtBidRequest.data.coppa).to.be.undefined; + }); + }); }); - describe('.interpretResponse', function () { - it('returns a correctly parsed out response', function () { - expect(spec.interpretResponse(bidderResponse, prebidRequests[0])[0]).to.include( + describe('.interpretResponse', function() { + it('returns a correctly parsed out response', function() { + expect(spec.interpretResponse(bidderResponse, prebidRequests[0])[0]).to.deep.include( { width: 1, height: 1, @@ -358,10 +483,11 @@ describe('sharethrough adapter spec', function () { currency: 'USD', netRevenue: true, ttl: 360, + meta: { advertiserDomains: [] } }); }); - it('returns a correctly parsed out response with largest size when strData.skipIframeBusting is true', function () { + it('returns a correctly parsed out response with largest size when strData.skipIframeBusting is true', function() { expect(spec.interpretResponse(bidderResponse, prebidRequests[1])[0]).to.include( { width: 300, @@ -371,11 +497,11 @@ describe('sharethrough adapter spec', function () { dealId: 'aDealId', currency: 'USD', netRevenue: true, - ttl: 360, + ttl: 360 }); }); - it('returns a correctly parsed out response with explicitly defined size when strData.skipIframeBusting is true and strData.iframeSize is provided', function () { + it('returns a correctly parsed out response with explicitly defined size when strData.skipIframeBusting is true and strData.iframeSize is provided', function() { expect(spec.interpretResponse(bidderResponse, prebidRequests[2])[0]).to.include( { width: 500, @@ -385,11 +511,11 @@ describe('sharethrough adapter spec', function () { dealId: 'aDealId', currency: 'USD', netRevenue: true, - ttl: 360, + ttl: 360 }); }); - it('returns a correctly parsed out response with explicitly defined size when strData.skipIframeBusting is false and strData.sizes contains [0, 0] only', function () { + it('returns a correctly parsed out response with explicitly defined size when strData.skipIframeBusting is false and strData.sizes contains [0, 0] only', function() { expect(spec.interpretResponse(bidderResponse, prebidRequests[3])[0]).to.include( { width: 0, @@ -399,11 +525,11 @@ describe('sharethrough adapter spec', function () { dealId: 'aDealId', currency: 'USD', netRevenue: true, - ttl: 360, + ttl: 360 }); }); - it('returns a correctly parsed out response with explicitly defined size when strData.skipIframeBusting is false and strData.sizes contains multiple sizes', function () { + it('returns a correctly parsed out response with explicitly defined size when strData.skipIframeBusting is false and strData.sizes contains multiple sizes', function() { expect(spec.interpretResponse(bidderResponse, prebidRequests[4])[0]).to.include( { width: 300, @@ -413,26 +539,26 @@ describe('sharethrough adapter spec', function () { dealId: 'aDealId', currency: 'USD', netRevenue: true, - ttl: 360, + ttl: 360 }); }); - it('returns a blank array if there are no creatives', function () { + it('returns a blank array if there are no creatives', function() { const bidResponse = { body: { creatives: [] } }; expect(spec.interpretResponse(bidResponse, prebidRequests[0])).to.be.an('array').that.is.empty; }); - it('returns a blank array if body object is empty', function () { + it('returns a blank array if body object is empty', function() { const bidResponse = { body: {} }; expect(spec.interpretResponse(bidResponse, prebidRequests[0])).to.be.an('array').that.is.empty; }); - it('returns a blank array if body is null', function () { + it('returns a blank array if body is null', function() { const bidResponse = { body: null }; expect(spec.interpretResponse(bidResponse, prebidRequests[0])).to.be.an('array').that.is.empty; }); - it('correctly generates ad markup when skipIframeBusting is false', function () { + it('correctly generates ad markup when skipIframeBusting is false', function() { const adMarkup = spec.interpretResponse(bidderResponse, prebidRequests[0])[0].ad; let resp = null; @@ -447,7 +573,7 @@ describe('sharethrough adapter spec', function () { expect(adMarkup).to.match(/handleIframe/); }); - it('correctly generates ad markup when skipIframeBusting is true', function () { + it('correctly generates ad markup when skipIframeBusting is true', function() { const adMarkup = spec.interpretResponse(bidderResponse, prebidRequests[1])[0].ad; let resp = null; @@ -461,11 +587,11 @@ describe('sharethrough adapter spec', function () { }); }); - describe('.getUserSyncs', function () { + describe('.getUserSyncs', function() { const cookieSyncs = ['cookieUrl1', 'cookieUrl2', 'cookieUrl3']; const serverResponses = [{ body: { cookieSyncUrls: cookieSyncs } }]; - it('returns an array of correctly formatted user syncs', function () { + it('returns an array of correctly formatted user syncs', function() { const syncArray = spec.getUserSyncs({ pixelEnabled: true }, serverResponses, null, 'fake-privacy-signal'); expect(syncArray).to.deep.equal([ { type: 'image', url: 'cookieUrl1&us_privacy=fake-privacy-signal' }, @@ -474,22 +600,22 @@ describe('sharethrough adapter spec', function () { ); }); - it('returns an empty array if serverResponses is empty', function () { + it('returns an empty array if serverResponses is empty', function() { const syncArray = spec.getUserSyncs({ pixelEnabled: true }, []); expect(syncArray).to.be.an('array').that.is.empty; }); - it('returns an empty array if the body is null', function () { + it('returns an empty array if the body is null', function() { const syncArray = spec.getUserSyncs({ pixelEnabled: true }, [{ body: null }]); expect(syncArray).to.be.an('array').that.is.empty; }); - it('returns an empty array if the body.cookieSyncUrls is missing', function () { + it('returns an empty array if the body.cookieSyncUrls is missing', function() { const syncArray = spec.getUserSyncs({ pixelEnabled: true }, [{ body: { creatives: ['creative'] } }]); expect(syncArray).to.be.an('array').that.is.empty; }); - it('returns an empty array if pixels are not enabled', function () { + it('returns an empty array if pixels are not enabled', function() { const syncArray = spec.getUserSyncs({ pixelEnabled: false }, serverResponses); expect(syncArray).to.be.an('array').that.is.empty; }); diff --git a/test/spec/modules/shinezBidAdapter_spec.js b/test/spec/modules/shinezBidAdapter_spec.js new file mode 100644 index 00000000000..cc3c2451c5d --- /dev/null +++ b/test/spec/modules/shinezBidAdapter_spec.js @@ -0,0 +1,152 @@ +import { expect } from 'chai'; +import sinon from 'sinon'; +import { spec, internal } from 'modules/shinezBidAdapter.js'; + +describe('shinezBidAdapter', () => { + let sandbox; + beforeEach(() => { + sandbox = sinon.sandbox.create(); + }); + afterEach(() => { + sandbox.restore(); + }); + describe('isBidRequestValid', () => { + const cases = [ + [ + 'should return false when placementId is missing', + { + params: {}, + }, + false, + ], + [ + 'should return false when placementId has wrong type', + { + params: { + placementId: 123, + }, + }, + false, + ], + [ + 'should return false when unit has wrong type', + { + params: { + placementId: '00654321', + unit: 150, + }, + }, + false, + ], + [ + 'should return true when required params found and valid', + { + params: { + placementId: '00654321', + }, + }, + true, + ], + [ + 'should return true when all params found and valid', + { + params: { + placementId: '00654321', + unit: '__header-bid-1', + }, + }, + true, + ], + ]; + cases.map(([description, request, expected]) => { + it(description, () => { + const result = spec.isBidRequestValid(request); + expect(result).to.be.equal(expected); + }); + }); + }); + describe('buildRequests', () => { + it('should build server request correctly', () => { + const utcOffset = 300; + const validBidRequests = [ + { + params: { + placementId: '00654321', + unit: 'header-bid-tag-1-shinez', + }, + crumbs: { + pubcid: 'c8584a82-bec3-4347-8d3e-e7612438a161', + }, + mediaTypes: { + banner: { + sizes: [[300, 250]], + }, + }, + adUnitCode: 'header-bid-tag-1', + transactionId: '665760dc-a249-4be7-ae86-91f417b2c65d', + }, + ]; + const bidderRequest = { + refererInfo: { + referer: 'http://site-with-ads.com', + }, + }; + sandbox.stub(Date.prototype, 'getTimezoneOffset').returns(utcOffset); + const result = spec.buildRequests(validBidRequests, bidderRequest); + const expectedData = [ + { + bidId: validBidRequests[0].bidId, + transactionId: validBidRequests[0].transactionId, + crumbs: validBidRequests[0].crumbs, + mediaTypes: validBidRequests[0].mediaTypes, + refererInfo: bidderRequest.refererInfo, + adUnitCode: validBidRequests[0].adUnitCode, + utcOffset: utcOffset, + placementId: validBidRequests[0].params.placementId, + unit: validBidRequests[0].params.unit, + }, + ]; + expect(result.method, "request should be POST'ed").equal('POST'); + expect(result.url, 'request should be send to correct url').equal( + internal.TARGET_URL + ); + expect(result.data, 'request should have correct payload').to.deep.equal( + expectedData + ); + }); + }); + describe('interpretResponse', () => { + it('should interpret bid responses correctly', () => { + const response = { + body: [ + { + bidId: '2ece6496f4d0c9', + cpm: 0.03, + currency: 'USD', + width: 300, + height: 250, + ad: `

The Ad

`, + ttl: 60, + creativeId: 'V8qlA6guwm', + netRevenue: true, + }, + ], + }; + const bids = [ + { + requestId: response.body[0].bidId, + cpm: response.body[0].cpm, + currency: response.body[0].currency, + width: response.body[0].width, + height: response.body[0].height, + ad: response.body[0].ad, + ttl: response.body[0].ttl, + creativeId: response.body[0].creativeId, + netRevenue: response.body[0].netRevenue, + }, + ]; + const result = spec.interpretResponse(response); + expect(result).to.deep.equal(bids); + }); + }); +}); diff --git a/test/spec/modules/sigmoidAnalyticsAdapter_spec.js b/test/spec/modules/sigmoidAnalyticsAdapter_spec.js index 75afb0ed86e..854c3a8e22d 100644 --- a/test/spec/modules/sigmoidAnalyticsAdapter_spec.js +++ b/test/spec/modules/sigmoidAnalyticsAdapter_spec.js @@ -38,7 +38,7 @@ describe('sigmoid Prebid Analytic', function () { events.emit(constants.EVENTS.BID_RESPONSE, {}); events.emit(constants.EVENTS.BID_WON, {}); - sinon.assert.callCount(sigmoidAnalytic.track, 5); + sinon.assert.callCount(sigmoidAnalytic.track, 7); }); }); describe('build utm tag data', function () { diff --git a/test/spec/modules/sirdataRtdProvider_spec.js b/test/spec/modules/sirdataRtdProvider_spec.js new file mode 100644 index 00000000000..a16359c50cb --- /dev/null +++ b/test/spec/modules/sirdataRtdProvider_spec.js @@ -0,0 +1,94 @@ +import { addSegmentData, getSegmentsAndCategories, sirdataSubmodule } from 'modules/sirdataRtdProvider.js'; +import { server } from 'test/mocks/xhr.js'; + +const responseHeader = {'Content-Type': 'application/json'}; + +describe('sirdataRtdProvider', function() { + describe('sirdataSubmodule', function() { + it('successfully instantiates', function () { + expect(sirdataSubmodule.init()).to.equal(true); + }); + }); + + describe('Add Segment Data', function() { + it('adds segment data', function() { + const config = { + params: { + setGptKeyValues: false, + contextualMinRelevancyScore: 50, + bidders: [{ + bidder: 'appnexus' + }, { + bidder: 'other' + }] + } + }; + + let adUnits = [ + { + bids: [{ + bidder: 'appnexus', + params: { + placementId: 13144370 + } + }, { + bidder: 'other' + }] + } + ]; + + let data = { + segments: [111111, 222222], + contextual_categories: {'333333': 100} + }; + + addSegmentData(adUnits, data, config, () => {}); + expect(adUnits[0].bids[0].params.keywords).to.have.deep.property('sd_rtd', ['111111', '222222', '333333']); + expect(adUnits[0].bids[1].ortb2.site.ext.data).to.have.deep.property('sd_rtd', ['333333']); + expect(adUnits[0].bids[1].ortb2.user.ext.data).to.have.deep.property('sd_rtd', ['111111', '222222']); + }); + }); + + describe('Get Segments And Categories', function() { + it('gets data from async request and adds segment data', function() { + const config = { + params: { + setGptKeyValues: false, + contextualMinRelevancyScore: 50, + bidders: [{ + bidder: 'appnexus' + }, { + bidder: 'other' + }] + } + }; + + let reqBidsConfigObj = { + adUnits: [{ + bids: [{ + bidder: 'appnexus', + params: { + placementId: 13144370 + } + }, { + bidder: 'other' + }] + }] + }; + + let data = { + segments: [111111, 222222], + contextual_categories: {'333333': 100} + }; + + getSegmentsAndCategories(reqBidsConfigObj, () => {}, config, {}); + + let request = server.requests[0]; + request.respond(200, responseHeader, JSON.stringify(data)); + + expect(reqBidsConfigObj.adUnits[0].bids[0].params.keywords).to.have.deep.property('sd_rtd', ['111111', '222222', '333333']); + expect(reqBidsConfigObj.adUnits[0].bids[1].ortb2.site.ext.data).to.have.deep.property('sd_rtd', ['333333']); + expect(reqBidsConfigObj.adUnits[0].bids[1].ortb2.user.ext.data).to.have.deep.property('sd_rtd', ['111111', '222222']); + }); + }); +}); diff --git a/test/spec/modules/sizeMappingV2_spec.js b/test/spec/modules/sizeMappingV2_spec.js index 77e17ef360f..ab06ddc8f51 100644 --- a/test/spec/modules/sizeMappingV2_spec.js +++ b/test/spec/modules/sizeMappingV2_spec.js @@ -205,6 +205,18 @@ describe('sizeMappingV2', function () { expect(adUnits[0].code).to.equal('div-gpt-ad-1460505748561-1'); }); + it('should filter out adUnit if it does not contain the required property "bids"', function() { + let adUnits = utils.deepClone(AD_UNITS); + delete adUnits[0].mediaTypes; + // before checkAdUnitSetupHook is called, the length of the adUnits should equal '2' + expect(adUnits.length).to.equal(2); + adUnits = checkAdUnitSetupHook(adUnits); + + // after checkAdUnitSetupHook is called, the length of the adUnits should be '1' + expect(adUnits.length).to.equal(1); + expect(adUnits[0].code).to.equal('div-gpt-ad-1460505748561-1'); + }); + it('should filter out adUnit if it has declared property mediaTypes with an empty object', function () { let adUnits = utils.deepClone(AD_UNITS); adUnits[0].mediaTypes = {}; @@ -657,7 +669,7 @@ describe('sizeMappingV2', function () { }, native: {} }, - bids: [] + bids: [{bidder: 'appnexus', params: 1234}] }]; checkAdUnitSetupHook(adUnit); @@ -679,7 +691,7 @@ describe('sizeMappingV2', function () { }, native: {} }, - bids: [] + bids: [{bidder: 'appnexus', params: 1234}] }]; checkAdUnitSetupHook(adUnit); @@ -698,7 +710,7 @@ describe('sizeMappingV2', function () { mediaTypes: { native: {} }, - bids: [] + bids: [{bidder: 'appnexus', params: 1234}] }]; checkAdUnitSetupHook(adUnit); diff --git a/test/spec/modules/smaatoBidAdapter_spec.js b/test/spec/modules/smaatoBidAdapter_spec.js index 95eb36d8a0d..1bc77fc9572 100644 --- a/test/spec/modules/smaatoBidAdapter_spec.js +++ b/test/spec/modules/smaatoBidAdapter_spec.js @@ -1,6 +1,7 @@ import { spec } from 'modules/smaatoBidAdapter.js'; import * as utils from 'src/utils.js'; import {config} from 'src/config.js'; +import {createEidsArray} from 'modules/userId/eids.js'; const imageAd = { image: { @@ -41,7 +42,7 @@ const ADTYPE_IMG = 'Img'; const ADTYPE_RICHMEDIA = 'Richmedia'; const ADTYPE_VIDEO = 'Video'; -const context = { +const site = { keywords: 'power tools,drills' }; @@ -291,6 +292,67 @@ const combinedBannerAndVideoBidRequest = { bidderWinsCount: 0 }; +const inAppBidRequest = { + bidder: 'smaato', + params: { + publisherId: 'publisherId', + adspaceId: 'adspaceId', + app: { + ifa: 'aDeviceId', + geo: { + lat: 33.3, + lon: -88.8 + } + } + }, + mediaTypes: { + banner: { + sizes: [[300, 50]] + } + }, + adUnitCode: '/19968336/header-bid-tag-0', + transactionId: 'transactionId', + sizes: [[300, 50]], + bidId: 'bidId', + bidderRequestId: 'bidderRequestId', + auctionId: 'auctionId', + src: 'client', + bidRequestsCount: 1, + bidderRequestsCount: 1, + bidderWinsCount: 0 +}; + +const userIdBidRequest = { + bidder: 'smaato', + params: { + publisherId: 'publisherId', + adspaceId: 'adspaceId' + }, + mediaTypes: { + banner: { + sizes: [[300, 50]] + } + }, + adUnitCode: '/19968336/header-bid-tag-0', + transactionId: 'transactionId', + sizes: [[300, 50]], + bidId: 'bidId', + bidderRequestId: 'bidderRequestId', + auctionId: 'auctionId', + src: 'client', + bidRequestsCount: 1, + bidderRequestsCount: 1, + bidderWinsCount: 0, + userId: { + criteoId: '123456', + tdid: '89145' + }, + userIdAsEids: createEidsArray({ + criteoId: '123456', + tdid: '89145' + }) +}; + describe('smaatoBidAdapterTest', () => { describe('isBidRequestValid', () => { it('has valid params', () => { @@ -377,8 +439,8 @@ describe('smaatoBidAdapterTest', () => { it('sends fp data', () => { this.sandbox.stub(config, 'getConfig').callsFake(key => { const config = { - fpd: { - context, + ortb2: { + site, user } }; @@ -392,7 +454,11 @@ describe('smaatoBidAdapterTest', () => { expect(req_fpd.user.ext.consent).to.equal('HFIDUYFIUYIUYWIPOI87392DSU'); expect(req_fpd.site.keywords).to.eql('power tools,drills'); expect(req_fpd.site.publisher.id).to.equal('publisherId'); - }) + }); + + it('has no user ids', () => { + expect(this.req.user.ext.eids).to.not.exist; + }); }); describe('buildRequests for video imps', () => { @@ -462,6 +528,35 @@ describe('smaatoBidAdapterTest', () => { }); }); + describe('in-app requests', () => { + it('add geo and ifa info to device object', () => { + let req = JSON.parse(spec.buildRequests([inAppBidRequest], defaultBidderRequest).data); + expect(req.device.geo).to.deep.equal({'lat': 33.3, 'lon': -88.8}); + expect(req.device.ifa).to.equal('aDeviceId'); + }); + it('add only ifa to device object', () => { + let inAppBidRequestWithoutGeo = utils.deepClone(inAppBidRequest); + delete inAppBidRequestWithoutGeo.params.app.geo + let req = JSON.parse(spec.buildRequests([inAppBidRequestWithoutGeo], defaultBidderRequest).data); + + expect(req.device.geo).to.not.exist; + expect(req.device.ifa).to.equal('aDeviceId'); + }); + it('add no specific device info if param does not exist', () => { + let req = JSON.parse(spec.buildRequests([singleBannerBidRequest], defaultBidderRequest).data); + expect(req.device.geo).to.not.exist; + expect(req.device.ifa).to.not.exist; + }); + }); + + describe('user ids in requests', () => { + it('user ids are added to user.ext.eids', () => { + let req = JSON.parse(spec.buildRequests([userIdBidRequest], defaultBidderRequest).data); + expect(req.user.ext.eids).to.exist; + expect(req.user.ext.eids).to.have.length(2); + }); + }); + describe('interpretResponse', () => { it('single image reponse', () => { const bids = spec.interpretResponse(openRtbBidResponse(ADTYPE_IMG), request); @@ -501,5 +596,11 @@ describe('smaatoBidAdapterTest', () => { expect(bids[0].ttl).to.equal(400); clock.restore(); }); + it('uses net revenue flag send from server', () => { + let resp = openRtbBidResponse(ADTYPE_IMG); + resp.body.seatbid[0].bid[0].ext = {net: false}; + const bids = spec.interpretResponse(resp, request); + expect(bids[0].netRevenue).to.equal(false); + }) }); }); diff --git a/test/spec/modules/smartadserverBidAdapter_spec.js b/test/spec/modules/smartadserverBidAdapter_spec.js index 2faad47aca8..749de43b9af 100644 --- a/test/spec/modules/smartadserverBidAdapter_spec.js +++ b/test/spec/modules/smartadserverBidAdapter_spec.js @@ -71,7 +71,7 @@ describe('Smart bid adapter tests', function () { britepoolid: '1111', criteoId: '1111', digitrustid: { data: { id: 'DTID', keyv: 4, privacy: { optout: false }, producer: 'ABC', version: 2 } }, - id5id: '1111', + id5id: { uid: '1111' }, idl_env: '1111', lipbid: '1111', parrableid: 'eidVersion.encryptionKeyReference.encryptedValue', @@ -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(); @@ -425,6 +492,7 @@ describe('Smart bid adapter tests', function () { expect(bid.mediaType).to.equal('video'); expect(bid.vastUrl).to.equal('http://awesome.fake-vast.url'); expect(bid.vastXml).to.equal(''); + expect(bid.content).to.equal(''); expect(bid.width).to.equal(640); expect(bid.height).to.equal(480); expect(bid.creativeId).to.equal('zioeufg'); @@ -473,6 +541,109 @@ describe('Smart bid adapter tests', function () { }); }); + describe('Outstream video tests', function () { + afterEach(function () { + config.resetConfig(); + $$PREBID_GLOBAL$$.requestBids.removeAll(); + }); + + const OUTSTREAM_DEFAULT_PARAMS = [{ + adUnitCode: 'sas_43', + bidId: 'abcd1234', + bidder: 'smartadserver', + mediaTypes: { + video: { + context: 'outstream', + playerSize: [[800, 600]] // It seems prebid.js transforms the player size array into an array of array... + } + }, + params: { + siteId: '1234', + pageId: '5678', + formatId: '91', + target: 'test=prebid-outstream', + bidfloor: 0.430, + buId: '7579', + appName: 'Mozilla', + ckId: 43, + video: { + protocol: 7 + } + }, + requestId: 'efgh5679', + transactionId: 'zsfgzzga' + }]; + + var OUTSTREAM_BID_RESPONSE = { + body: { + cpm: 14, + width: 800, + height: 600, + creativeId: 'zioeufga', + currency: 'USD', + isNetCpm: true, + ttl: 300, + adUrl: 'http://awesome.fake-vast2.url', + ad: '', + cSyncUrl: 'http://awesome.fake2.csync.url' + } + }; + + it('Verify outstream video build request', function () { + config.setConfig({ + 'currency': { + 'adServerCurrency': 'EUR' + } + }); + const request = spec.buildRequests(OUTSTREAM_DEFAULT_PARAMS); + expect(request[0]).to.have.property('url').and.to.equal('https://prg.smartadserver.com/prebid/v1'); + expect(request[0]).to.have.property('method').and.to.equal('POST'); + const requestContent = JSON.parse(request[0].data); + expect(requestContent).to.have.property('siteid').and.to.equal('1234'); + expect(requestContent).to.have.property('pageid').and.to.equal('5678'); + expect(requestContent).to.have.property('formatid').and.to.equal('91'); + expect(requestContent).to.have.property('currencyCode').and.to.equal('EUR'); + expect(requestContent).to.have.property('bidfloor').and.to.equal(0.43); + expect(requestContent).to.have.property('targeting').and.to.equal('test=prebid-outstream'); + expect(requestContent).to.have.property('tagId').and.to.equal('sas_43'); + expect(requestContent).to.not.have.property('pageDomain'); + expect(requestContent).to.have.property('transactionId').and.to.not.equal(null).and.to.not.be.undefined; + expect(requestContent).to.have.property('buid').and.to.equal('7579'); + expect(requestContent).to.have.property('appname').and.to.equal('Mozilla'); + expect(requestContent).to.have.property('ckid').and.to.equal(43); + expect(requestContent).to.have.property('isVideo').and.to.equal(false); + expect(requestContent).to.have.property('videoData'); + expect(requestContent.videoData).to.have.property('videoProtocol').and.to.equal(7); + expect(requestContent.videoData).to.have.property('playerWidth').and.to.equal(800); + expect(requestContent.videoData).to.have.property('playerHeight').and.to.equal(600); + }); + + it('Verify outstream parse response', function () { + const request = spec.buildRequests(OUTSTREAM_DEFAULT_PARAMS); + const bids = spec.interpretResponse(OUTSTREAM_BID_RESPONSE, request[0]); + expect(bids).to.have.lengthOf(1); + const bid = bids[0]; + expect(bid.cpm).to.equal(14); + expect(bid.mediaType).to.equal('video'); + expect(bid.vastUrl).to.equal('http://awesome.fake-vast2.url'); + expect(bid.vastXml).to.equal(''); + expect(bid.content).to.equal(''); + expect(bid.width).to.equal(800); + expect(bid.height).to.equal(600); + expect(bid.creativeId).to.equal('zioeufga'); + expect(bid.currency).to.equal('USD'); + expect(bid.netRevenue).to.equal(true); + expect(bid.ttl).to.equal(300); + expect(bid.requestId).to.equal(OUTSTREAM_DEFAULT_PARAMS[0].bidId); + + expect(function () { + spec.interpretResponse(OUTSTREAM_BID_RESPONSE, { + data: 'invalid Json' + }) + }).to.not.throw(); + }); + }); + describe('External ids tests', function () { it('Verify external ids in request and ids found', function () { config.setConfig({ diff --git a/test/spec/modules/smarticoBidAdapter_spec.js b/test/spec/modules/smarticoBidAdapter_spec.js new file mode 100644 index 00000000000..e8ca5b0e127 --- /dev/null +++ b/test/spec/modules/smarticoBidAdapter_spec.js @@ -0,0 +1,118 @@ +import {expect} from 'chai'; +import {spec} from 'modules/smarticoBidAdapter.js'; +import {newBidder} from 'src/adapters/bidderFactory.js'; + +describe('smarticoBidAdapter', function () { + const adapter = newBidder(spec); + let bid = { + adUnitCode: 'adunit-code', + auctionId: '5kaj89l8-3456-2s56-c455-4g6h78jsdfgf', + bidRequestsCount: 1, + bidder: 'smartico', + bidderRequestId: '24081afs940568', + bidderRequestsCount: 1, + bidderWinsCount: 0, + bidId: '22499d052045', + mediaTypes: {banner: {sizes: [[300, 250]]}}, + params: { + token: 'FNVzUGZn9ebpIOoheh3kEJ2GQ6H6IyMH39sHXaya', + placementId: 'testPlacementId' + }, + sizes: [ + [300, 250] + ], + transactionId: '34562345-4dg7-46g7-4sg6-45gdsdj8fd56' + } + let bidderRequests = { + auctionId: 'b06c5141-fe8f-4cdf-9d7d-54415490a917', + auctionStart: 1579746300522, + bidderCode: 'myBidderCode', + bidderRequestId: '15246a574e859f', + bids: [bid], + refererInfo: { + canonicalUrl: '', + numIframes: 0, + reachedTop: true + } + } + describe('isBidRequestValid', function () { + it('should return true where required params found', function () { + expect(spec.isBidRequestValid(bid)).to.equal(true); + }); + }); + describe('buildRequests', function () { + let bidRequests = [ bid ]; + let request = spec.buildRequests(bidRequests, bidderRequests); + it('sends bid request via POST', function () { + expect(request.method).to.equal('POST'); + }); + it('must contain token', function() { + expect(request.data.bidParams[0].token).to.equal('FNVzUGZn9ebpIOoheh3kEJ2GQ6H6IyMH39sHXaya'); + }); + it('must contain auctionId', function() { + expect(request.data.auctionId).to.exist.and.to.be.a('string') + }); + it('must contain valid width and height', function() { + expect(request.data.bidParams[0]['banner-format-width']).to.exist.and.to.be.a('number') + expect(request.data.bidParams[0]['banner-format-height']).to.exist.and.to.be.a('number') + }); + }); + + describe('interpretResponse', function () { + let bidRequest = { + method: 'POST', + url: 'https://trmads.eu/preBidRequest', + bids: [bid], + data: [{ + token: 'FNVzUGZn9ebpIOoheh3kEJ2GQ6H6IyMH39sHXaya', + bidId: '22499d052045', + 'banner-format-width': 300, + 'banner-format-height': 250, + placementId: 'testPlacementId', + }] + }; + let serverResponse = [{ + bidId: '22499d052045', + id: 987654, + cpm: 10, + ttl: 30, + bannerFormatWidth: 300, + bannerFormatHeight: 250, + bannerFormatAlias: 'medium_rectangle' + }]; + let expectedResponse = [{ + requestId: bid.bidId, + cpm: 10, + width: 300, + height: 250, + creativeId: 987654, + netRevenue: false, // gross + ttl: 30, + ad: '
', - creative_id: '9874652394875', - }, - ], - header: 'header?', - }; + const mockServerResponse = () => ({ + body: [{ + callback_id: '21989fdbef550a', + cpm: 3.45455, + width: 300, + height: 250, + ad: '' + + '
', + creative_id: '9874652394875', + adomain: ['www.example.com'], + }], + header: 'header?', }); it('should correctly reorder the server response', function () { - const newResponse = spec.interpretResponse(serverResponse); + const newResponse = spec.interpretResponse(mockServerResponse()); expect(newResponse.length).to.be.equal(1); expect(newResponse[0]).to.deep.equal({ requestId: '21989fdbef550a', @@ -289,19 +392,71 @@ describe('YieldmoAdapter', function () { currency: 'USD', netRevenue: true, ttl: 300, - ad: - '
', + ad: '' + + '
', + meta: { + advertiserDomains: ['www.example.com'], + mediaType: 'banner', + }, + }); + }); + + it('should correctly reorder video bids', function () { + const response = mockServerResponse(); + const seatbid = [ + { + bid: { + adm: '', + adomain: ['www.example.com'], + crid: 'dd65c0a7536aff', + impid: '91ea8bba1', + price: 1.5, + }, + }, + ]; + const bidRequest = { + data: { + imp: [ + { + id: '91ea8bba1', + video: { + h: 250, + w: 300, + }, + }, + ], + }, + }; + + response.body.seatbid = seatbid; + + const newResponse = spec.interpretResponse(response, bidRequest); + expect(newResponse.length).to.be.equal(2); + expect(newResponse[1]).to.deep.equal({ + cpm: 1.5, + creativeId: 'dd65c0a7536aff', + currency: 'USD', + height: 250, + mediaType: 'video', + meta: { + advertiserDomains: ['www.example.com'], + mediaType: 'video', + }, + netRevenue: true, + requestId: '91ea8bba1', + ttl: 300, + vastXml: '', + width: 300, }); }); it('should not add responses if the cpm is 0 or null', function () { - serverResponse.body[0].cpm = 0; - let response = spec.interpretResponse(serverResponse); - expect(response).to.deep.equal([]); + let response = mockServerResponse(); + response.body[0].cpm = 0; + expect(spec.interpretResponse(response)).to.deep.equal([]); - serverResponse.body[0].cpm = null; - response = spec.interpretResponse(serverResponse); - expect(response).to.deep.equal([]); + response.body[0].cpm = null; + expect(spec.interpretResponse(response)).to.deep.equal([]); }); }); diff --git a/test/spec/modules/yuktamediaAnalyticsAdapter_spec.js b/test/spec/modules/yuktamediaAnalyticsAdapter_spec.js index 24781d749e0..e8eb4ab73be 100644 --- a/test/spec/modules/yuktamediaAnalyticsAdapter_spec.js +++ b/test/spec/modules/yuktamediaAnalyticsAdapter_spec.js @@ -30,7 +30,7 @@ let prebidAuction = { } }, 'userId': { - 'id5id': 'ID5-ZHMOxXeRXPe3inZKGD-Lj0g7y8UWdDbsYXQ_n6aWMQ', + 'id5id': { uid: 'ID5-ZHMOxXeRXPe3inZKGD-Lj0g7y8UWdDbsYXQ_n6aWMQ' }, 'parrableid': '01.1595590997.46d951017bdc272ca50b88dbcfb0545cfc636bec3e3d8c02091fb1b413328fb2fd3baf65cb4114b3f782895fd09f82f02c9042b85b42c4654d08ba06dc77f0ded936c8ea3fc4085b4a99', 'pubcid': '100a8bc9-f588-4c22-873e-a721cb68bc34' }, @@ -391,7 +391,7 @@ describe('yuktamedia analytics adapter', function () { yuktamediaAnalyticsAdapter.track.restore(); }); - it('should catch all events', function () { + it('should catch all events 1', function () { yuktamediaAnalyticsAdapter.enableAnalytics({ provider: 'yuktamedia', options: { @@ -403,12 +403,82 @@ describe('yuktamedia analytics adapter', function () { } }); events.emit(constants.EVENTS.AUCTION_INIT, prebidAuction[constants.EVENTS.AUCTION_INIT]); + sinon.assert.called(yuktamediaAnalyticsAdapter.track); + }); + + it('should catch all events 2', function () { + yuktamediaAnalyticsAdapter.enableAnalytics({ + provider: 'yuktamedia', + options: { + pubId: '1', + pubKey: 'ZXlKaGJHY2lPaUpJVXpJMU5pSjkuT==', + enableUTMCollection: true, + enableSession: true, + enableUserIdCollection: true + } + }); events.emit(constants.EVENTS.BID_REQUESTED, prebidAuction[constants.EVENTS.BID_REQUESTED]); + sinon.assert.called(yuktamediaAnalyticsAdapter.track); + }); + + it('should catch all events 3', function () { + yuktamediaAnalyticsAdapter.enableAnalytics({ + provider: 'yuktamedia', + options: { + pubId: '1', + pubKey: 'ZXlKaGJHY2lPaUpJVXpJMU5pSjkuT==', + enableUTMCollection: true, + enableSession: true, + enableUserIdCollection: true + } + }); events.emit(constants.EVENTS.NO_BID, prebidAuction[constants.EVENTS.NO_BID]); + sinon.assert.called(yuktamediaAnalyticsAdapter.track); + }); + + it('should catch all events 4', function () { + yuktamediaAnalyticsAdapter.enableAnalytics({ + provider: 'yuktamedia', + options: { + pubId: '1', + pubKey: 'ZXlKaGJHY2lPaUpJVXpJMU5pSjkuT==', + enableUTMCollection: true, + enableSession: true, + enableUserIdCollection: true + } + }); events.emit(constants.EVENTS.BID_TIMEOUT, prebidAuction[constants.EVENTS.BID_TIMEOUT]); + sinon.assert.called(yuktamediaAnalyticsAdapter.track); + }); + + it('should catch all events 5', function () { + yuktamediaAnalyticsAdapter.enableAnalytics({ + provider: 'yuktamedia', + options: { + pubId: '1', + pubKey: 'ZXlKaGJHY2lPaUpJVXpJMU5pSjkuT==', + enableUTMCollection: true, + enableSession: true, + enableUserIdCollection: true + } + }); events.emit(constants.EVENTS.BID_RESPONSE, prebidAuction[constants.EVENTS.BID_RESPONSE]); + sinon.assert.called(yuktamediaAnalyticsAdapter.track); + }); + + it('should catch all events 6', function () { + yuktamediaAnalyticsAdapter.enableAnalytics({ + provider: 'yuktamedia', + options: { + pubId: '1', + pubKey: 'ZXlKaGJHY2lPaUpJVXpJMU5pSjkuT==', + enableUTMCollection: true, + enableSession: true, + enableUserIdCollection: true + } + }); events.emit(constants.EVENTS.AUCTION_END, prebidAuction[constants.EVENTS.AUCTION_END]); - sinon.assert.callCount(yuktamediaAnalyticsAdapter.track, 6); + sinon.assert.called(yuktamediaAnalyticsAdapter.track); }); it('should catch no events if no pubKey and pubId', function () { @@ -427,7 +497,7 @@ describe('yuktamedia analytics adapter', function () { sinon.assert.callCount(yuktamediaAnalyticsAdapter.track, 0); }); - it('should catch nobid, timeout and biwon event events', function () { + it('should catch nobid, timeout and bidwon event events one of eight', function () { yuktamediaAnalyticsAdapter.enableAnalytics({ provider: 'yuktamedia', options: { @@ -439,14 +509,112 @@ describe('yuktamedia analytics adapter', function () { } }); events.emit(constants.EVENTS.AUCTION_INIT, prebidNativeAuction[constants.EVENTS.AUCTION_INIT]); + sinon.assert.called(yuktamediaAnalyticsAdapter.track); + }); + + it('should catch nobid, timeout and bidwon event events two of eight', function () { + yuktamediaAnalyticsAdapter.enableAnalytics({ + provider: 'yuktamedia', + options: { + pubId: '1', + pubKey: 'ZXlKaGJHY2lPaUpJVXpJMU5pSjkuT==', + enableUTMCollection: true, + enableSession: true, + enableUserIdCollection: true + } + }); events.emit(constants.EVENTS.BID_REQUESTED, prebidNativeAuction[constants.EVENTS.BID_REQUESTED]); + sinon.assert.called(yuktamediaAnalyticsAdapter.track); + }); + + it('should catch nobid, timeout and bidwon event events three of eight', function () { + yuktamediaAnalyticsAdapter.enableAnalytics({ + provider: 'yuktamedia', + options: { + pubId: '1', + pubKey: 'ZXlKaGJHY2lPaUpJVXpJMU5pSjkuT==', + enableUTMCollection: true, + enableSession: true, + enableUserIdCollection: true + } + }); events.emit(constants.EVENTS.BID_REQUESTED, prebidNativeAuction[constants.EVENTS.BID_REQUESTED + '1']); + sinon.assert.called(yuktamediaAnalyticsAdapter.track); + }); + + it('should catch nobid, timeout and bidwon event events four of eight', function () { + yuktamediaAnalyticsAdapter.enableAnalytics({ + provider: 'yuktamedia', + options: { + pubId: '1', + pubKey: 'ZXlKaGJHY2lPaUpJVXpJMU5pSjkuT==', + enableUTMCollection: true, + enableSession: true, + enableUserIdCollection: true + } + }); events.emit(constants.EVENTS.NO_BID, prebidNativeAuction[constants.EVENTS.NO_BID]); + sinon.assert.called(yuktamediaAnalyticsAdapter.track); + }); + + it('should catch nobid, timeout and bidwon event events five of eight', function () { + yuktamediaAnalyticsAdapter.enableAnalytics({ + provider: 'yuktamedia', + options: { + pubId: '1', + pubKey: 'ZXlKaGJHY2lPaUpJVXpJMU5pSjkuT==', + enableUTMCollection: true, + enableSession: true, + enableUserIdCollection: true + } + }); events.emit(constants.EVENTS.BID_TIMEOUT, prebidNativeAuction[constants.EVENTS.BID_TIMEOUT]); + sinon.assert.called(yuktamediaAnalyticsAdapter.track); + }); + + it('should catch nobid, timeout and bidwon event events six of eight', function () { + yuktamediaAnalyticsAdapter.enableAnalytics({ + provider: 'yuktamedia', + options: { + pubId: '1', + pubKey: 'ZXlKaGJHY2lPaUpJVXpJMU5pSjkuT==', + enableUTMCollection: true, + enableSession: true, + enableUserIdCollection: true + } + }); events.emit(constants.EVENTS.BID_RESPONSE, prebidNativeAuction[constants.EVENTS.BID_RESPONSE]); + sinon.assert.called(yuktamediaAnalyticsAdapter.track); + }); + + it('should catch nobid, timeout and bidwon event events seven of eight', function () { + yuktamediaAnalyticsAdapter.enableAnalytics({ + provider: 'yuktamedia', + options: { + pubId: '1', + pubKey: 'ZXlKaGJHY2lPaUpJVXpJMU5pSjkuT==', + enableUTMCollection: true, + enableSession: true, + enableUserIdCollection: true + } + }); events.emit(constants.EVENTS.AUCTION_END, prebidNativeAuction[constants.EVENTS.AUCTION_END]); + sinon.assert.called(yuktamediaAnalyticsAdapter.track); + }); + + it('should catch nobid, timeout and bidwon event events eight of eight', function () { + yuktamediaAnalyticsAdapter.enableAnalytics({ + provider: 'yuktamedia', + options: { + pubId: '1', + pubKey: 'ZXlKaGJHY2lPaUpJVXpJMU5pSjkuT==', + enableUTMCollection: true, + enableSession: true, + enableUserIdCollection: true + } + }); events.emit(constants.EVENTS.AUCTION_END, prebidNativeAuction[constants.EVENTS.BID_WON]); - sinon.assert.callCount(yuktamediaAnalyticsAdapter.track, 8); + sinon.assert.called(yuktamediaAnalyticsAdapter.track); }); }); diff --git a/test/spec/modules/zemantaBidAdapter_spec.js b/test/spec/modules/zemantaBidAdapter_spec.js new file mode 100644 index 00000000000..523cdcd2eb3 --- /dev/null +++ b/test/spec/modules/zemantaBidAdapter_spec.js @@ -0,0 +1,558 @@ +import {expect} from 'chai'; +import {spec} from 'modules/zemantaBidAdapter.js'; +import {config} from 'src/config.js'; +import {server} from 'test/mocks/xhr'; + +describe('Zemanta Adapter', function () { + describe('Bid request and response', function () { + const commonBidRequest = { + bidder: 'zemanta', + params: { + publisher: { + id: 'publisher-id' + }, + }, + bidId: '2d6815a92ba1ba', + auctionId: '12043683-3254-4f74-8934-f941b085579e', + } + const nativeBidRequestParams = { + nativeParams: { + image: { + required: true, + sizes: [ + 120, + 100 + ], + sendId: true + }, + title: { + required: true, + sendId: true + }, + sponsoredBy: { + required: false + } + }, + } + + const displayBidRequestParams = { + sizes: [ + [ + 300, + 250 + ] + ] + } + + describe('isBidRequestValid', function () { + before(() => { + config.setConfig({ + zemanta: { + bidderUrl: 'https://bidder-url.com', + } + } + ) + }) + after(() => { + config.resetConfig() + }) + + it('should fail when bid is invalid', function () { + const bid = { + bidder: 'zemanta', + params: { + publisher: { + id: 'publisher-id', + } + }, + } + expect(spec.isBidRequestValid(bid)).to.equal(false) + }) + it('should succeed when bid contains native params', function () { + const bid = { + bidder: 'zemanta', + params: { + publisher: { + id: 'publisher-id', + } + }, + ...nativeBidRequestParams, + } + expect(spec.isBidRequestValid(bid)).to.equal(true) + }) + it('should succeed when bid contains sizes', function () { + const bid = { + bidder: 'zemanta', + params: { + publisher: { + id: 'publisher-id', + } + }, + ...displayBidRequestParams, + } + expect(spec.isBidRequestValid(bid)).to.equal(true) + }) + it('should fail if publisher id is not set', function () { + const bid = { + bidder: 'zemanta', + ...nativeBidRequestParams, + } + expect(spec.isBidRequestValid(bid)).to.equal(false) + }) + it('should succeed with outbrain config', function () { + const bid = { + bidder: 'zemanta', + params: { + publisher: { + id: 'publisher-id', + } + }, + ...nativeBidRequestParams, + } + config.resetConfig() + config.setConfig({ + outbrain: { + bidderUrl: 'https://bidder-url.com', + } + }) + expect(spec.isBidRequestValid(bid)).to.equal(true) + }) + it('should fail if bidder url is not set', function () { + const bid = { + bidder: 'zemanta', + params: { + publisher: { + id: 'publisher-id', + } + }, + ...nativeBidRequestParams, + } + config.resetConfig() + expect(spec.isBidRequestValid(bid)).to.equal(false) + }) + }) + + describe('buildRequests', function () { + before(() => { + config.setConfig({ + zemanta: { + bidderUrl: 'https://bidder-url.com', + } + } + ) + }) + after(() => { + config.resetConfig() + }) + + const commonBidderRequest = { + refererInfo: { + referer: 'https://example.com/' + } + } + + it('should build native request', function () { + const bidRequest = { + ...commonBidRequest, + ...nativeBidRequestParams, + } + const expectedNativeAssets = { + assets: [ + { + required: 1, + id: 3, + img: { + type: 3, + w: 120, + h: 100 + } + }, + { + required: 1, + id: 0, + title: {} + }, + { + required: 0, + id: 5, + data: { + type: 1 + } + } + ] + } + const expectedData = { + site: { + page: 'https://example.com/', + publisher: { + id: 'publisher-id' + } + }, + device: { + ua: navigator.userAgent + }, + source: { + fd: 1 + }, + cur: [ + 'USD' + ], + imp: [ + { + id: '1', + native: { + request: JSON.stringify(expectedNativeAssets) + } + } + ] + } + const res = spec.buildRequests([bidRequest], commonBidderRequest) + expect(res.url).to.equal('https://bidder-url.com') + expect(res.data).to.deep.equal(JSON.stringify(expectedData)) + }); + + it('should build display request', function () { + const bidRequest = { + ...commonBidRequest, + ...displayBidRequestParams, + } + const expectedData = { + site: { + page: 'https://example.com/', + publisher: { + id: 'publisher-id' + } + }, + device: { + ua: navigator.userAgent + }, + source: { + fd: 1 + }, + cur: [ + 'USD' + ], + imp: [ + { + id: '1', + banner: { + format: [ + { + w: 300, + h: 250 + } + ] + } + } + ] + } + const res = spec.buildRequests([bidRequest], commonBidderRequest) + expect(res.url).to.equal('https://bidder-url.com') + expect(res.data).to.deep.equal(JSON.stringify(expectedData)) + }) + + it('should pass optional parameters in request', function () { + const bidRequest = { + ...commonBidRequest, + ...nativeBidRequestParams, + } + bidRequest.params.tagid = 'test-tag' + bidRequest.params.publisher.name = 'test-publisher' + bidRequest.params.publisher.domain = 'test-publisher.com' + bidRequest.params.bcat = ['bad-category'] + bidRequest.params.badv = ['bad-advertiser'] + + const res = spec.buildRequests([bidRequest], commonBidderRequest) + const resData = JSON.parse(res.data) + expect(resData.imp[0].tagid).to.equal('test-tag') + expect(resData.site.publisher.name).to.equal('test-publisher') + expect(resData.site.publisher.domain).to.equal('test-publisher.com') + expect(resData.bcat).to.deep.equal(['bad-category']) + expect(resData.badv).to.deep.equal(['bad-advertiser']) + }); + + it('should pass bidder timeout', function () { + const bidRequest = { + ...commonBidRequest, + ...nativeBidRequestParams, + } + const bidderRequest = { + ...commonBidderRequest, + timeout: 500 + } + const res = spec.buildRequests([bidRequest], bidderRequest) + const resData = JSON.parse(res.data) + expect(resData.tmax).to.equal(500) + }); + + it('should pass GDPR consent', function () { + const bidRequest = { + ...commonBidRequest, + ...nativeBidRequestParams, + } + const bidderRequest = { + ...commonBidderRequest, + gdprConsent: { + gdprApplies: true, + consentString: 'consentString', + } + } + const res = spec.buildRequests([bidRequest], bidderRequest) + const resData = JSON.parse(res.data) + expect(resData.user.ext.consent).to.equal('consentString') + expect(resData.regs.ext.gdpr).to.equal(1) + }); + + it('should pass us privacy consent', function () { + const bidRequest = { + ...commonBidRequest, + ...nativeBidRequestParams, + } + const bidderRequest = { + ...commonBidderRequest, + uspConsent: 'consentString' + } + const res = spec.buildRequests([bidRequest], bidderRequest) + const resData = JSON.parse(res.data) + expect(resData.regs.ext.us_privacy).to.equal('consentString') + }); + + it('should pass coppa consent', function () { + const bidRequest = { + ...commonBidRequest, + ...nativeBidRequestParams, + } + config.setConfig({coppa: true}) + + const res = spec.buildRequests([bidRequest], commonBidderRequest) + const resData = JSON.parse(res.data) + expect(resData.regs.coppa).to.equal(1) + + config.resetConfig() + }); + }) + + describe('interpretResponse', function () { + it('should return empty array if no valid bids', function () { + const res = spec.interpretResponse({}, []) + expect(res).to.be.an('array').that.is.empty + }); + + it('should interpret native response', function () { + const serverResponse = { + body: { + id: '0a73e68c-9967-4391-b01b-dda2d9fc54e4', + seatbid: [ + { + bid: [ + { + id: '82822cf5-259c-11eb-8a52-f29e5275aa57', + impid: '1', + price: 1.1, + nurl: 'http://example.com/win/${AUCTION_PRICE}', + adm: '{"ver":"1.2","assets":[{"id":3,"required":1,"img":{"url":"http://example.com/img/url","w":120,"h":100}},{"id":0,"required":1,"title":{"text":"Test title"}},{"id":5,"data":{"value":"Test sponsor"}}],"link":{"url":"http://example.com/click/url"},"eventtrackers":[{"event":1,"method":1,"url":"http://example.com/impression"}]}', + adomain: [ + 'example.com' + ], + cid: '3487171', + crid: '28023739', + cat: [ + 'IAB10-2' + ] + } + ], + seat: 'acc-5537' + } + ], + bidid: '82822cf5-259c-11eb-8a52-b48e7518c657', + cur: 'USD' + }, + } + const request = { + bids: [ + { + ...commonBidRequest, + ...nativeBidRequestParams, + } + ] + } + const expectedRes = [ + { + requestId: request.bids[0].bidId, + cpm: 1.1, + creativeId: '28023739', + ttl: 360, + netRevenue: false, + currency: 'USD', + mediaType: 'native', + nurl: 'http://example.com/win/${AUCTION_PRICE}', + meta: { + 'advertiserDomains': [ + 'example.com' + ] + }, + native: { + clickTrackers: undefined, + clickUrl: 'http://example.com/click/url', + image: { + url: 'http://example.com/img/url', + width: 120, + height: 100 + }, + title: 'Test title', + sponsoredBy: 'Test sponsor', + impressionTrackers: [ + 'http://example.com/impression', + ] + } + } + ] + + const res = spec.interpretResponse(serverResponse, request) + expect(res).to.deep.equal(expectedRes) + }); + + it('should interpret display response', function () { + const serverResponse = { + body: { + id: '6b2eedc8-8ff5-46ef-adcf-e701b508943e', + seatbid: [ + { + bid: [ + { + id: 'd90fe7fa-28d7-11eb-8ce4-462a842a7cf9', + impid: '1', + price: 1.1, + nurl: 'http://example.com/win/${AUCTION_PRICE}', + adm: '
ad
', + adomain: [ + 'example.com' + ], + cid: '3865084', + crid: '29998660', + cat: [ + 'IAB10-2' + ], + w: 300, + h: 250 + } + ], + seat: 'acc-6536' + } + ], + bidid: 'd90fe7fa-28d7-11eb-8ce4-13d94bfa26f9', + cur: 'USD' + } + } + const request = { + bids: [ + { + ...commonBidRequest, + ...displayBidRequestParams + } + ] + } + const expectedRes = [ + { + requestId: request.bids[0].bidId, + cpm: 1.1, + creativeId: '29998660', + ttl: 360, + netRevenue: false, + currency: 'USD', + mediaType: 'banner', + nurl: 'http://example.com/win/${AUCTION_PRICE}', + ad: '
ad
', + width: 300, + height: 250, + meta: { + 'advertiserDomains': [ + 'example.com' + ] + }, + } + ] + + const res = spec.interpretResponse(serverResponse, request) + expect(res).to.deep.equal(expectedRes) + }); + }) + }) + + describe('getUserSyncs', function () { + const usersyncUrl = 'https://usersync-url.com'; + beforeEach(() => { + config.setConfig({ + zemanta: { + usersyncUrl: usersyncUrl, + } + } + ) + }) + after(() => { + config.resetConfig() + }) + + it('should return user sync if pixel enabled', function () { + const ret = spec.getUserSyncs({pixelEnabled: true}) + expect(ret).to.deep.equal([{type: 'image', url: 'https://usersync-url.com'}]) + }) + it('should return user sync if pixel enabled with outbrain config', function () { + config.resetConfig() + config.setConfig({ + outbrain: { + usersyncUrl: 'https://usersync-url.com', + } + }) + const ret = spec.getUserSyncs({pixelEnabled: true}) + expect(ret).to.deep.equal([{type: 'image', url: 'https://usersync-url.com'}]) + }) + + it('should not return user sync if pixel disabled', function () { + const ret = spec.getUserSyncs({pixelEnabled: false}) + expect(ret).to.be.an('array').that.is.empty + }) + + it('should not return user sync if url is not set', function () { + config.resetConfig() + const ret = spec.getUserSyncs({pixelEnabled: true}) + expect(ret).to.be.an('array').that.is.empty + }) + + it('should pass GDPR consent', function() { + expect(spec.getUserSyncs({ pixelEnabled: true }, {}, {gdprApplies: true, consentString: 'foo'}, undefined)).to.deep.equal([{ + type: 'image', url: `${usersyncUrl}&gdpr=1&gdpr_consent=foo` + }]); + expect(spec.getUserSyncs({ pixelEnabled: true }, {}, {gdprApplies: false, consentString: 'foo'}, undefined)).to.deep.equal([{ + type: 'image', url: `${usersyncUrl}&gdpr=0&gdpr_consent=foo` + }]); + expect(spec.getUserSyncs({ pixelEnabled: true }, {}, {gdprApplies: true, consentString: undefined}, undefined)).to.deep.equal([{ + type: 'image', url: `${usersyncUrl}&gdpr=1&gdpr_consent=` + }]); + }); + + it('should pass US consent', function() { + expect(spec.getUserSyncs({ pixelEnabled: true }, {}, undefined, '1NYN')).to.deep.equal([{ + type: 'image', url: `${usersyncUrl}&us_privacy=1NYN` + }]); + }); + + it('should pass GDPR and US consent', function() { + expect(spec.getUserSyncs({ pixelEnabled: true }, {}, {gdprApplies: true, consentString: 'foo'}, '1NYN')).to.deep.equal([{ + type: 'image', url: `${usersyncUrl}&gdpr=1&gdpr_consent=foo&us_privacy=1NYN` + }]); + }); + }) + + describe('onBidWon', function () { + it('should make an ajax call with the original cpm', function () { + const bid = { + nurl: 'http://example.com/win/${AUCTION_PRICE}', + cpm: 2.1, + originalCpm: 1.1, + } + spec.onBidWon(bid) + expect(server.requests[0].url).to.equals('http://example.com/win/1.1') + }); + }) +}) diff --git a/test/spec/modules/zeotapIdPlusIdSystem_spec.js b/test/spec/modules/zeotapIdPlusIdSystem_spec.js new file mode 100644 index 00000000000..9de6fa843bc --- /dev/null +++ b/test/spec/modules/zeotapIdPlusIdSystem_spec.js @@ -0,0 +1,195 @@ +import { expect } from 'chai'; +import find from 'core-js-pure/features/array/find.js'; +import { config } from 'src/config.js'; +import { init, requestBidsHook, setSubmoduleRegistry } from 'modules/userId/index.js'; +import { storage, getStorage, zeotapIdPlusSubmodule } from 'modules/zeotapIdPlusIdSystem.js'; +import * as storageManager from 'src/storageManager.js'; + +const ZEOTAP_COOKIE_NAME = 'IDP'; +const ZEOTAP_COOKIE = 'THIS-IS-A-DUMMY-COOKIE'; +const ENCODED_ZEOTAP_COOKIE = btoa(JSON.stringify(ZEOTAP_COOKIE)); + +function getConfigMock() { + return { + userSync: { + syncDelay: 0, + userIds: [{ + name: 'zeotapIdPlus' + }] + } + } +} + +function getAdUnitMock(code = 'adUnit-code') { + return { + code, + mediaTypes: {banner: {}, native: {}}, + sizes: [ + [300, 200], + [300, 600] + ], + bids: [{ + bidder: 'sampleBidder', + params: { placementId: 'banner-only-bidder' } + }] + }; +} + +function unsetCookie() { + storage.setCookie(ZEOTAP_COOKIE_NAME, ''); +} + +function unsetLocalStorage() { + storage.setDataInLocalStorage(ZEOTAP_COOKIE_NAME, ''); +} + +describe('Zeotap ID System', function() { + describe('Zeotap Module invokes StorageManager with appropriate arguments', function() { + let getStorageManagerSpy; + + beforeEach(function() { + getStorageManagerSpy = sinon.spy(storageManager, 'getStorageManager'); + }); + + it('when a stored Zeotap ID exists it is added to bids', function() { + let store = getStorage(); + expect(getStorageManagerSpy.calledOnce).to.be.true; + sinon.assert.calledWith(getStorageManagerSpy, 301, 'zeotapIdPlus'); + }); + }); + + describe('test method: getId calls storage methods to fetch ID', function() { + let cookiesAreEnabledStub; + let getCookieStub; + let localStorageIsEnabledStub; + let getDataFromLocalStorageStub; + + beforeEach(() => { + cookiesAreEnabledStub = sinon.stub(storage, 'cookiesAreEnabled'); + getCookieStub = sinon.stub(storage, 'getCookie'); + localStorageIsEnabledStub = sinon.stub(storage, 'localStorageIsEnabled'); + getDataFromLocalStorageStub = sinon.stub(storage, 'getDataFromLocalStorage'); + }); + + afterEach(() => { + storage.cookiesAreEnabled.restore(); + storage.getCookie.restore(); + storage.localStorageIsEnabled.restore(); + storage.getDataFromLocalStorage.restore(); + unsetCookie(); + unsetLocalStorage(); + }); + + it('should check if cookies are enabled', function() { + let id = zeotapIdPlusSubmodule.getId(); + expect(cookiesAreEnabledStub.calledOnce).to.be.true; + }); + + it('should call getCookie if cookies are enabled', function() { + cookiesAreEnabledStub.returns(true); + let id = zeotapIdPlusSubmodule.getId(); + expect(cookiesAreEnabledStub.calledOnce).to.be.true; + expect(getCookieStub.calledOnce).to.be.true; + sinon.assert.calledWith(getCookieStub, 'IDP'); + }); + + it('should check for localStorage if cookies are disabled', function() { + cookiesAreEnabledStub.returns(false); + localStorageIsEnabledStub.returns(true) + let id = zeotapIdPlusSubmodule.getId(); + expect(cookiesAreEnabledStub.calledOnce).to.be.true; + expect(getCookieStub.called).to.be.false; + expect(localStorageIsEnabledStub.calledOnce).to.be.true; + expect(getDataFromLocalStorageStub.calledOnce).to.be.true; + sinon.assert.calledWith(getDataFromLocalStorageStub, 'IDP'); + }); + }); + + describe('test method: getId', function() { + afterEach(() => { + unsetCookie(); + unsetLocalStorage(); + }); + + it('provides the stored Zeotap id if a cookie exists', function() { + storage.setCookie(ZEOTAP_COOKIE_NAME, ENCODED_ZEOTAP_COOKIE); + let id = zeotapIdPlusSubmodule.getId(); + expect(id).to.deep.equal({ + id: ENCODED_ZEOTAP_COOKIE + }); + }); + + it('provides the stored Zeotap id if cookie is absent but present in local storage', function() { + storage.setDataInLocalStorage(ZEOTAP_COOKIE_NAME, ENCODED_ZEOTAP_COOKIE); + let id = zeotapIdPlusSubmodule.getId(); + expect(id).to.deep.equal({ + id: ENCODED_ZEOTAP_COOKIE + }); + }); + + it('returns undefined if both cookie and local storage are empty', function() { + let id = zeotapIdPlusSubmodule.getId(); + expect(id).to.be.undefined + }) + }); + + describe('test method: decode', function() { + it('provides the Zeotap ID (IDP) from a stored object', function() { + let zeotapId = { + id: ENCODED_ZEOTAP_COOKIE, + }; + + expect(zeotapIdPlusSubmodule.decode(zeotapId)).to.deep.equal({ + IDP: ZEOTAP_COOKIE + }); + }); + + it('provides the Zeotap ID (IDP) from a stored string', function() { + let zeotapId = ENCODED_ZEOTAP_COOKIE; + + expect(zeotapIdPlusSubmodule.decode(zeotapId)).to.deep.equal({ + IDP: ZEOTAP_COOKIE + }); + }); + }); + + describe('requestBids hook', function() { + let adUnits; + + beforeEach(function() { + adUnits = [getAdUnitMock()]; + storage.setCookie( + ZEOTAP_COOKIE_NAME, + ENCODED_ZEOTAP_COOKIE + ); + setSubmoduleRegistry([zeotapIdPlusSubmodule]); + init(config); + config.setConfig(getConfigMock()); + }); + + afterEach(function() { + unsetCookie(); + unsetLocalStorage(); + }); + + it('when a stored Zeotap ID exists it is added to bids', function(done) { + requestBidsHook(function() { + adUnits.forEach(unit => { + unit.bids.forEach(bid => { + expect(bid).to.have.deep.nested.property('userId.IDP'); + expect(bid.userId.IDP).to.equal(ZEOTAP_COOKIE); + const zeotapIdAsEid = find(bid.userIdAsEids, e => e.source == 'zeotap.com'); + expect(zeotapIdAsEid).to.deep.equal({ + source: 'zeotap.com', + uids: [{ + id: ZEOTAP_COOKIE, + atype: 1, + }] + }); + }); + }); + done(); + }, { adUnits }); + }); + }); +}); diff --git a/test/spec/modules/zetaBidAdapter_spec.js b/test/spec/modules/zetaBidAdapter_spec.js new file mode 100644 index 00000000000..ccdd5f43cb0 --- /dev/null +++ b/test/spec/modules/zetaBidAdapter_spec.js @@ -0,0 +1,84 @@ +import { spec } from '../../../modules/zetaBidAdapter.js' + +describe('Zeta Bid Adapter', function() { + const bannerRequest = [{ + bidId: 12345, + auctionId: 67890, + mediaTypes: { + banner: { + sizes: [[300, 250]], + } + }, + refererInfo: { + referer: 'testprebid.com' + }, + params: { + placement: 12345, + user: { + uid: 12345, + buyeruid: 12345 + }, + device: { + ip: '111.222.33.44', + geo: { + country: 'USA' + } + }, + definerId: '0', + test: 1 + } + }]; + + it('Test the bid validation function', function() { + const validBid = spec.isBidRequestValid(bannerRequest[0]); + const invalidBid = spec.isBidRequestValid(null); + + expect(validBid).to.be.true; + expect(invalidBid).to.be.false; + }); + + it('Test the request processing function', function () { + const request = spec.buildRequests(bannerRequest, bannerRequest[0]); + expect(request).to.not.be.empty; + + const payload = request.data; + expect(payload).to.not.be.empty; + }); + + const responseBody = { + id: '12345', + seatbid: [ + { + bid: [ + { + id: 'auctionId', + impid: 'impId', + price: 0.0, + adm: 'adMarkup', + crid: 'creativeId', + h: 250, + w: 300 + } + ] + } + ], + cur: 'USD' + }; + + it('Test the response parsing function', function () { + const receivedBid = responseBody.seatbid[0].bid[0]; + const response = {}; + response.body = responseBody; + + const bidResponse = spec.interpretResponse(response, null); + expect(bidResponse).to.not.be.empty; + + const bid = bidResponse[0]; + expect(bid).to.not.be.empty; + expect(bid.ad).to.equal(receivedBid.adm); + expect(bid.cpm).to.equal(receivedBid.price); + expect(bid.height).to.equal(receivedBid.h); + expect(bid.width).to.equal(receivedBid.w); + expect(bid.requestId).to.equal(receivedBid.impid); + }); +}); diff --git a/test/spec/modules/zetaSspBidAdapter_spec.js b/test/spec/modules/zetaSspBidAdapter_spec.js new file mode 100644 index 00000000000..bdfc64c3234 --- /dev/null +++ b/test/spec/modules/zetaSspBidAdapter_spec.js @@ -0,0 +1,85 @@ +import { spec } from '../../../modules/zetaSspBidAdapter.js' + +describe('Zeta Ssp Bid Adapter', function() { + const bannerRequest = [{ + bidId: 12345, + auctionId: 67890, + mediaTypes: { + banner: { + sizes: [[300, 250]], + } + }, + refererInfo: { + referer: 'zetaglobal.com' + }, + params: { + placement: 12345, + user: { + uid: 12345, + buyeruid: 12345 + }, + tags: { + someTag: 123, + sid: 'publisherId' + }, + test: 1 + } + }]; + + it('Test the bid validation function', function() { + const validBid = spec.isBidRequestValid(bannerRequest[0]); + const invalidBid = spec.isBidRequestValid(null); + + expect(validBid).to.be.true; + expect(invalidBid).to.be.false; + }); + + it('Test the request processing function', function () { + const request = spec.buildRequests(bannerRequest, bannerRequest[0]); + expect(request).to.not.be.empty; + + const payload = request.data; + expect(payload).to.not.be.empty; + }); + + const responseBody = { + id: '12345', + seatbid: [ + { + bid: [ + { + id: 'auctionId', + impid: 'impId', + price: 0.0, + adm: 'adMarkup', + crid: 'creativeId', + adomain: [ + 'https://example.com' + ], + h: 250, + w: 300 + } + ] + } + ], + cur: 'USD' + }; + + it('Test the response parsing function', function () { + const receivedBid = responseBody.seatbid[0].bid[0]; + const response = {}; + response.body = responseBody; + + const bidResponse = spec.interpretResponse(response, null); + expect(bidResponse).to.not.be.empty; + + const bid = bidResponse[0]; + expect(bid).to.not.be.empty; + expect(bid.ad).to.equal(receivedBid.adm); + expect(bid.cpm).to.equal(receivedBid.price); + expect(bid.height).to.equal(receivedBid.h); + expect(bid.width).to.equal(receivedBid.w); + expect(bid.requestId).to.equal(receivedBid.impid); + expect(bid.meta.advertiserDomains).to.equal(receivedBid.adomain); + }); +}); diff --git a/test/spec/native_spec.js b/test/spec/native_spec.js index bd56ba53e4a..0ffef30965b 100644 --- a/test/spec/native_spec.js +++ b/test/spec/native_spec.js @@ -1,5 +1,5 @@ import { expect } from 'chai'; -import { fireNativeTrackers, getNativeTargeting, nativeBidIsValid, getAssetMessage } from 'src/native.js'; +import { fireNativeTrackers, getNativeTargeting, nativeBidIsValid, getAssetMessage, getAllAssetsMessage } from 'src/native.js'; import CONSTANTS from 'src/constants.json'; const utils = require('src/utils'); @@ -23,7 +23,11 @@ const bid = { clickUrl: 'https://www.link.example', clickTrackers: ['https://tracker.example'], impressionTrackers: ['https://impression.example'], - javascriptTrackers: '' + 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 () { @@ -187,11 +433,7 @@ describe('validate native', function () { native: { body: 'This is a Prebid Native Creative. There are many like it, but this one is mine.', clickTrackers: ['http://my.click.tracker/url'], - icon: { - url: 'http://my.image.file/ad_image.jpg', - height: 0, - width: 0 - }, + icon: 'http://my.image.file/ad_image.jpg', image: { url: 'http://my.icon.file/ad_icon.jpg', height: 2250, @@ -217,11 +459,7 @@ describe('validate native', function () { height: 75, width: 75 }, - image: { - url: 'http://my.icon.file/ad_icon.jpg', - height: 0, - width: 0 - }, + image: 'http://my.icon.file/ad_icon.jpg', clickUrl: 'http://prebid.org/dev-docs/show-native-ads.html', impressionTrackers: ['http://my.imp.tracker/url'], javascriptTrackers: '', @@ -233,12 +471,12 @@ describe('validate native', function () { afterEach(function () {}); - it('should reject bid if no image sizes are defined', function () { + it('should accept bid if no image sizes are defined', function () { let result = nativeBidIsValid(validBid, bidReq); expect(result).to.be.true; result = nativeBidIsValid(noIconDimBid, bidReq); - expect(result).to.be.false; + expect(result).to.be.true; result = nativeBidIsValid(noImgDimBid, bidReq); - expect(result).to.be.false; + expect(result).to.be.true; }); }); diff --git a/test/spec/refererDetection_spec.js b/test/spec/refererDetection_spec.js index 90892d915fe..46990ae841f 100644 --- a/test/spec/refererDetection_spec.js +++ b/test/spec/refererDetection_spec.js @@ -1,87 +1,357 @@ import { detectReferer } from 'src/refererDetection.js'; import { expect } from 'chai'; -var mocks = { - createFakeWindow: function (referrer, href) { - return { - document: { - referrer: referrer - }, - location: { - href: href, - // TODO: add ancestorOrigins to increase test coverage - }, - parent: null, - top: null - }; +/** + * Build a walkable linked list of window-like objects for testing. + * + * @param {Array} urls Array of URL strings starting from the top window. + * @param {string} [topReferrer] + * @param {string} [canonicalUrl] + * @param {boolean} [ancestorOrigins] + * @returns {Object} + */ +function buildWindowTree(urls, topReferrer = '', canonicalUrl = null, ancestorOrigins = false) { + /** + * Find the origin from a given fully-qualified URL. + * + * @param {string} url The fully qualified URL + * @returns {string|null} + */ + function getOrigin(url) { + const originRegex = new RegExp('^(https?://[^/]+/?)'); + + const result = originRegex.exec(url); + + if (result && result[0]) { + return result[0]; + } + + return null; } -} -describe('referer detection', () => { - it('should return referer details in nested friendly iframes', function() { - // Fake window object to test friendly iframes - // - Main page http://example.com/page.html - // - - Iframe1 http://example.com/iframe1.html - // - - - Iframe2 http://example.com/iframe2.html - let mockIframe2WinObject = mocks.createFakeWindow('http://example.com/iframe1.html', 'http://example.com/iframe2.html'); - let mockIframe1WinObject = mocks.createFakeWindow('http://example.com/page.html', 'http://example.com/iframe1.html'); - let mainWinObject = mocks.createFakeWindow('http://example.com/page.html', 'http://example.com/page.html'); - mainWinObject.document.querySelector = function() { - return { - href: 'http://prebid.org' + let previousWindow; + const myOrigin = getOrigin(urls[urls.length - 1]); + + const windowList = urls.map((url, index) => { + const theirOrigin = getOrigin(url), + sameOrigin = (myOrigin === theirOrigin); + + const win = {}; + + if (sameOrigin) { + win.location = { + href: url + }; + + if (ancestorOrigins) { + win.location.ancestorOrigins = urls.slice(0, index).reverse().map(getOrigin); + } + + if (index === 0) { + win.document = { + referrer: topReferrer + }; + + if (canonicalUrl) { + win.document.querySelector = function(selector) { + if (selector === "link[rel='canonical']") { + return { + href: canonicalUrl + }; + } + + return null; + }; + } + } else { + win.document = { + referrer: urls[index - 1] + }; } } - mockIframe2WinObject.parent = mockIframe1WinObject; - mockIframe2WinObject.top = mainWinObject; - mockIframe1WinObject.parent = mainWinObject; - mockIframe1WinObject.top = mainWinObject; - mainWinObject.top = mainWinObject; - - const getRefererInfo = detectReferer(mockIframe2WinObject); - let result = getRefererInfo(); - let expectedResult = { - referer: 'http://example.com/page.html', - reachedTop: true, - numIframes: 2, - stack: [ - 'http://example.com/page.html', - 'http://example.com/iframe1.html', - 'http://example.com/iframe2.html' - ], - canonicalUrl: 'http://prebid.org' - }; - expect(result).to.deep.equal(expectedResult); + + previousWindow = win; + + return win; + }); + + const topWindow = windowList[0]; + + previousWindow = null; + + windowList.forEach((win) => { + win.top = topWindow; + win.parent = previousWindow || topWindow; + previousWindow = win; + }); + + return windowList[windowList.length - 1]; +} + +describe('Referer detection', () => { + describe('Non cross-origin scenarios', () => { + describe('No iframes', () => { + it('Should return the current window location and no canonical URL', () => { + const testWindow = buildWindowTree(['https://example.com/some/page'], 'https://othersite.com/'), + result = detectReferer(testWindow)(); + + expect(result).to.deep.equal({ + referer: 'https://example.com/some/page', + reachedTop: true, + isAmp: false, + numIframes: 0, + stack: ['https://example.com/some/page'], + canonicalUrl: null + }); + }); + + it('Should return the current window location and a canonical URL', () => { + const testWindow = buildWindowTree(['https://example.com/some/page'], 'https://othersite.com/', 'https://example.com/canonical/page'), + result = detectReferer(testWindow)(); + + expect(result).to.deep.equal({ + referer: 'https://example.com/some/page', + reachedTop: true, + isAmp: false, + numIframes: 0, + stack: ['https://example.com/some/page'], + canonicalUrl: 'https://example.com/canonical/page' + }); + }); + }); + + describe('Friendly iframes', () => { + it('Should return the top window location and no canonical URL', () => { + const testWindow = buildWindowTree(['https://example.com/some/page', 'https://example.com/other/page', 'https://example.com/third/page'], 'https://othersite.com/'), + result = detectReferer(testWindow)(); + + expect(result).to.deep.equal({ + referer: 'https://example.com/some/page', + reachedTop: true, + isAmp: false, + numIframes: 2, + stack: [ + 'https://example.com/some/page', + 'https://example.com/other/page', + 'https://example.com/third/page' + ], + canonicalUrl: null + }); + }); + + it('Should return the top window location and a canonical URL', () => { + const testWindow = buildWindowTree(['https://example.com/some/page', 'https://example.com/other/page', 'https://example.com/third/page'], 'https://othersite.com/', 'https://example.com/canonical/page'), + result = detectReferer(testWindow)(); + + expect(result).to.deep.equal({ + referer: 'https://example.com/some/page', + reachedTop: true, + isAmp: false, + numIframes: 2, + stack: [ + 'https://example.com/some/page', + 'https://example.com/other/page', + 'https://example.com/third/page' + ], + canonicalUrl: 'https://example.com/canonical/page' + }); + }); + }); }); - it('should return referer details in nested cross domain iframes', function() { - // Fake window object to test cross domain iframes. - // - Main page http://example.com/page.html - // - - Iframe1 http://aaa.com/iframe1.html - // - - - Iframe2 http://bbb.com/iframe2.html - let mockIframe2WinObject = mocks.createFakeWindow('http://aaa.com/iframe1.html', 'http://bbb.com/iframe2.html'); - // Sinon cannot throw exception when accessing a propery so passing null to create cross domain - // environment for refererDetection module - let mockIframe1WinObject = mocks.createFakeWindow(null, null); - let mainWinObject = mocks.createFakeWindow(null, null); - mockIframe2WinObject.parent = mockIframe1WinObject; - mockIframe2WinObject.top = mainWinObject; - mockIframe1WinObject.parent = mainWinObject; - mockIframe1WinObject.top = mainWinObject; - mainWinObject.top = mainWinObject; - - const getRefererInfo = detectReferer(mockIframe2WinObject); - let result = getRefererInfo(); - let expectedResult = { - referer: 'http://aaa.com/iframe1.html', - reachedTop: false, - numIframes: 2, - stack: [ - null, - 'http://aaa.com/iframe1.html', - 'http://bbb.com/iframe2.html' - ], - canonicalUrl: undefined - }; - expect(result).to.deep.equal(expectedResult); + describe('Cross-origin scenarios', () => { + it('Should return the top URL and no canonical URL with one cross-origin iframe', () => { + const testWindow = buildWindowTree(['https://example.com/some/page', 'https://safe.frame/ad'], 'https://othersite.com/', 'https://canonical.example.com/'), + result = detectReferer(testWindow)(); + + expect(result).to.deep.equal({ + referer: 'https://example.com/some/page', + reachedTop: true, + isAmp: false, + numIframes: 1, + stack: [ + 'https://example.com/some/page', + 'https://safe.frame/ad' + ], + canonicalUrl: null + }); + }); + + it('Should return the top URL and no canonical URL with one cross-origin iframe and one friendly iframe', () => { + const testWindow = buildWindowTree(['https://example.com/some/page', 'https://safe.frame/ad', 'https://safe.frame/ad'], 'https://othersite.com/', 'https://canonical.example.com/'), + result = detectReferer(testWindow)(); + + expect(result).to.deep.equal({ + referer: 'https://example.com/some/page', + reachedTop: true, + isAmp: false, + numIframes: 2, + stack: [ + 'https://example.com/some/page', + 'https://safe.frame/ad', + 'https://safe.frame/ad' + ], + canonicalUrl: null + }); + }); + + it('Should return the second iframe location with three cross-origin windows and no ancessorOrigins', () => { + const testWindow = buildWindowTree(['https://example.com/some/page', 'https://safe.frame/ad', 'https://otherfr.ame/ad'], 'https://othersite.com/', 'https://canonical.example.com/'), + result = detectReferer(testWindow)(); + + expect(result).to.deep.equal({ + referer: 'https://safe.frame/ad', + reachedTop: false, + isAmp: false, + numIframes: 2, + stack: [ + null, + 'https://safe.frame/ad', + 'https://otherfr.ame/ad' + ], + canonicalUrl: null + }); + }); + + it('Should return the top window origin with three cross-origin windows with ancessorOrigins', () => { + const testWindow = buildWindowTree(['https://example.com/some/page', 'https://safe.frame/ad', 'https://otherfr.ame/ad'], 'https://othersite.com/', 'https://canonical.example.com/', true), + result = detectReferer(testWindow)(); + + expect(result).to.deep.equal({ + referer: 'https://example.com/', + reachedTop: false, + isAmp: false, + numIframes: 2, + stack: [ + 'https://example.com/', + 'https://safe.frame/ad', + 'https://otherfr.ame/ad' + ], + canonicalUrl: null + }); + }); + }); + + describe('Cross-origin AMP page scenarios', () => { + it('Should return the AMP page source and canonical URLs in an amp-ad iframe for a non-cached AMP page', () => { + const testWindow = buildWindowTree(['https://example.com/some/page/amp/', 'https://ad-iframe.ampproject.org/ad']); + + testWindow.context = { + sourceUrl: 'https://example.com/some/page/amp/', + canonicalUrl: 'https://example.com/some/page/' + }; + + const result = detectReferer(testWindow)(); + + expect(result).to.deep.equal({ + referer: 'https://example.com/some/page/amp/', + reachedTop: true, + isAmp: true, + numIframes: 1, + stack: [ + 'https://example.com/some/page/amp/', + 'https://ad-iframe.ampproject.org/ad' + ], + canonicalUrl: 'https://example.com/some/page/' + }); + }); + + it('Should return the AMP page source and canonical URLs in an amp-ad iframe for a cached AMP page on top', () => { + const testWindow = buildWindowTree(['https://example-com.amp-cache.example.com/some/page/amp/', 'https://ad-iframe.ampproject.org/ad']); + + testWindow.context = { + sourceUrl: 'https://example.com/some/page/amp/', + canonicalUrl: 'https://example.com/some/page/' + }; + + const result = detectReferer(testWindow)(); + + expect(result).to.deep.equal({ + referer: 'https://example.com/some/page/amp/', + reachedTop: true, + isAmp: true, + numIframes: 1, + stack: [ + 'https://example.com/some/page/amp/', + 'https://ad-iframe.ampproject.org/ad' + ], + canonicalUrl: 'https://example.com/some/page/' + }); + }); + + describe('Cached AMP page in iframed search result', () => { + it('Should return the AMP source and canonical URLs but with a null top-level stack location Without ancesorOrigins', () => { + const testWindow = buildWindowTree(['https://google.com/amp/example-com/some/page/amp/', 'https://example-com.amp-cache.example.com/some/page/amp/', 'https://ad-iframe.ampproject.org/ad']); + + testWindow.context = { + sourceUrl: 'https://example.com/some/page/amp/', + canonicalUrl: 'https://example.com/some/page/' + }; + + const result = detectReferer(testWindow)(); + + expect(result).to.deep.equal({ + referer: 'https://example.com/some/page/amp/', + reachedTop: false, + isAmp: true, + numIframes: 2, + stack: [ + null, + 'https://example.com/some/page/amp/', + 'https://ad-iframe.ampproject.org/ad' + ], + canonicalUrl: 'https://example.com/some/page/' + }); + }); + + it('Should return the AMP source and canonical URLs and include the top window origin in the stack with ancesorOrigins', () => { + const testWindow = buildWindowTree(['https://google.com/amp/example-com/some/page/amp/', 'https://example-com.amp-cache.example.com/some/page/amp/', 'https://ad-iframe.ampproject.org/ad'], null, null, true); + + testWindow.context = { + sourceUrl: 'https://example.com/some/page/amp/', + canonicalUrl: 'https://example.com/some/page/' + }; + + const result = detectReferer(testWindow)(); + + expect(result).to.deep.equal({ + referer: 'https://example.com/some/page/amp/', + reachedTop: false, + isAmp: true, + numIframes: 2, + stack: [ + 'https://google.com/', + 'https://example.com/some/page/amp/', + 'https://ad-iframe.ampproject.org/ad' + ], + canonicalUrl: 'https://example.com/some/page/' + }); + }); + + it('Should return the AMP source and canonical URLs and include the top window origin in the stack with ancesorOrigins and a friendly iframe under the amp-ad iframe', () => { + const testWindow = buildWindowTree(['https://google.com/amp/example-com/some/page/amp/', 'https://example-com.amp-cache.example.com/some/page/amp/', 'https://ad-iframe.ampproject.org/ad', 'https://ad-iframe.ampproject.org/ad'], null, null, true); + + testWindow.parent.context = { + sourceUrl: 'https://example.com/some/page/amp/', + canonicalUrl: 'https://example.com/some/page/' + }; + + const result = detectReferer(testWindow)(); + + expect(result).to.deep.equal({ + referer: 'https://example.com/some/page/amp/', + reachedTop: false, + isAmp: true, + numIframes: 3, + stack: [ + 'https://google.com/', + 'https://example.com/some/page/amp/', + 'https://ad-iframe.ampproject.org/ad', + 'https://ad-iframe.ampproject.org/ad' + ], + canonicalUrl: 'https://example.com/some/page/' + }); + }); + }); }); }); diff --git a/test/spec/renderer_spec.js b/test/spec/renderer_spec.js index 77d806e4dbc..dcca94396cd 100644 --- a/test/spec/renderer_spec.js +++ b/test/spec/renderer_spec.js @@ -132,6 +132,59 @@ describe('Renderer', function () { expect(utilsSpy.callCount).to.equal(1); }); + it('should load renderer adunit renderer when backupOnly', function() { + $$PREBID_GLOBAL$$.adUnits = [{ + code: 'video1', + renderer: { + url: 'http://acdn.adnxs.com/video/outstream/ANOutstreamVideo.js', + backupOnly: true, + render: sinon.spy() + } + }] + + let testRenderer = Renderer.install({ + url: 'https://httpbin.org/post', + config: { test: 'config1' }, + id: 1, + adUnitCode: 'video1' + + }); + testRenderer.setRender(() => {}) + + testRenderer.render() + expect(loadExternalScript.called).to.be.true; + }); + + it('should load external script instead of publisher-defined one when backupOnly option is true in mediaTypes.video options', function() { + $$PREBID_GLOBAL$$.adUnits = [{ + code: 'video1', + mediaTypes: { + video: { + context: 'outstream', + mimes: ['video/mp4'], + playerSize: [[400, 300]], + renderer: { + url: 'https://acdn.adnxs.com/video/outstream/ANOutstreamVideo.js', + backupOnly: true, + render: sinon.spy() + }, + } + } + }] + + let testRenderer = Renderer.install({ + url: 'https://httpbin.org/post', + config: { test: 'config1' }, + id: 1, + adUnitCode: 'video1' + + }); + testRenderer.setRender(() => {}) + + testRenderer.render() + expect(loadExternalScript.called).to.be.true; + }); + it('should call loadExternalScript() for script not defined on adUnit, only when .render() is called', function() { $$PREBID_GLOBAL$$.adUnits = [{ code: 'video1', diff --git a/test/spec/unit/core/adapterManager_spec.js b/test/spec/unit/core/adapterManager_spec.js index 9ccdd6aef59..f33c9139e5f 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, coppaDataHandler } 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 client adapters if client sources defined', function () { - stubGetSourceBidderMap.returns({[s2sTesting.CLIENT]: ['appnexus', 'adequant'], [s2sTesting.SERVER]: []}); + 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 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 => { @@ -1290,13 +1904,35 @@ describe('adapterManager tests', function () { expect(bidRequests[0].gdprConsent).to.be.undefined; }); }); - + describe('coppa consent module', function () { + afterEach(() => { + config.resetConfig(); + }); + it('test coppa configuration with value false', function () { + config.setConfig({ coppa: 0 }); + const coppa = coppaDataHandler.getCoppa(); + expect(coppa).to.be.false; + }); + it('test coppa configuration with value true', function () { + config.setConfig({ coppa: 1 }); + const coppa = coppaDataHandler.getCoppa(); + expect(coppa).to.be.true; + }); + it('test coppa configuration', function () { + const coppa = coppaDataHandler.getCoppa(); + expect(coppa).to.be.false; + }); + }); 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 +2049,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/test/spec/unit/core/storageManager_spec.js b/test/spec/unit/core/storageManager_spec.js index 0b406242f90..5bb766217f5 100644 --- a/test/spec/unit/core/storageManager_spec.js +++ b/test/spec/unit/core/storageManager_spec.js @@ -42,5 +42,57 @@ describe('storage manager', function() { storage.setCookie('foo1', 'baz1'); expect(deviceAccessSpy.calledOnce).to.equal(true); deviceAccessSpy.restore(); + }); + + describe('localstorage forbidden access in 3rd-party context', function() { + let errorLogSpy; + let originalLocalStorage; + const localStorageMock = { get: () => { throw Error } }; + + beforeEach(function() { + originalLocalStorage = window.localStorage; + Object.defineProperty(window, 'localStorage', localStorageMock); + errorLogSpy = sinon.spy(utils, 'logError'); + }); + + afterEach(function() { + Object.defineProperty(window, 'localStorage', { get: () => originalLocalStorage }); + errorLogSpy.restore(); + }) + + it('should not throw if the localstorage is not accessible when setting/getting/removing from localstorage', function() { + const coreStorage = getStorageManager(); + + coreStorage.setDataInLocalStorage('key', 'value'); + const val = coreStorage.getDataFromLocalStorage('key'); + coreStorage.removeDataFromLocalStorage('key'); + + expect(val).to.be.null; + sinon.assert.calledThrice(errorLogSpy); + }) }) + + describe('localstorage is enabled', function() { + let localStorage; + + beforeEach(function() { + localStorage = window.localStorage; + localStorage.clear(); + }); + + afterEach(function() { + localStorage.clear(); + }) + + it('should remove side-effect after checking', function () { + const storage = getStorageManager(); + + localStorage.setItem('unrelated', 'dummy'); + const val = storage.localStorageIsEnabled(); + + expect(val).to.be.true; + expect(localStorage.length).to.be.eq(1); + expect(localStorage.getItem('unrelated')).to.be.eq('dummy'); + }); + }); }); diff --git a/test/spec/unit/core/targeting_spec.js b/test/spec/unit/core/targeting_spec.js index 3aae6e3c33a..5d43ed48266 100644 --- a/test/spec/unit/core/targeting_spec.js +++ b/test/spec/unit/core/targeting_spec.js @@ -416,6 +416,46 @@ describe('targeting tests', function () { }); }); + describe('targetingControls.allowTargetingKeys', function () { + let bid4; + + beforeEach(function() { + bid4 = utils.deepClone(bid1); + bid4.adserverTargeting = { + hb_deal: '4321', + hb_pb: '0.1', + hb_adid: '567891011', + hb_bidder: 'appnexus', + }; + bid4.bidder = bid4.bidderCode = 'appnexus'; + bid4.cpm = 0.1; // losing bid so not included if enableSendAllBids === false + bid4.dealId = '4321'; + enableSendAllBids = true; + config.setConfig({ + targetingControls: { + allowTargetingKeys: ['BIDDER', 'AD_ID', 'PRICE_BUCKET'] + } + }); + bidsReceived.push(bid4); + }); + + it('targeting should include custom keys', function () { + const targeting = targetingInstance.getAllTargeting(['/123456/header-bid-tag-0']); + expect(targeting['/123456/header-bid-tag-0']).to.include.all.keys('foobar'); + }); + + it('targeting should include keys prefixed by allowed default targeting keys', function () { + const targeting = targetingInstance.getAllTargeting(['/123456/header-bid-tag-0']); + expect(targeting['/123456/header-bid-tag-0']).to.include.all.keys('hb_bidder_rubicon', 'hb_adid_rubicon', 'hb_pb_rubicon'); + expect(targeting['/123456/header-bid-tag-0']).to.include.all.keys('hb_bidder_appnexus', 'hb_adid_appnexus', 'hb_pb_appnexus'); + }); + + it('targeting should not include keys prefixed by disallowed default targeting keys', function () { + const targeting = targetingInstance.getAllTargeting(['/123456/header-bid-tag-0']); + expect(targeting['/123456/header-bid-tag-0']).to.not.have.all.keys(['hb_deal_appnexus', 'hb_deal_rubicon']); + }); + }); + describe('targetingControls.alwaysIncludeDeals', function () { let bid4; diff --git a/test/spec/unit/pbjs_api_spec.js b/test/spec/unit/pbjs_api_spec.js index 960ccf08c92..805eae9e3bc 100644 --- a/test/spec/unit/pbjs_api_spec.js +++ b/test/spec/unit/pbjs_api_spec.js @@ -1197,6 +1197,14 @@ describe('Unit: Prebid Module', function () { assert.deepEqual($$PREBID_GLOBAL$$.getAllWinningBids()[0], adResponse); }); + it('should replace ${CLICKTHROUGH} macro in winning bids response', function () { + pushBidResponseToAuction({ + ad: "" + }); + $$PREBID_GLOBAL$$.renderAd(doc, bidId, {clickThrough: 'https://someadserverclickurl.com'}); + expect(adResponse).to.have.property('ad').and.to.match(/https:\/\/someadserverclickurl\.com/i); + }); + it('fires billing url if present on s2s bid', function () { const burl = 'http://www.example.com/burl'; pushBidResponseToAuction({ @@ -1765,6 +1773,32 @@ describe('Unit: Prebid Module', function () { expect(auctionArgs.adUnits[0].mediaTypes.native.icon).to.exist; assert.ok(logErrorSpy.calledWith('Please use an array of sizes for native.icon.sizes field. Removing invalid mediaTypes.native.icon.sizes property from request.')); }); + + it('should throw error message and remove adUnit if adUnit.bids is not defined correctly', function () { + const adUnits = [{ + code: 'ad-unit-1', + mediaTypes: { + banner: { + sizes: [300, 400] + } + }, + bids: [{code: 'appnexus', params: 1234}] + }, { + code: 'bad-ad-unit-2', + mediaTypes: { + banner: { + sizes: [300, 400] + } + } + }]; + + $$PREBID_GLOBAL$$.requestBids({ + adUnits: adUnits + }); + expect(auctionArgs.adUnits.length).to.equal(1); + expect(auctionArgs.adUnits[1]).to.not.exist; + assert.ok(logErrorSpy.calledWith("Detected adUnit.code 'bad-ad-unit-2' did not have 'adUnit.bids' defined or 'adUnit.bids' is not an array. Removing adUnit from auction.")); + }); }); }); }); @@ -2541,6 +2575,58 @@ describe('Unit: Prebid Module', function () { }); }); + describe('getHighestUnusedBidResponseForAdUnitCode', () => { + afterEach(() => { + resetAuction(); + }) + + it('returns an empty object if there is no bid for the given adUnitCode', () => { + const highestBid = $$PREBID_GLOBAL$$.getHighestUnusedBidResponseForAdUnitCode('stallone'); + expect(highestBid).to.deep.equal({}); + }) + + it('returns undefined if adUnitCode is provided', () => { + const highestBid = $$PREBID_GLOBAL$$.getHighestUnusedBidResponseForAdUnitCode(); + expect(highestBid).to.be.undefined; + }) + + it('should ignore bids that have already been used (\'rendered\')', () => { + const _bidsReceived = getBidResponses().slice(0, 3); + _bidsReceived[0].cpm = 11 + _bidsReceived[1].cpm = 13 + _bidsReceived[2].cpm = 12 + + _bidsReceived.forEach((bid) => { + bid.adUnitCode = '/19968336/header-bid-tag-0'; + }); + + auction.getBidsReceived = function() { return _bidsReceived }; + const highestBid1 = $$PREBID_GLOBAL$$.getHighestUnusedBidResponseForAdUnitCode('/19968336/header-bid-tag-0'); + expect(highestBid1).to.deep.equal(_bidsReceived[1]) + _bidsReceived[1].status = CONSTANTS.BID_STATUS.RENDERED + const highestBid2 = $$PREBID_GLOBAL$$.getHighestUnusedBidResponseForAdUnitCode('/19968336/header-bid-tag-0'); + expect(highestBid2).to.deep.equal(_bidsReceived[2]) + }) + + it('should ignore expired bids', () => { + const _bidsReceived = getBidResponses().slice(0, 3); + _bidsReceived[0].cpm = 11 + _bidsReceived[1].cpm = 13 + _bidsReceived[2].cpm = 12 + + _bidsReceived.forEach((bid) => { + bid.adUnitCode = '/19968336/header-bid-tag-0'; + }); + + auction.getBidsReceived = function() { return _bidsReceived }; + + bidExpiryStub.restore(); + bidExpiryStub = sinon.stub(filters, 'isBidNotExpired').callsFake((bid) => bid.cpm !== 13); + const highestBid = $$PREBID_GLOBAL$$.getHighestUnusedBidResponseForAdUnitCode('/19968336/header-bid-tag-0'); + expect(highestBid).to.deep.equal(_bidsReceived[2]) + }) + }) + describe('getHighestCpm', () => { after(() => { resetAuction(); diff --git a/test/spec/userSync_spec.js b/test/spec/userSync_spec.js index 2b68349ca31..55b613ce929 100644 --- a/test/spec/userSync_spec.js +++ b/test/spec/userSync_spec.js @@ -475,6 +475,24 @@ describe('user sync', function () { }); expect(userSync.canBidderRegisterSync('iframe', 'otherTestBidder')).to.equal(false); }); + it('should return false for iframe if there is no iframe filterSettings', function () { + const userSync = newUserSync({ + config: { + syncEnabled: true, + filterSettings: { + image: { + bidders: '*', + filter: 'include' + } + }, + syncsPerBidder: 5, + syncDelay: 3000, + auctionDelay: 0 + } + }); + + expect(userSync.canBidderRegisterSync('iframe', 'otherTestBidder')).to.equal(false); + }); it('should return true if filter settings does allow it', function () { const userSync = newUserSync({ config: { diff --git a/test/spec/utils_spec.js b/test/spec/utils_spec.js index fca59633ebe..6494ead78e7 100644 --- a/test/spec/utils_spec.js +++ b/test/spec/utils_spec.js @@ -1177,5 +1177,25 @@ describe('Utils', function () { } expect(utils.deepEqual(obj1, obj2)).to.equal(false); }); + + describe('cyrb53Hash', function() { + it('should return the same hash for the same string', function() { + const stringOne = 'string1'; + expect(utils.cyrb53Hash(stringOne)).to.equal(utils.cyrb53Hash(stringOne)); + }); + it('should return a different hash for the same string with different seeds', function() { + const stringOne = 'string1'; + expect(utils.cyrb53Hash(stringOne, 1)).to.not.equal(utils.cyrb53Hash(stringOne, 2)); + }); + it('should return a different hash for different strings with the same seed', function() { + const stringOne = 'string1'; + const stringTwo = 'string2'; + expect(utils.cyrb53Hash(stringOne)).to.not.equal(utils.cyrb53Hash(stringTwo)); + }); + it('should return a string value, not a number', function() { + const stringOne = 'string1'; + expect(typeof utils.cyrb53Hash(stringOne)).to.equal('string'); + }); + }); }); }); diff --git a/test/spec/videoCache_spec.js b/test/spec/videoCache_spec.js index 6bb214af8a0..554db3ebe4e 100644 --- a/test/spec/videoCache_spec.js +++ b/test/spec/videoCache_spec.js @@ -201,13 +201,15 @@ describe('The video cache', function () { ttl: 25, customCacheKey: customKey1, requestId: '12345abc', - bidder: 'appnexus' + bidder: 'appnexus', + auctionId: '1234-56789-abcde' }, { vastXml: vastXml2, ttl: 25, customCacheKey: customKey2, requestId: 'cba54321', - bidder: 'rubicon' + bidder: 'rubicon', + auctionId: '1234-56789-abcde' }]; store(bids, function () { }); @@ -222,6 +224,7 @@ describe('The video cache', function () { ttlseconds: 25, key: customKey1, bidid: '12345abc', + aid: '1234-56789-abcde', bidder: 'appnexus' }, { type: 'xml', @@ -229,6 +232,7 @@ describe('The video cache', function () { ttlseconds: 25, key: customKey2, bidid: 'cba54321', + aid: '1234-56789-abcde', bidder: 'rubicon' }] }; @@ -254,13 +258,15 @@ describe('The video cache', function () { ttl: 25, customCacheKey: customKey1, requestId: '12345abc', - bidder: 'appnexus' + bidder: 'appnexus', + auctionId: '1234-56789-abcde' }, { vastXml: vastXml2, ttl: 25, customCacheKey: customKey2, requestId: 'cba54321', - bidder: 'rubicon' + bidder: 'rubicon', + auctionId: '1234-56789-abcde' }]; store(bids, function () { }, getMockBidRequest()); @@ -276,6 +282,7 @@ describe('The video cache', function () { key: customKey1, bidid: '12345abc', bidder: 'appnexus', + aid: '1234-56789-abcde', timestamp: 1510852447530 }, { type: 'xml', @@ -284,6 +291,7 @@ describe('The video cache', function () { key: customKey2, bidid: 'cba54321', bidder: 'rubicon', + aid: '1234-56789-abcde', timestamp: 1510852447530 }] }; diff --git a/test/spec/video_spec.js b/test/spec/video_spec.js index 72a585049c3..3ce8ba081da 100644 --- a/test/spec/video_spec.js +++ b/test/spec/video_spec.js @@ -71,6 +71,29 @@ describe('video.js', function () { expect(valid).to.equal(true); }); + it('validates valid outstream bids with a publisher defined renderer', function () { + const bid = { + requestId: '123abc', + }; + const bidRequests = [{ + bids: [{ + bidId: '123abc', + bidder: 'appnexus', + mediaTypes: { + video: { + context: 'outstream', + renderer: { + url: 'render.url', + render: () => true, + } + } + } + }] + }]; + const valid = isValidVideoBid(bid, bidRequests); + expect(valid).to.equal(true); + }); + it('catches invalid outstream bids', function () { const bid = { requestId: '123abc' 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) {