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 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ID Import Library Example
+ Steps before logging in:
+
+
+ Open console
+
+ For Mac, Command+Option+J
+ Windows/Linux, Control+Shift+J
+
+
+ Search for 'ID-Library' in console
+
+
+Login
+
+
+
+
+
+
+
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 @@
+
+
+
+
+
+
+
+
+
+
+
+ Refresh Ad Unit
+ 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
+
+
-
+
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) {