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/.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 962e057fbc5..606d26cd25a 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -3,7 +3,7 @@ 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). @@ -59,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 d7703cf20ae..0519cbb7b6e 100644 --- a/PR_REVIEW.md +++ b/PR_REVIEW.md @@ -5,49 +5,113 @@ 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 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` - - If they support one or more userId modules, add `userId: (list of supported vendors)` - - If they support video and/or native mediaTypes add `media_types: video, native`. Note that display is added by default. If you don't support display, add "no-display" as the first entry, e.g. `media_types: no-display, native` - - 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 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. -- 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: +- 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 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 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. - 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 `getFloors()` function. + - 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` + - If they support one or more userId modules, add `userId: (list of supported vendors)` + - If they support video and/or native mediaTypes add `media_types: video, native`. Note that display is added by default. If you don't support display, add "no-display" as the first entry, e.g. `media_types: no-display, native` + - 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 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 44882570d89..40df62ccee4 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/gulpfile.js b/gulpfile.js index 879e34ae588..b7a9e442a8c 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -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([ @@ -383,4 +397,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/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/haloRtdProvider_example.html b/integrationExamples/gpt/haloRtdProvider_example.html new file mode 100644 index 00000000000..7f9a34e55ee --- /dev/null +++ b/integrationExamples/gpt/haloRtdProvider_example.html @@ -0,0 +1,149 @@ + + + + + + + + + + + + + + + +

Audigent Segments Prebid

+ +
+ +
+ +Halo Id: +
+
+ +Audigent Segments (Appnexus): +
+
+ + diff --git a/integrationExamples/gpt/jwplayerRtdProvider_example.html b/integrationExamples/gpt/jwplayerRtdProvider_example.html index 3791ab42137..75eb85a2d8c 100644 --- a/integrationExamples/gpt/jwplayerRtdProvider_example.html +++ b/integrationExamples/gpt/jwplayerRtdProvider_example.html @@ -11,10 +11,18 @@ var adUnits = [{ code: 'div-gpt-ad-1460505748561-0', - jwTargeting: { - playerID: '123', - mediaID: 'abc' + fpd: { + context: { + data: { + jwTargeting: { + // Note: the following Ids are placeholders and should be replaced with your Ids. + playerID: '123', + mediaID: 'abc' + } + }, + } }, + mediaTypes: { banner: { sizes: [[300, 250], [300,600]], @@ -32,7 +40,6 @@ var pbjs = pbjs || {}; pbjs.que = pbjs.que || []; - + + + Reconciliation RTD Provider Example + + + + + + + + +
Div-1
+
+ +
+ + diff --git a/integrationExamples/gpt/userId_example.html b/integrationExamples/gpt/userId_example.html index 8e69bc6c6a7..7375293fdf0 100644 --- a/integrationExamples/gpt/userId_example.html +++ b/integrationExamples/gpt/userId_example.html @@ -134,6 +134,30 @@ // }, userSync: { userIds: [{ + name: "pubProvidedId", + params: { + eids: [{ + source: "domain.com", + uids:[{ + id: "value read from cookie or local storage", + atype: 1, + ext: { + stype: "ppuid" // allowable options are sha256email, DMP, ppuid for now + } + }] + },{ + source: "3rdpartyprovided.com", + uids:[{ + id: "value read from cookie or local storage", + atype: 3, + ext: { + stype: "sha256email" + } + }] + }], + eidsFunction: getHashedEmail // any user defined function that exists in the page + } + },{ name: "unifiedId", params: { partner: "prebid", @@ -245,7 +269,10 @@ }, { name: "quantcastId" - } + }, + { + name: "criteo" + }, ], syncDelay: 5000, auctionDelay: 1000 diff --git a/integrationExamples/reviewerTools/index.html b/integrationExamples/reviewerTools/index.html new file mode 100755 index 00000000000..2732cb4fd88 --- /dev/null +++ b/integrationExamples/reviewerTools/index.html @@ -0,0 +1,40 @@ + + + + + + + Prebid Reviewer Tools + + + + +
+
+
+

Reviewer Tools

+

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

+

Common

+ +

Other Tools

+ +

Documentation & Training Material

+ +
+
+
+ + \ No newline at end of file diff --git a/modules/.submodules.json b/modules/.submodules.json index 18e75dd1794..4e02391129a 100644 --- a/modules/.submodules.json +++ b/modules/.submodules.json @@ -15,7 +15,11 @@ "intentIqIdSystem", "zeotapIdPlusIdSystem", "haloIdSystem", - "quantcastIdSystem" + "quantcastIdSystem", + "idxIdSystem", + "fabrickIdSystem", + "verizonMediaIdSystem", + "pubProvidedIdSystem" ], "adpod": [ "freeWheelAdserverVideo", @@ -23,7 +27,9 @@ ], "rtdModule": [ "browsiRtdProvider", - "audigentRtdProvider", - "jwplayerRtdProvider" + "haloRtdProvider", + "jwplayerRtdProvider", + "reconciliationRtdProvider", + "geoedgeRtdProvider" ] } diff --git a/modules/33acrossBidAdapter.js b/modules/33acrossBidAdapter.js index 798b6450946..65df8baad2e 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) { @@ -173,53 +245,187 @@ 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: SIZE INFERENCE +function _transformSizes(sizes) { + if (utils.isArray(sizes) && sizes.length === 2 && !utils.isArray(sizes[0])) { + return [ _getSize(sizes) ]; + } - const { consentString, gdprApplies } = gdprConsent; + return sizes.map(_getSize); +} - const sync = { - type: 'iframe', - url: `${syncUrl}&id=${siteId}&gdpr_consent=${encodeURIComponent(consentString)}&us_privacy=${encodeURIComponent(uspConsent)}` - }; +function _getSize(size) { + return { + w: parseInt(size[0], 10), + h: parseInt(size[1], 10) + } +} - if (typeof gdprApplies === 'boolean') { - sync.url += `&gdpr=${Number(gdprApplies)}`; +// 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 sync; + 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 +445,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 +505,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); - - ttx.viewability = { amount: isNaN(viewabilityAmount) ? viewabilityAmount : Math.round(viewabilityAmount) }; +function contributeViewability(viewabilityAmount) { + const amount = isNaN(viewabilityAmount) ? viewabilityAmount : Math.round(viewabilityAmount); - return req; - } - - return contributeViewability; + return { + ttx: { + viewability: { + amount + } + } + }; } function _isIframe() { @@ -331,42 +525,9 @@ function _isIframe() { } } -function isBidRequestValid(bid) { - if (bid.bidder !== BIDDER_CODE || typeof bid.params === 'undefined') { - return false; - } - - if (typeof bid.params.siteId === 'undefined' || typeof bid.params.productId === 'undefined') { - return false; - } - - return true; -} - -// 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); - - return bidRequests.map(bidRequest => _createServerRequest( - { - bidRequest, - gdprConsent, - uspConsent, - pageUrl - }) - ); -} - -// NOTE: At this point, the response from 33exchange will only ever contain one bid i.e. the highest bid +// **************************** 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 = []; @@ -378,6 +539,36 @@ function interpretResponse(serverResponse, bidRequest) { return bidResponses; } +// All this assumes that only one bid is ever returned by ttx +function _createBidResponse(response) { + 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 + } + + if (bid.mediaType === VIDEO) { + const vastType = utils.deepAccess(response.seatbid[0].bid[0], 'ext.ttx.vastType', 'xml'); + + if (vastType === 'xml') { + bid.vastXml = bid.ad; + } else { + bid.vastUrl = bid.ad; + } + } + + 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 +586,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/ablidaBidAdapter.js b/modules/ablidaBidAdapter.js index af9c206700f..2400952367f 100644 --- a/modules/ablidaBidAdapter.js +++ b/modules/ablidaBidAdapter.js @@ -1,14 +1,14 @@ import * as utils from '../src/utils.js'; import {config} from '../src/config.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; -import { BANNER, NATIVE } from '../src/mediaTypes.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], + supportedMediaTypes: [BANNER, NATIVE, VIDEO], /** * Determines whether or not the given bid request is valid. @@ -35,6 +35,8 @@ export const spec = { 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(); @@ -46,7 +48,7 @@ export const spec = { referer: bidderRequest.refererInfo.referer, jaySupported: jaySupported, device: device, - adapterVersion: 4, + adapterVersion: 5, mediaTypes: bidRequest.mediaTypes, gdprConsent: bidderRequest.gdprConsent }; diff --git a/modules/ablidaBidAdapter.md b/modules/ablidaBidAdapter.md index 001bee4f35c..e0a9f3f9405 100644 --- a/modules/ablidaBidAdapter.md +++ b/modules/ablidaBidAdapter.md @@ -51,6 +51,22 @@ Module that connects to Ablida's bidder for bids. } } ] + }, { + 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..3a0a8a22274 --- /dev/null +++ b/modules/adWMGBidAdapter.js @@ -0,0 +1,297 @@ +'use strict'; + +import * as utils from '../src/utils.js'; +import { registerBidder } from '../src/adapters/bidderFactory.js'; +import { config } from '../src/config.js'; +import { BANNER } from '../src/mediaTypes.js'; + +const BIDDER_CODE = 'adWMG'; +const ENDPOINT = 'https://rtb.adwmg.com/prebid'; +let SYNC_ENDPOINT = 'https://rtb.adwmg.com/cphb.html?'; + +export const spec = { + code: BIDDER_CODE, + aliases: ['wmg'], + supportedMediaTypes: [BANNER], + isBidRequestValid: (bid) => { + if (bid.bidder !== BIDDER_CODE) { + return false; + } + + if (!(bid.params.publisherId)) { + return false; + } + + return true; + }, + buildRequests: (validBidRequests, bidderRequest) => { + const timeout = bidderRequest.timeout || 0; + const debug = config.getConfig('debug') || false; + const referrer = bidderRequest.refererInfo.referer; + const locale = window.navigator.language && window.navigator.language.length > 0 ? window.navigator.language.substr(0, 2) : ''; + const domain = config.getConfig('publisherDomain') || (window.location && window.location.host ? window.location.host : ''); + const ua = window.navigator.userAgent.toLowerCase(); + const additional = spec.parseUserAgent(ua); + + return validBidRequests.map(bidRequest => { + const adUnit = { + code: bidRequest.adUnitCode, + bids: { + bidder: bidRequest.bidder, + params: bidRequest.params + }, + mediaTypes: bidRequest.mediaTypes + }; + + if (bidRequest.hasOwnProperty('sizes') && bidRequest.sizes.length > 0) { + adUnit.sizes = bidRequest.sizes; + } + + const request = { + auctionId: bidRequest.auctionId, + requestId: bidRequest.bidId, + bidRequestsCount: bidRequest.bidRequestsCount, + bidderRequestId: bidRequest.bidderRequestId, + transactionId: bidRequest.transactionId, + referrer: referrer, + timeout: timeout, + adUnit: adUnit, + locale: locale, + domain: domain, + os: additional.os, + osv: additional.osv, + devicetype: additional.devicetype + }; + + if (bidderRequest.gdprConsent) { + request.gdpr = { + applies: bidderRequest.gdprConsent.gdprApplies, + consentString: bidderRequest.gdprConsent.consentString + }; + } + + /* if (bidderRequest.uspConsent) { + request.uspConsent = bidderRequest.uspConsent; + } + */ + if (bidRequest.userId && bidRequest.userId.pubcid) { + request.userId = { + pubcid: bidRequest.userId.pubcid + }; + } + + if (debug) { + request.debug = debug; + } + + return { + method: 'POST', + url: ENDPOINT, + data: JSON.stringify(request) + } + }); + }, + interpretResponse: (serverResponse) => { + const bidResponses = []; + + if (serverResponse.body) { + const response = serverResponse.body; + const bidResponse = { + requestId: response.requestId, + cpm: response.cpm, + width: response.width, + height: response.height, + creativeId: response.creativeId, + currency: response.currency, + netRevenue: response.netRevenue, + ttl: response.ttl, + ad: response.ad, + }; + bidResponses.push(bidResponse); + } + + return bidResponses; + }, + getUserSyncs: (syncOptions, serverResponses, gdprConsent, uspConsent) => { + if (gdprConsent) { + SYNC_ENDPOINT = utils.tryAppendQueryString(SYNC_ENDPOINT, 'gdpr', (gdprConsent.gdprApplies ? 1 : 0)); + } + + if (gdprConsent && typeof gdprConsent.consentString === 'string') { + SYNC_ENDPOINT = utils.tryAppendQueryString(SYNC_ENDPOINT, 'gdpr_consent', gdprConsent.consentString); + } + + /* 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..8c277b803db --- /dev/null +++ b/modules/adWMGBidAdapter.md @@ -0,0 +1,34 @@ +# 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 | + + +# 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..6f7feec59c9 100644 --- a/modules/adagioBidAdapter.js +++ b/modules/adagioBidAdapter.js @@ -1,22 +1,28 @@ 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, 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.6.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, 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 ADAGIO_PUBKEY = `-----BEGIN PUBLIC KEY----- MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC9el0+OEn6fvEh1RdVHQu4cnT0 @@ -25,6 +31,35 @@ 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; export function adagioScriptFromLocalStorageCb(ls) { @@ -62,7 +97,7 @@ export function adagioScriptFromLocalStorageCb(ls) { export function getAdagioScript() { storage.getDataFromLocalStorage(ADAGIO_LOCALSTORAGE_KEY, (ls) => { - internal.adagioScriptFromLocalStorageCb(ls) + internal.adagioScriptFromLocalStorageCb(ls); }); storage.localStorageIsEnabled(isValid => { @@ -333,7 +368,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 +468,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 +483,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 +546,21 @@ 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)) + ); +} + export const internal = { enqueue, getOrAddAdagioAdUnit, @@ -519,7 +575,9 @@ export const internal = { getRefererInfo, adagioScriptFromLocalStorageCb, getCurrentWindow, - canAccessTopWindow + supportIObs, + canAccessTopWindow, + isRendererPreferredFromPublisher }; function _getGdprConsent(bidderRequest) { @@ -537,7 +595,7 @@ function _getGdprConsent(bidderRequest) { const consent = {}; if (apiVersion !== undefined) { - consent.apiVersion = apiVersion + consent.apiVersion = apiVersion; } if (consentString !== undefined) { @@ -555,12 +613,80 @@ 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`); + } + }); +} + export const spec = { code: BIDDER_CODE, gvlid: GVLID, @@ -573,16 +699,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 +745,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 +775,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 +793,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 +812,15 @@ export const spec = { site: site, pageviewId: pageviewId, adUnits: groupedAdUnits[organizationId], - gdpr: gdprConsent, + regs: { + gdpr: gdprConsent, + coppa: coppa, + ccpa: uspConsent + }, schain: schain, + user: { + eids: eids + }, prebidVersion: '$prebid.version$', adapterVersion: VERSION, featuresVersion: FEATURES_VERSION @@ -681,7 +828,7 @@ export const spec = { options: { contentType: 'text/plain' } - } + }; }); return requests; @@ -702,7 +849,30 @@ export const spec = { if (response.bids) { response.bids.forEach(bidObj => { const bidReq = (find(bidRequest.data.adUnits, bid => bid.bidId === bidObj.requestId)); + if (bidReq) { + 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); + } + } + 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..aa79338d79e 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,58 @@ 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 + } } }] } @@ -88,5 +139,4 @@ Connects to Adagio demand source to fetch bids. ] } } - ``` diff --git a/modules/adformBidAdapter.js b/modules/adformBidAdapter.js index 48000e082b2..05e45a428d3 100644 --- a/modules/adformBidAdapter.js +++ b/modules/adformBidAdapter.js @@ -23,7 +23,7 @@ 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 ] ]; var bids = JSON.parse(JSON.stringify(validBidRequests)); var bidder = (bids[0] && bids[0].bidder) || BIDDER_CODE; for (i = 0, l = bids.length; i < l; i++) { @@ -65,10 +65,6 @@ export const spec = { request.push('us_privacy=' + bidderRequest.uspConsent); } - if (eids) { - request.push('eids=' + eids); - } - for (i = 1, l = globalParams.length; i < l; i++) { _key = globalParams[i][0]; _value = globalParams[i][1]; @@ -100,7 +96,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)); } } diff --git a/modules/adheseBidAdapter.js b/modules/adheseBidAdapter.js index 606e56c13d5..80758668a95 100644 --- a/modules/adheseBidAdapter.js +++ b/modules/adheseBidAdapter.js @@ -150,8 +150,8 @@ function getAccount(validBidRequests) { } function getId5Id(validBidRequests) { - if (validBidRequests[0] && validBidRequests[0].userId && validBidRequests[0].userId.id5id) { - return validBidRequests[0].userId.id5id; + if (validBidRequests[0] && validBidRequests[0].userId && validBidRequests[0].userId.id5id && validBidRequests[0].userId.id5id.uid) { + return validBidRequests[0].userId.id5id.uid; } } diff --git a/modules/adkernelBidAdapter.js b/modules/adkernelBidAdapter.js index 972dd696bf6..c34902eda46 100644 --- a/modules/adkernelBidAdapter.js +++ b/modules/adkernelBidAdapter.js @@ -52,7 +52,7 @@ const NATIVE_INDEX = NATIVE_MODEL.reduce((acc, val, idx) => { export const spec = { code: 'adkernel', - aliases: ['headbidding', 'adsolut', 'oftmediahb', 'audiencemedia', 'waardex_ak', 'roqoon'], + aliases: ['headbidding', 'adsolut', 'oftmediahb', 'audiencemedia', 'waardex_ak', 'roqoon', 'andbeyond', 'adbite', 'houseofpubs'], supportedMediaTypes: [BANNER, VIDEO, NATIVE], /** @@ -116,13 +116,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 +134,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; }); }, diff --git a/modules/admanBidAdapter.js b/modules/admanBidAdapter.js index 5dc3412ee66..2e4091e7a24 100644 --- a/modules/admanBidAdapter.js +++ b/modules/admanBidAdapter.js @@ -95,10 +95,21 @@ export const spec = { return response; }, - getUserSyncs: (syncOptions, serverResponses) => { + getUserSyncs: (syncOptions, serverResponses, gdprConsent, uspConsent) => { + let syncUrl = URL_SYNC + if (gdprConsent && gdprConsent.consentString) { + if (typeof gdprConsent.gdprApplies === 'boolean') { + syncUrl += `&gdpr=${Number(gdprConsent.gdprApplies)}&gdpr_consent=${gdprConsent.consentString}`; + } else { + syncUrl += `&gdpr==0&gdpr_consent=${gdprConsent.consentString}`; + } + } + if (uspConsent && uspConsent.consentString) { + syncUrl += `&ccpa_consent=${uspConsent.consentString}`; + } return [{ type: 'image', - url: URL_SYNC + url: syncUrl }]; } diff --git a/modules/admixerBidAdapter.js b/modules/admixerBidAdapter.js index b2f24cfa910..405dd81cc8c 100644 --- a/modules/admixerBidAdapter.js +++ b/modules/admixerBidAdapter.js @@ -3,7 +3,7 @@ import {registerBidder} from '../src/adapters/bidderFactory.js'; const BIDDER_CODE = 'admixer'; const ALIASES = ['go2net']; -const ENDPOINT_URL = 'https://inv-nets.admixer.net/prebid.1.0.aspx'; +const ENDPOINT_URL = 'https://inv-nets.admixer.net/prebid.1.1.aspx'; export const spec = { code: BIDDER_CODE, aliases: ALIASES, @@ -51,10 +51,9 @@ export const spec = { */ interpretResponse: function (serverResponse, bidRequest) { const bidResponses = []; - // loop through serverResponses { try { - serverResponse = serverResponse.body; - serverResponse.forEach((bidResponse) => { + const {body: {ads = []} = {}} = serverResponse; + ads.forEach((bidResponse) => { const bidResp = { requestId: bidResponse.bidId, cpm: bidResponse.cpm, @@ -66,6 +65,7 @@ export const spec = { netRevenue: bidResponse.netRevenue, currency: bidResponse.currency, vastUrl: bidResponse.vastUrl, + dealId: bidResponse.dealId, }; bidResponses.push(bidResp); }); @@ -73,6 +73,19 @@ export const spec = { utils.logError(e); } return bidResponses; + }, + getUserSyncs: function(syncOptions, serverResponses, gdprConsent) { + const pixels = []; + serverResponses.forEach(({body: {cm = {}} = {}}) => { + const {pixels: img = [], iframes: frm = []} = cm; + if (syncOptions.pixelEnabled) { + img.forEach((url) => pixels.push({type: 'image', url})); + } + if (syncOptions.iframeEnabled) { + frm.forEach((url) => pixels.push({type: 'iframe', url})); + } + }); + return pixels; } }; registerBidder(spec); diff --git a/modules/adnowBidAdapter.js b/modules/adnowBidAdapter.js new file mode 100644 index 00000000000..7412db8d7b6 --- /dev/null +++ b/modules/adnowBidAdapter.js @@ -0,0 +1,178 @@ +import { registerBidder } from '../src/adapters/bidderFactory.js'; +import { NATIVE, BANNER } from '../src/mediaTypes.js'; +import * as utils from '../src/utils.js'; +import includes from 'core-js-pure/features/array/includes.js'; + +const BIDDER_CODE = 'adnow'; +const ENDPOINT = 'https://n.ads3-adnow.com/a'; + +/** + * @typedef {object} CommonBidData + * + * @property {string} requestId The specific BidRequest which this bid is aimed at. + * This should match the BidRequest.bidId which this Bid targets. + * @property {string} currency The currency code for the cpm value + * @property {number} cpm The bid price, in US cents per thousand impressions. + * @property {string} creativeId The id of ad content + * @property {number} ttl Time-to-live - how long (in seconds) Prebid can use this bid. + * @property {boolean} netRevenue Boolean defining whether the bid is Net or Gross. The default is true (Net). + * @property {object} [meta] Object for storing bid meta data + * @property {string} [meta.mediaType] banner or native + */ + +/** @type {BidderSpec} */ +export const spec = { + code: BIDDER_CODE, + supportedMediaTypes: [ NATIVE, BANNER ], + + /** + * @param {object} bid + * @return {boolean} + */ + isBidRequestValid(bid) { + if (!bid || !bid.params) return false; + + const codeId = parseInt(bid.params.codeId, 10); + if (!codeId) { + return false; + } + + const mediaType = bid.params.mediaType || NATIVE; + + return includes(this.supportedMediaTypes, mediaType); + }, + + /** + * @param {BidRequest[]} validBidRequests + * @param {*} bidderRequest + * @return {ServerRequest} + */ + buildRequests(validBidRequests, bidderRequest) { + return validBidRequests.map(req => { + const mediaType = this._isBannerRequest(req) ? BANNER : NATIVE; + const codeId = parseInt(req.params.codeId, 10); + + const data = { + Id: codeId, + mediaType: mediaType, + out: 'prebid', + d_user_agent: navigator.userAgent, + requestid: req.bidId + }; + + if (mediaType === BANNER) { + data.sizes = utils.parseSizesInput( + req.mediaTypes && req.mediaTypes.banner && req.mediaTypes.banner.sizes + ).join('|') + } else { + data.width = data.height = 200; + + let sizes = utils.deepAccess(req, 'mediaTypes.native.image.sizes', []); + + if (sizes.length > 0) { + const size = Array.isArray(sizes[0]) ? sizes[0] : sizes; + + data.width = size[0] || data.width; + data.height = size[1] || data.height; + } + } + + /** @type {ServerRequest} */ + return { + method: 'GET', + url: ENDPOINT, + data: utils.parseQueryStringParameters(data), + options: { + withCredentials: false, + crossOrigin: true + }, + bidRequest: req + }; + }); + }, + + /** + * @param {*} response + * @param {ServerRequest} request + * @return {Bid[]} + */ + interpretResponse(response, request) { + const bidObj = request.bidRequest; + let bid = response.body; + + if (!bid || !bid.currency || !bid.cpm) { + return []; + } + + const mediaType = bid.meta.mediaType || NATIVE; + if (!includes(this.supportedMediaTypes, mediaType)) { + return []; + } + + bid.requestId = bidObj.bidId; + + if (mediaType === BANNER) { + return [ this._getBannerBid(bid) ]; + } + + if (mediaType === NATIVE) { + return [ this._getNativeBid(bid) ]; + } + + return []; + }, + + /** + * @private + * @param {object} bid + * @return {CommonBidData} + */ + _commonBidData(bid) { + return { + requestId: bid.requestId, + currency: bid.currency || 'USD', + cpm: bid.cpm || 0.00, + creativeId: bid.creativeId || 'undefined-creative', + netRevenue: bid.netRevenue || true, + ttl: bid.ttl || 360, + meta: bid.meta || {} + }; + }, + + /** + * @param {BidRequest} req + * @return {boolean} + * @private + */ + _isBannerRequest(req) { + return !!(req.mediaTypes && req.mediaTypes.banner); + }, + + /** + * @private + * @param {object} bid + * @return {Bid} + */ + _getBannerBid(bid) { + return { + ...this._commonBidData(bid), + width: bid.width || 300, + height: bid.height || 250, + ad: bid.ad || '
Empty Ad
' + }; + }, + + /** + * @private + * @param {object} bid + * @return {Bid} + */ + _getNativeBid(bid) { + return { + ...this._commonBidData(bid), + native: bid.native || {} + }; + } +} + +registerBidder(spec); diff --git a/modules/adnowBidAdapter.md b/modules/adnowBidAdapter.md new file mode 100644 index 00000000000..9ad99a67fc5 --- /dev/null +++ b/modules/adnowBidAdapter.md @@ -0,0 +1,46 @@ +# Overview + +``` +Module Name: AdNow Bidder Adapter +Module Type: Bidder Adapter +Maintainer: support@adnow.com +``` + +# Description + +AdNow Bidder Adapter for Prebid.js. +Banner and Native format are supported. +Please use ```adnow``` as the bidder code. + +# Test Parameters +```javascript + const adUnits = [{ + code: 'test', + mediaTypes: { + banner: { + sizes: [[300, 250]] + } + }, + bids: [{ + bidder: 'adnow', + params: { + codeId: 794934 + } + }] + }, { + code: 'test', + mediaTypes: { + native: { + image: { + sizes: [200, 200] + } + } + }, + bids: [{ + bidder: 'adnow', + params: { + codeId: 794934 + } + }] + }]; +``` diff --git a/modules/adotBidAdapter.js b/modules/adotBidAdapter.js index 911da416cfe..54bd9156b48 100644 --- a/modules/adotBidAdapter.js +++ b/modules/adotBidAdapter.js @@ -1,14 +1,27 @@ import {Renderer} from '../src/Renderer.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; import {BANNER, NATIVE, VIDEO} from '../src/mediaTypes.js'; -import {isStr, isArray, isNumber, isPlainObject, isBoolean, logError} from '../src/utils.js'; +import {isStr, isArray, isNumber, isPlainObject, isBoolean, logError, replaceAuctionPrice} from '../src/utils.js'; import find from 'core-js-pure/features/array/find.js'; +import { config } from '../src/config.js'; const ADAPTER_VERSION = 'v1.0.0'; const BID_METHOD = 'POST'; const BIDDER_URL = 'https://dsp.adotmob.com/headerbidding/bidrequest'; +const FIRST_PRICE = 1; +const NET_REVENUE = true; +// eslint-disable-next-line no-template-curly-in-string +const AUCTION_PRICE = '${AUCTION_PRICE}'; +const TTL = 10; + const SUPPORTED_VIDEO_CONTEXTS = ['instream', 'outstream']; const SUPPORTED_INSTREAM_CONTEXTS = ['pre-roll', 'mid-roll', 'post-roll']; +const SUPPORTED_VIDEO_MIMES = ['video/mp4']; +const BID_SUPPORTED_MEDIA_TYPES = ['banner', 'video', 'native']; + +const DOMAIN_REGEX = new RegExp('//([^/]*)'); +const OUTSTREAM_VIDEO_PLAYER_URL = 'https://adserver.adotmob.com/video/player.min.js'; + const NATIVE_PLACEMENTS = { title: {id: 1, name: 'title'}, icon: {id: 2, type: 1, name: 'img'}, @@ -18,15 +31,9 @@ const NATIVE_PLACEMENTS = { cta: {id: 6, type: 12, name: 'data'} }; const NATIVE_ID_MAPPING = {1: 'title', 2: 'icon', 3: 'image', 4: 'sponsoredBy', 5: 'body', 6: 'cta'}; -const SUPPORTED_VIDEO_MIMES = ['video/mp4']; -const DOMAIN_REGEX = new RegExp('//([^/]*)'); -const FIRST_PRICE = 1; -const BID_SUPPORTED_MEDIA_TYPES = ['banner', 'video', 'native']; -const TTL = 10; -const NET_REVENUE = true; -// eslint-disable-next-line no-template-curly-in-string -const AUCTION_PRICE = '${AUCTION_PRICE}'; -const OUTSTREAM_VIDEO_PLAYER_URL = 'https://adserver.adotmob.com/video/player.min.js'; +const NATIVE_PRESET_FORMATTERS = { + image: formatNativePresetImage +} function isNone(value) { return (value === null) || (value === undefined); @@ -183,6 +190,7 @@ function generateImpressionsFromAdUnit(acc, adUnit) { const {bidId, mediaTypes, params} = adUnit; const {placementId} = params; const pmp = {}; + const ext = {placementId}; if (placementId) pmp.deals = [{id: placementId}] @@ -193,8 +201,8 @@ function generateImpressionsFromAdUnit(acc, adUnit) { const impId = `${bidId}_${index}`; if (mediaType === 'banner') return acc.concat(generateBannerFromAdUnit(impId, data, params)); - if (mediaType === 'video') return acc.concat({id: impId, video: generateVideoFromAdUnit(data, params), pmp}); - if (mediaType === 'native') return acc.concat({id: impId, native: generateNativeFromAdUnit(data, params), pmp}); + if (mediaType === 'video') return acc.concat({id: impId, video: generateVideoFromAdUnit(data, params), pmp, ext}); + if (mediaType === 'native') return acc.concat({id: impId, native: generateNativeFromAdUnit(data, params), pmp, ext}); }, []); return acc.concat(imps); @@ -208,10 +216,11 @@ function generateBannerFromAdUnit(impId, data, params) { const {position, placementId} = params; const pos = position || 0; const pmp = {}; + const ext = {placementId}; if (placementId) pmp.deals = [{id: placementId}] - return data.sizes.map(([w, h], index) => ({id: `${impId}_${index}`, banner: {format: [{w, h}], w, h, pos}, pmp})); + return data.sizes.map(([w, h], index) => ({id: `${impId}_${index}`, banner: {format: [{w, h}], w, h, pos}, pmp, ext})); } function generateVideoFromAdUnit(data, params) { @@ -254,19 +263,31 @@ function computeStartDelay(data, params) { } function generateNativeFromAdUnit(data, params) { - const placements = NATIVE_PLACEMENTS; + const {type} = data; + const presetFormatter = type && NATIVE_PRESET_FORMATTERS[data.type]; + const nativeFields = presetFormatter ? presetFormatter(data) : data; + const assets = Object - .keys(data) + .keys(nativeFields) .reduce((acc, placement) => { - const placementData = data[placement]; - const assetInfo = placements[placement]; + const placementData = nativeFields[placement]; + const assetInfo = NATIVE_PLACEMENTS[placement]; if (!assetInfo) return acc; const {id, name, type} = assetInfo; - const {required, len, sizes} = placementData; - const wmin = sizes && sizes[0]; - const hmin = sizes && sizes[1]; + const {required, len, sizes = []} = placementData; + let wmin; + let hmin; + + if (isArray(sizes[0])) { + wmin = sizes[0][0]; + hmin = sizes[0][1]; + } else { + wmin = sizes[0]; + hmin = sizes[1]; + } + const content = {}; if (type) content.type = type; @@ -284,17 +305,47 @@ function generateNativeFromAdUnit(data, params) { }; } +function formatNativePresetImage(data) { + const sizes = data.sizes; + + return { + image: { + required: true, + sizes + }, + title: { + required: true + }, + sponsoredBy: { + required: true + }, + body: { + required: false + }, + cta: { + required: false + }, + icon: { + required: false + } + }; +} + function generateSiteFromAdUnitContext(adUnitContext) { if (!adUnitContext || !adUnitContext.refererInfo) return null; const domain = extractSiteDomainFromURL(adUnitContext.refererInfo.referer); + const publisherId = config.getConfig('adot.publisherId'); if (!domain) return null; return { page: adUnitContext.refererInfo.referer, domain: domain, - name: domain + name: domain, + publisher: { + id: publisherId + } }; } @@ -448,7 +499,7 @@ function generateAdFromBid(bid, bidResponse, serverRequest) { mediaType: bid.ext.adot.media_type, }; - if (isBidANative(bid)) return {...base, native: formatNativeData(bid.adm)}; + if (isBidANative(bid)) return {...base, native: formatNativeData(bid)}; const size = getSizeFromBid(bid, impressionData); const creative = getCreativeFromBid(bid, impressionData); @@ -465,7 +516,7 @@ function generateAdFromBid(bid, bidResponse, serverRequest) { }; } -function formatNativeData(adm) { +function formatNativeData({adm, price}) { const parsedAdm = tryParse(adm); const {assets, link: {url, clicktrackers}, imptrackers, jstracker} = parsedAdm.native; const placements = NATIVE_PLACEMENTS; @@ -480,7 +531,7 @@ function formatNativeData(adm) { }, { clickUrl: url, clickTrackers: clicktrackers, - impressionTrackers: imptrackers, + impressionTrackers: imptrackers && imptrackers.map(impTracker => replaceAuctionPrice(impTracker, price)), javascriptTrackers: jstracker && [jstracker] }); } @@ -503,10 +554,11 @@ function getSizeFromBid(bid, impressionData) { function getCreativeFromBid(bid, impressionData) { const shouldUseAdMarkup = !!bid.adm; + const price = bid.price; return { - markup: shouldUseAdMarkup ? bid.adm : null, - markupUrl: !shouldUseAdMarkup ? bid.nurl : null, + markup: shouldUseAdMarkup ? replaceAuctionPrice(bid.adm, price) : null, + markupUrl: !shouldUseAdMarkup ? replaceAuctionPrice(bid.nurl, price) : null, renderer: getRendererFromBid(bid, impressionData) }; } diff --git a/modules/adotBidAdapter.md b/modules/adotBidAdapter.md index 88c8fb0b936..e1388311e23 100644 --- a/modules/adotBidAdapter.md +++ b/modules/adotBidAdapter.md @@ -114,39 +114,37 @@ const adUnit = { code: 'test-div', mediaTypes: { native: { - native: { - image: { - // Field required status - required: false, - // Image dimensions supported by the native ad unit. - // Each ad unit size is formatted as follows: [width, height]. - sizes: [100, 50] - }, - title: { - // Field required status - required: false, - // Maximum length of the title - len: 140 - }, - sponsoredBy: { - // Field required status - required: false - }, - clickUrl: { - // Field required status - required: false - }, - body: { - // Field required status - required: false - }, - icon: { - // Field required status - required: false, - // Icon dimensions supported by the native ad unit. - // Each ad unit size is formatted as follows: [width, height]. - sizes: [50, 50] - } + image: { + // Field required status + required: false, + // Image dimensions supported by the native ad unit. + // Each ad unit size is formatted as follows: [width, height]. + sizes: [100, 50] + }, + title: { + // Field required status + required: false, + // Maximum length of the title + len: 140 + }, + sponsoredBy: { + // Field required status + required: false + }, + clickUrl: { + // Field required status + required: false + }, + body: { + // Field required status + required: false + }, + icon: { + // Field required status + required: false, + // Icon dimensions supported by the native ad unit. + // Each ad unit size is formatted as follows: [width, height]. + sizes: [50, 50] } } }, @@ -216,3 +214,20 @@ const adUnit = { }] } ``` + +### PublisherId + +You can set a publisherId using `pbjs.setBidderConfig` for the bidder `adot` + +#### Example + +```javascript +pbjs.setBidderConfig({ + bidders: ['adot'], + config: { + adot: { + publisherId: '__MY_PUBLISHER_ID__' + } + } +}); +``` \ No newline at end of file diff --git a/modules/adrelevantisBidAdapter.js b/modules/adrelevantisBidAdapter.js new file mode 100644 index 00000000000..5da941c65ca --- /dev/null +++ b/modules/adrelevantisBidAdapter.js @@ -0,0 +1,603 @@ +import { Renderer } from '../src/Renderer.js'; +import * as utils from '../src/utils.js'; +import { config } from '../src/config.js'; +import { registerBidder } from '../src/adapters/bidderFactory.js'; +import { BANNER, NATIVE, VIDEO } from '../src/mediaTypes.js'; +import find from 'core-js-pure/features/array/find.js'; +import includes from 'core-js-pure/features/array/includes.js'; +import { OUTSTREAM, INSTREAM } from '../src/video.js'; + +const BIDDER_CODE = 'adrelevantis'; +const URL = 'https://ssp.adrelevantis.com/prebid'; +const VIDEO_TARGETING = ['id', 'mimes', 'minduration', 'maxduration', + 'startdelay', 'skippable', 'playback_method', 'frameworks']; +const USER_PARAMS = ['age', 'externalUid', 'segments', 'gender', 'dnt', 'language']; +const APP_DEVICE_PARAMS = ['geo', 'device_id']; // appid is collected separately +const SOURCE = 'pbjs'; +const MAX_IMPS_PER_REQUEST = 15; + +const NATIVE_MAPPING = { + body: 'description', + body2: 'desc2', + cta: 'ctatext', + image: { + serverName: 'main_image', + requiredParams: { required: true } + }, + icon: { + serverName: 'icon', + requiredParams: { required: true } + }, + sponsoredBy: 'sponsored_by', + privacyLink: 'privacy_link', + salePrice: 'saleprice', + displayUrl: 'displayurl' +}; + +export const spec = { + code: BIDDER_CODE, + aliases: ['adr', 'adsmart', 'compariola'], + 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: function(bid) { + return !!(bid.params.placementId); + }, + + /** + * Make a server request from the list of BidRequests. + * + * @param {BidRequest[]} bidRequests A non-empty list of bid requests which should be sent to the Server. + * @return ServerRequest Info describing the request to the server. + */ + buildRequests: function(bidRequests, bidderRequest) { + const tags = bidRequests.map(bidToTag); + const userObjBid = find(bidRequests, hasUserInfo); + let userObj; + if (config.getConfig('coppa') === true) { + userObj = {'coppa': true}; + } + if (userObjBid) { + userObj = {}; + Object.keys(userObjBid.params.user) + .filter(param => includes(USER_PARAMS, param)) + .forEach(param => userObj[param] = userObjBid.params.user[param]); + } + + const appDeviceObjBid = find(bidRequests, hasAppDeviceInfo); + let appDeviceObj; + if (appDeviceObjBid && appDeviceObjBid.params && appDeviceObjBid.params.app) { + appDeviceObj = {}; + Object.keys(appDeviceObjBid.params.app) + .filter(param => includes(APP_DEVICE_PARAMS, param)) + .forEach(param => appDeviceObj[param] = appDeviceObjBid.params.app[param]); + } + + const appIdObjBid = find(bidRequests, hasAppId); + let appIdObj; + if (appIdObjBid && appIdObjBid.params && appDeviceObjBid.params.app && appDeviceObjBid.params.app.id) { + appIdObj = { + appid: appIdObjBid.params.app.id + }; + } + + const payload = { + tags: [...tags], + user: userObj, + sdk: { + source: SOURCE, + version: '$prebid.version$' + } + }; + + if (appDeviceObjBid) { + payload.device = appDeviceObj + } + if (appIdObjBid) { + payload.app = appIdObj; + } + + if (bidderRequest && bidderRequest.gdprConsent) { + // note - objects for impbus use underscore instead of camelCase + payload.gdpr_consent = { + consent_string: bidderRequest.gdprConsent.consentString, + consent_required: bidderRequest.gdprConsent.gdprApplies + }; + } + + if (bidderRequest && bidderRequest.refererInfo) { + let refererinfo = { + rd_ref: encodeURIComponent(bidderRequest.refererInfo.referer), + rd_top: bidderRequest.refererInfo.reachedTop, + rd_ifs: bidderRequest.refererInfo.numIframes, + rd_stk: bidderRequest.refererInfo.stack.map((url) => encodeURIComponent(url)).join(',') + } + payload.referrer_detection = refererinfo; + } + + let fpdcfg = config.getConfig('fpd') + if (fpdcfg && fpdcfg.context) { + let fdata = { + keywords: fpdcfg.context.keywords, + category: fpdcfg.context.data.category + } + payload.fpd = fdata; + } + + const request = formatRequest(payload, bidderRequest); + return request; + }, + + /** + * Unpack the response from the server into a list of bids. + * + * @param {*} serverResponse A successful response from the server. + * @return {Bid[]} An array of bids which were nested inside the server. + */ + interpretResponse: function(serverResponse, {bidderRequest}) { + serverResponse = serverResponse.body; + const bids = []; + if (!serverResponse || serverResponse.error) { + let errorMessage = `in response for ${bidderRequest.bidderCode} adapter`; + if (serverResponse && serverResponse.error) { errorMessage += `: ${serverResponse.error}`; } + utils.logError(errorMessage); + return bids; + } + + if (serverResponse.tags) { + serverResponse.tags.forEach(serverBid => { + const rtbBid = getRtbBid(serverBid); + if (rtbBid) { + if (rtbBid.cpm !== 0 && includes(this.supportedMediaTypes, rtbBid.ad_type)) { + const bid = newBid(serverBid, rtbBid, bidderRequest); + bid.mediaType = parseMediaType(rtbBid); + bids.push(bid); + } + } + }); + } + + return bids; + }, + + transformBidParams: function(params, isOpenRtb) { + params = utils.convertTypes({ + 'placementId': 'number', + 'keywords': utils.transformBidderParamKeywords + }, params); + + if (isOpenRtb) { + params.use_pmt_rule = (typeof params.usePaymentRule === 'boolean') ? params.usePaymentRule : false; + if (params.usePaymentRule) { delete params.usePaymentRule; } + + if (isPopulatedArray(params.keywords)) { + params.keywords.forEach(deleteValues); + } + + Object.keys(params).forEach(paramKey => { + let convertedKey = utils.convertCamelToUnderscore(paramKey); + if (convertedKey !== paramKey) { + params[convertedKey] = params[paramKey]; + delete params[paramKey]; + } + }); + } + + return params; + } +} + +function isPopulatedArray(arr) { + return !!(utils.isArray(arr) && arr.length > 0); +} + +function deleteValues(keyPairObj) { + if (isPopulatedArray(keyPairObj.value) && keyPairObj.value[0] === '') { + delete keyPairObj.value; + } +} + +function formatRequest(payload, bidderRequest) { + let request = []; + + if (payload.tags.length > MAX_IMPS_PER_REQUEST) { + const clonedPayload = utils.deepClone(payload); + + utils.chunk(payload.tags, MAX_IMPS_PER_REQUEST).forEach(tags => { + clonedPayload.tags = tags; + const payloadString = JSON.stringify(clonedPayload); + request.push({ + method: 'POST', + url: URL, + data: payloadString, + bidderRequest + }); + }); + } else { + const payloadString = JSON.stringify(payload); + request = { + method: 'POST', + url: URL, + data: payloadString, + bidderRequest + }; + } + + return request; +} + +function newRenderer(adUnitCode, rtbBid, rendererOptions = {}) { + const renderer = Renderer.install({ + id: rtbBid.renderer_id, + url: rtbBid.renderer_url, + config: rendererOptions, + loaded: false, + adUnitCode + }); + + try { + renderer.setRender(outstreamRender); + } catch (err) { + utils.logWarn('Prebid Error calling setRender on renderer', err); + } + + renderer.setEventHandlers({ + impression: () => utils.logMessage('AdRelevantis outstream video impression event'), + loaded: () => utils.logMessage('AdRelevantis outstream video loaded event'), + ended: () => { + utils.logMessage('AdRelevantis outstream renderer video event'); + document.querySelector(`#${adUnitCode}`).style.display = 'none'; + } + }); + return renderer; +} + +/** + * This function hides google div container for outstream bids to remove unwanted space on page. Appnexus renderer creates a new iframe outside of google iframe to render the outstream creative. + * @param {string} elementId element id + */ +function hidedfpContainer(elementId) { + var el = document.getElementById(elementId).querySelectorAll("div[id^='google_ads']"); + if (el[0]) { + el[0].style.setProperty('display', 'none'); + } +} + +function outstreamRender(bid) { + // push to render queue because ANOutstreamVideo may not be loaded yet + hidedfpContainer(bid.adUnitCode); + bid.renderer.push(() => { + window.ANOutstreamVideo.renderAd({ + tagId: bid.adResponse.tag_id, + sizes: [bid.getSize().split('x')], + targetId: bid.adUnitCode, // target div id to render video + uuid: bid.adResponse.uuid, + adResponse: bid.adResponse, + rendererOptions: bid.renderer.getConfig() + }, handleOutstreamRendererEvents.bind(null, bid)); + }); +} + +function handleOutstreamRendererEvents(bid, id, eventName) { + bid.renderer.handleVideoEvent({ id, eventName }); +} + +/** + * Unpack the Server's Bid into a Prebid-compatible one. + * @param serverBid + * @param rtbBid + * @param bidderRequest + * @return Bid + */ +function newBid(serverBid, rtbBid, bidderRequest) { + const bidRequest = utils.getBidRequest(serverBid.uuid, [bidderRequest]); + const bid = { + requestId: serverBid.uuid, + cpm: rtbBid.cpm, + creativeId: rtbBid.creative_id, + dealId: rtbBid.deal_id, + currency: 'USD', + netRevenue: true, + ttl: 300, + adUnitCode: bidRequest.adUnitCode, + adrelevantis: { + buyerMemberId: rtbBid.buyer_member_id, + dealPriority: rtbBid.deal_priority, + dealCode: rtbBid.deal_code + } + }; + + if (rtbBid.advertiser_id) { + bid.meta = Object.assign({}, bid.meta, { advertiserId: rtbBid.advertiser_id }); + } + + if (rtbBid.rtb.video) { + Object.assign(bid, { + width: rtbBid.rtb.video.player_width, + height: rtbBid.rtb.video.player_height, + vastImpUrl: rtbBid.notify_url, + ttl: 3600 + }); + + const videoContext = utils.deepAccess(bidRequest, 'mediaTypes.video.context'); + switch (videoContext) { + case OUTSTREAM: + bid.adResponse = serverBid; + bid.adResponse.ad = bid.adResponse.ads[0]; + bid.adResponse.ad.video = bid.adResponse.ad.rtb.video; + bid.vastXml = rtbBid.rtb.video.content; + + if (rtbBid.renderer_url) { + const videoBid = find(bidderRequest.bids, bid => bid.bidId === serverBid.uuid); + const rendererOptions = utils.deepAccess(videoBid, 'renderer.options'); + bid.renderer = newRenderer(bid.adUnitCode, rtbBid, rendererOptions); + } + break; + case INSTREAM: + bid.vastUrl = rtbBid.notify_url + '&redir=' + encodeURIComponent(rtbBid.rtb.video.asset_url); + break; + } + } else if (rtbBid.rtb[NATIVE]) { + const nativeAd = rtbBid.rtb[NATIVE]; + + // setting up the jsTracker: + // we put it as a data-src attribute so that the tracker isn't called + // until we have the adId (see onBidWon) + let jsTrackerDisarmed = rtbBid.viewability.config.replace('src=', 'data-src='); + + let jsTrackers = nativeAd.javascript_trackers; + + if (jsTrackers == undefined) { + jsTrackers = jsTrackerDisarmed; + } else if (utils.isStr(jsTrackers)) { + jsTrackers = [jsTrackers, jsTrackerDisarmed]; + } else { + jsTrackers.push(jsTrackerDisarmed); + } + + bid[NATIVE] = { + title: nativeAd.title, + body: nativeAd.desc, + body2: nativeAd.desc2, + cta: nativeAd.ctatext, + rating: nativeAd.rating, + sponsoredBy: nativeAd.sponsored, + privacyLink: nativeAd.privacy_link, + address: nativeAd.address, + downloads: nativeAd.downloads, + likes: nativeAd.likes, + phone: nativeAd.phone, + price: nativeAd.price, + salePrice: nativeAd.saleprice, + clickUrl: nativeAd.link.url, + displayUrl: nativeAd.displayurl, + clickTrackers: nativeAd.link.click_trackers, + impressionTrackers: nativeAd.impression_trackers, + javascriptTrackers: jsTrackers + }; + if (nativeAd.main_img) { + bid['native'].image = { + url: nativeAd.main_img.url, + height: nativeAd.main_img.height, + width: nativeAd.main_img.width, + }; + } + if (nativeAd.icon) { + bid['native'].icon = { + url: nativeAd.icon.url, + height: nativeAd.icon.height, + width: nativeAd.icon.width, + }; + } + } else { + Object.assign(bid, { + width: rtbBid.rtb.banner.width, + height: rtbBid.rtb.banner.height, + ad: rtbBid.rtb.banner.content + }); + try { + const url = rtbBid.rtb.trackers[0].impression_urls[0]; + const tracker = utils.createTrackPixelHtml(url); + bid.ad += tracker; + } catch (error) { + utils.logError('Error appending tracking pixel', error); + } + } + + return bid; +} + +function bidToTag(bid) { + const tag = {}; + tag.sizes = transformSizes(bid.sizes); + tag.primary_size = tag.sizes[0]; + tag.ad_types = []; + tag.uuid = bid.bidId; + if (bid.params.placementId) { + tag.id = parseInt(bid.params.placementId, 10); + } + if (bid.params.cpm) { + tag.cpm = bid.params.cpm; + } + tag.allow_smaller_sizes = bid.params.allowSmallerSizes || false; + tag.use_pmt_rule = bid.params.usePaymentRule || false + tag.prebid = true; + tag.disable_psa = true; + if (bid.params.reserve) { + tag.reserve = bid.params.reserve; + } + if (bid.params.position) { + tag.position = {'above': 1, 'below': 2}[bid.params.position] || 0; + } + if (bid.params.trafficSourceCode) { + tag.traffic_source_code = bid.params.trafficSourceCode; + } + if (bid.params.privateSizes) { + tag.private_sizes = transformSizes(bid.params.privateSizes); + } + if (bid.params.supplyType) { + tag.supply_type = bid.params.supplyType; + } + if (bid.params.pubClick) { + tag.pubclick = bid.params.pubClick; + } + if (bid.params.extInvCode) { + tag.ext_inv_code = bid.params.extInvCode; + } + if (bid.params.externalImpId) { + tag.external_imp_id = bid.params.externalImpId; + } + if (!utils.isEmpty(bid.params.keywords)) { + let keywords = utils.transformBidderParamKeywords(bid.params.keywords); + + if (keywords.length > 0) { + keywords.forEach(deleteValues); + } + tag.keywords = keywords; + } + if (bid.params.category) { + tag.category = bid.params.category; + } + + if (bid.mediaType === NATIVE || utils.deepAccess(bid, `mediaTypes.${NATIVE}`)) { + tag.ad_types.push(NATIVE); + if (tag.sizes.length === 0) { + tag.sizes = transformSizes([1, 1]); + } + + if (bid.nativeParams) { + const nativeRequest = buildNativeRequest(bid.nativeParams); + tag[NATIVE] = {layouts: [nativeRequest]}; + } + } + + const videoMediaType = utils.deepAccess(bid, `mediaTypes.${VIDEO}`); + const context = utils.deepAccess(bid, 'mediaTypes.video.context'); + + tag.hb_source = 1; + if (bid.mediaType === VIDEO || videoMediaType) { + tag.ad_types.push(VIDEO); + } + + // instream gets vastUrl, outstream gets vastXml + if (bid.mediaType === VIDEO || (videoMediaType && context !== 'outstream')) { + tag.require_asset_url = true; + } + + if (bid.params.video) { + tag.video = {}; + // place any valid video params on the tag + Object.keys(bid.params.video) + .filter(param => includes(VIDEO_TARGETING, param)) + .forEach(param => tag.video[param] = bid.params.video[param]); + } + + if (bid.renderer) { + tag.video = Object.assign({}, tag.video, {custom_renderer_present: true}); + } + + if ( + (utils.isEmpty(bid.mediaType) && utils.isEmpty(bid.mediaTypes)) || + (bid.mediaType === BANNER || (bid.mediaTypes && bid.mediaTypes[BANNER])) + ) { + tag.ad_types.push(BANNER); + } + + return tag; +} + +/* Turn bid request sizes into ut-compatible format */ +function transformSizes(requestSizes) { + let sizes = []; + let sizeObj = {}; + + if (utils.isArray(requestSizes) && requestSizes.length === 2 && + !utils.isArray(requestSizes[0])) { + sizeObj.width = parseInt(requestSizes[0], 10); + sizeObj.height = parseInt(requestSizes[1], 10); + sizes.push(sizeObj); + } else if (typeof requestSizes === 'object') { + for (let i = 0; i < requestSizes.length; i++) { + let size = requestSizes[i]; + sizeObj = {}; + sizeObj.width = parseInt(size[0], 10); + sizeObj.height = parseInt(size[1], 10); + sizes.push(sizeObj); + } + } + + return sizes; +} + +function hasUserInfo(bid) { + return !!bid.params.user; +} + +function hasAppDeviceInfo(bid) { + if (bid.params) { + return !!bid.params.app + } +} + +function hasAppId(bid) { + if (bid.params && bid.params.app) { + return !!bid.params.app.id + } + return !!bid.params.app +} + +function getRtbBid(tag) { + return tag && tag.ads && tag.ads.length && find(tag.ads, ad => ad.rtb); +} + +function buildNativeRequest(params) { + const request = {}; + + // map standard prebid native asset identifier to /ut parameters + // e.g., tag specifies `body` but /ut only knows `description`. + // mapping may be in form {tag: ''} or + // {tag: {serverName: '', requiredParams: {...}}} + Object.keys(params).forEach(key => { + // check if one of the forms is used, otherwise + // a mapping wasn't specified so pass the key straight through + const requestKey = + (NATIVE_MAPPING[key] && NATIVE_MAPPING[key].serverName) || + NATIVE_MAPPING[key] || + key; + + // required params are always passed on request + const requiredParams = NATIVE_MAPPING[key] && NATIVE_MAPPING[key].requiredParams; + request[requestKey] = Object.assign({}, requiredParams, params[key]); + + // convert the sizes of image/icon assets to proper format (if needed) + const isImageAsset = !!(requestKey === NATIVE_MAPPING.image.serverName || requestKey === NATIVE_MAPPING.icon.serverName); + if (isImageAsset && request[requestKey].sizes) { + let sizes = request[requestKey].sizes; + if (utils.isArrayOfNums(sizes) || (utils.isArray(sizes) && sizes.length > 0 && sizes.every(sz => utils.isArrayOfNums(sz)))) { + request[requestKey].sizes = transformSizes(request[requestKey].sizes); + } + } + + if (requestKey === NATIVE_MAPPING.privacyLink) { + request.privacy_supported = true; + } + }); + + return request; +} + +function parseMediaType(rtbBid) { + const adType = rtbBid.ad_type; + if (adType === VIDEO) { + return VIDEO; + } else { + return BANNER; + } +} + +registerBidder(spec); diff --git a/modules/adrelevantisBidAdapter.md b/modules/adrelevantisBidAdapter.md new file mode 100644 index 00000000000..a60a47508ff --- /dev/null +++ b/modules/adrelevantisBidAdapter.md @@ -0,0 +1,120 @@ +# Overview + +``` +Module Name: Adrelevantis Bid Adapter +Module Type: Bidder Adapter +Maintainer: info@adrelevantis.com +``` + +# Description + +Connects to Adrelevantis exchange for bids. + +Adrelevantis bid adapter supports Banner, Video (outstream) and Native. + +# Test Parameters +``` +var adUnits = [ + // Banner adUnit + { + code: 'banner-div', + mediaTypes: { + banner: { + sizes: [[300, 250], [300,600]] + } + }, + bids: [{ + bidder: 'adrelevantis', + params: { + placementId: 13144370, + cpm: 0.50 + } + }] + }, + // Native adUnit + { + code: 'native-div', + sizes: [[1, 1]], + mediaTypes: { + native: { + title: { + required: true + }, + body: { + required: true + }, + image: { + required: true + }, + sponsoredBy: { + required: true + }, + icon: { + required: false + } + } + }, + bids: [{ + bidder: 'adrelevantis', + params: { + placementId: 13232354, + allowSmallerSizes: true + } + }] + }, + // Video outstream adUnit + { + code: 'video-outstream', + mediaTypes: { + video: { + playerSize: [[640, 360]], + context: 'outstream' + } + }, + bids: [ + { + bidder: 'adrelevantis', + params: { + placementId: 13232385, + video: { + skippable: true, + playback_method: ['auto_play_sound_off'] + } + } + } + ] + }, + + // Banner adUnit in a App Webview + // Only use this for situations where prebid.js is in a webview of an App + // See Prebid Mobile for displaying ads via an SDK + { + code: 'banner-div', + mediaTypes: { + banner: { + sizes: [[300, 250], [300,600]] + } + } + bids: [{ + bidder: 'adrelevantis', + params: { + placementId: 13144370, + app: { + id: "B1O2W3M4AN.com.prebid.webview", + geo: { + lat: 40.0964439, + lng: -75.3009142 + }, + device_id: { + idfa: "4D12078D-3246-4DA4-AD5E-7610481E7AE", // Apple advertising identifier + aaid: "38400000-8cf0-11bd-b23e-10b96e40000d", // Android advertising identifier + md5udid: "5756ae9022b2ea1e47d84fead75220c8", // MD5 hash of the ANDROID_ID + sha1udid: "4DFAA92388699AC6539885AEF1719293879985BF", // SHA1 hash of the ANDROID_ID + windowsadid: "750c6be243f1c4b5c9912b95a5742fc5" // Windows advertising identifier + } + } + } + }] + } +]; +``` diff --git a/modules/adtelligentBidAdapter.js b/modules/adtelligentBidAdapter.js index e22aafb73fc..b2e37767a1e 100644 --- a/modules/adtelligentBidAdapter.js +++ b/modules/adtelligentBidAdapter.js @@ -6,12 +6,31 @@ import { Renderer } from '../src/Renderer.js'; import find from 'core-js-pure/features/array/find.js'; const subdomainSuffixes = ['', 1, 2]; -const getUri = (function () { - let num = 0; - return function () { - return 'https://ghb' + subdomainSuffixes[num++ % subdomainSuffixes.length] + '.adtelligent.com/v2/auction/' +const AUCTION_PATH = '/v2/auction/'; +const PROTOCOL = 'https://'; +const HOST_GETTERS = { + default: (function () { + let num = 0; + return function () { + return 'ghb' + subdomainSuffixes[num++ % subdomainSuffixes.length] + '.adtelligent.com'; + } + }()), + appaloosa: function () { + return 'ghb.hb.appaloosa.media'; + }, + onefiftytwomedia: function() { + return 'ghb.ads.152media.com'; + }, + mediafuse: function() { + return 'ghb.hbmp.mediafuse.com'; } -}()) + +} +const getUri = function (bidderCode) { + let bidderWithoutSuffix = bidderCode.split('_')[0]; + let getter = HOST_GETTERS[bidderWithoutSuffix] || HOST_GETTERS['default']; + return PROTOCOL + getter() + AUCTION_PATH +} const OUTSTREAM_SRC = 'https://player.adtelligent.com/outstream-unit/2.01/outstream.min.js'; const BIDDER_CODE = 'adtelligent'; const OUTSTREAM = 'outstream'; @@ -21,7 +40,7 @@ const syncsCache = {}; export const spec = { code: BIDDER_CODE, gvlid: 410, - aliases: ['onefiftytwomedia', 'selectmedia'], + aliases: ['onefiftytwomedia', 'selectmedia', 'appaloosa', 'mediafuse'], supportedMediaTypes: [VIDEO, BANNER], isBidRequestValid: function (bid) { return !!utils.deepAccess(bid, 'params.aid'); @@ -82,7 +101,7 @@ export const spec = { data: Object.assign({}, tag, { BidRequests: bids }), adapterRequest, method: 'POST', - url: getUri() + url: getUri(adapterRequest.bidderCode) }; }) }, diff --git a/modules/adxcgBidAdapter.js b/modules/adxcgBidAdapter.js index 2d5c64dfe53..e61792288ed 100644 --- a/modules/adxcgBidAdapter.js +++ b/modules/adxcgBidAdapter.js @@ -170,8 +170,8 @@ export const spec = { beaconParams.tdid = validBidRequests[0].userId.tdid; } - if (utils.isStr(utils.deepAccess(validBidRequests, '0.userId.id5id'))) { - beaconParams.id5id = validBidRequests[0].userId.id5id; + if (utils.isStr(utils.deepAccess(validBidRequests, '0.userId.id5id.uid'))) { + beaconParams.id5id = validBidRequests[0].userId.id5id.uid; } if (utils.isStr(utils.deepAccess(validBidRequests, '0.userId.idl_env'))) { diff --git a/modules/adyoulikeBidAdapter.js b/modules/adyoulikeBidAdapter.js index 725ee0f5626..40d3cf84369 100644 --- a/modules/adyoulikeBidAdapter.js +++ b/modules/adyoulikeBidAdapter.js @@ -1,5 +1,6 @@ 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'; const VERSION = '1.0'; @@ -104,15 +105,6 @@ function getHostname(bidderRequest) { return ''; } -/* Get current page referrer url */ -function getReferrerUrl(bidderRequest) { - let referer = ''; - if (bidderRequest && bidderRequest.refererInfo) { - referer = bidderRequest.refererInfo.referer; - } - return referer; -} - /* Get current page canonical url */ function getCanonicalUrl() { let link; @@ -155,9 +147,14 @@ function createEndpoint(bidRequests, bidderRequest) { function createEndpointQS(bidderRequest) { const qs = {}; - const ref = getReferrerUrl(bidderRequest); - if (ref) { - qs.RefererUrl = encodeURIComponent(ref); + if (bidderRequest) { + const ref = bidderRequest.refererInfo; + if (ref) { + qs.RefererUrl = encodeURIComponent(ref.referer); + if (ref.numIframes > 0) { + qs.SafeFrame = true; + } + } } const can = getCanonicalUrl(); @@ -165,6 +162,11 @@ function createEndpointQS(bidderRequest) { qs.CanonicalUrl = encodeURIComponent(can); } + const domain = config.getConfig('publisherDomain'); + if (domain) { + qs.PublisherDomain = encodeURIComponent(domain); + } + return qs; } diff --git a/modules/amxBidAdapter.js b/modules/amxBidAdapter.js index 2e9529b633c..a1fa202c154 100644 --- a/modules/amxBidAdapter.js +++ b/modules/amxBidAdapter.js @@ -6,16 +6,28 @@ import { getStorageManager } from '../src/storageManager.js'; const BIDDER_CODE = 'amx'; const storage = getStorageManager(737, BIDDER_CODE); -const SIMPLE_TLD_TEST = /\.co\.\w{2,4}$/; +const SIMPLE_TLD_TEST = /\.com?\.\w{2,4}$/; const DEFAULT_ENDPOINT = 'https://prebid.a-mo.net/a/c'; -const VERSION = 'pba1.2'; +const VERSION = 'pba1.2.1'; const xmlDTDRxp = /^\s*<\?xml[^\?]+\?>/; const VAST_RXP = /^\s*<\??(?:vast|xml)/i; const TRACKING_ENDPOINT = 'https://1x1.a-mo.net/hbx/'; const AMUID_KEY = '__amuidpb'; -const getLocation = (request) => - parseUrl(deepAccess(request, 'refererInfo.canonicalUrl', location.href)) +function getLocation (request) { + const refInfo = request.refererInfo; + if (refInfo == null) { + return parseUrl(location.href); + } + + if (refInfo.isAmp && refInfo.referer != null) { + return parseUrl(refInfo.referer) + } + + const topUrl = refInfo.numIframes > 0 && refInfo.stack[0] != null + ? refInfo.stack[0] : location.href; + return parseUrl(topUrl); +}; const largestSize = (sizes, mediaTypes) => { const allSizes = sizes @@ -44,7 +56,7 @@ const nullOrType = (value, type) => function getID(loc) { const host = loc.hostname.split('.'); const short = host.slice( - host.length - (SIMPLE_TLD_TEST.test(loc.host) ? 3 : 2) + host.length - (SIMPLE_TLD_TEST.test(loc.hostname) ? 3 : 2) ).join('.'); return btoa(short).replace(/=+$/, ''); } @@ -239,7 +251,7 @@ export const spec = { gs: deepAccess(bidderRequest, 'gdprConsent.gdprApplies', ''), gc: deepAccess(bidderRequest, 'gdprConsent.consentString', ''), u: deepAccess(bidderRequest, 'refererInfo.canonicalUrl', loc.href), - do: loc.host, + do: loc.hostname, re: deepAccess(bidderRequest, 'refererInfo.referer'), am: getUIDSafe(), usp: bidderRequest.uspConsent || '1---', diff --git a/modules/andbeyondBidAdapter.md b/modules/andbeyondBidAdapter.md deleted file mode 100644 index 7d58bac0abc..00000000000 --- a/modules/andbeyondBidAdapter.md +++ /dev/null @@ -1,32 +0,0 @@ -# Overview - -``` -Module Name: andbeyond Bidder Adapter -Module Type: Bidder Adapter -Maintainer: shreyanschopra@rtbdemand.com -``` - -# Description - -Connects to andbeyond whitelabel platform. -Banner formats are supported. - - -# Test Parameters -``` - var adUnits = [ - { - code: 'banner-ad-div', - sizes: [[300, 250]], // banner size - bids: [ - { - bidder: 'andbeyond', - params: { - zoneId: '30164', //required parameter - host: 'cpm.metaadserving.com' //required parameter - } - } - ] - } - ]; -``` diff --git a/modules/aolBidAdapter.js b/modules/aolBidAdapter.js index 1f43231e495..c899da32340 100644 --- a/modules/aolBidAdapter.js +++ b/modules/aolBidAdapter.js @@ -30,6 +30,17 @@ const SYNC_TYPES = { } }; +const SUPPORTED_USER_ID_SOURCES = [ + 'adserver.org', + 'criteo.com', + 'id5-sync.com', + 'intentiq.com', + 'liveintent.com', + 'quantcast.com', + 'verizonmedia.com', + 'liveramp.com' +]; + const pubapiTemplate = template`${'host'}/pubapi/3.0/${'network'}/${'placement'}/${'pageid'}/${'sizeid'}/ADTECH;v=2;cmd=bid;cors=yes;alias=${'alias'};misc=${'misc'};${'dynamicParams'}`; const nexageBaseApiTemplate = template`${'host'}/bidRequest?`; const nexageGetApiTemplate = template`dcn=${'dcn'}&pos=${'pos'}&cmd=bid${'dynamicParams'}`; @@ -103,6 +114,12 @@ function resolveEndpointCode(bid) { } } +function getSupportedEids(bid) { + return bid.userIdAsEids.filter(eid => { + return SUPPORTED_USER_ID_SOURCES.indexOf(eid.source) !== -1 + }); +} + export const spec = { code: AOL_BIDDERS_CODES.AOL, gvlid: 25, @@ -226,6 +243,13 @@ export const spec = { }, buildOneMobileGetUrl(bid, consentData) { let { dcn, pos, ext } = bid.params; + if (typeof bid.userId === 'object') { + ext = ext || {}; + let eids = getSupportedEids(bid); + eids.forEach(eid => { + ext['eid' + eid.source] = eid.uids[0].id; + }); + } let nexageApi = this.buildOneMobileBaseUrl(bid); if (dcn && pos) { let dynamicParams = this.formatOneMobileDynamicParams(ext, consentData); @@ -292,6 +316,16 @@ export const spec = { utils.deepSetValue(openRtbObject, 'regs.ext.us_privacy', consentData.uspConsent); } + if (typeof bid.userId === 'object') { + openRtbObject.user = openRtbObject.user || {}; + openRtbObject.user.ext = openRtbObject.user.ext || {}; + + let eids = getSupportedEids(bid); + if (eids.length > 0) { + openRtbObject.user.ext.eids = eids + } + } + return openRtbObject; }, isEUConsentRequired(consentData) { diff --git a/modules/quantumdexBidAdapter.js b/modules/apacdexBidAdapter.js similarity index 94% rename from modules/quantumdexBidAdapter.js rename to modules/apacdexBidAdapter.js index 738b6165f79..2582e4788c1 100644 --- a/modules/quantumdexBidAdapter.js +++ b/modules/apacdexBidAdapter.js @@ -1,7 +1,11 @@ import * as utils from '../src/utils.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; -const BIDDER_CODE = 'quantumdex'; +const BIDDER_CODE = 'apacdex'; const CONFIG = { + 'apacdex': { + 'ENDPOINT': 'https://useast.quantumdex.io/auction/apacdex', + 'USERSYNC': 'https://sync.quantumdex.io/usersync/apacdex' + }, 'quantumdex': { 'ENDPOINT': 'https://useast.quantumdex.io/auction/quantumdex', 'USERSYNC': 'https://sync.quantumdex.io/usersync/quantumdex' @@ -12,14 +16,14 @@ const CONFIG = { } }; -var bidderConfig = CONFIG['quantumdex']; +var bidderConfig = CONFIG[BIDDER_CODE]; var bySlotTargetKey = {}; var bySlotSizesCount = {} export const spec = { code: BIDDER_CODE, supportedMediaTypes: ['banner', 'video'], - aliases: ['valueimpression'], + aliases: ['quantumdex', 'valueimpression'], isBidRequestValid: function (bid) { if (!bid.params) { return false; @@ -30,7 +34,7 @@ export const spec = { if (!utils.deepAccess(bid, 'mediaTypes.banner') && !utils.deepAccess(bid, 'mediaTypes.video')) { return false; } - if (utils.deepAccess(bid, 'mediaTypes.banner')) { // Quantumdex does not support multi type bids, favor banner over video + if (utils.deepAccess(bid, 'mediaTypes.banner')) { // Not support multi type bids, favor banner over video if (!utils.deepAccess(bid, 'mediaTypes.banner.sizes')) { // sizes at the banner is required. return false; diff --git a/modules/quantumdexBidAdapter.md b/modules/apacdexBidAdapter.md similarity index 56% rename from modules/quantumdexBidAdapter.md rename to modules/apacdexBidAdapter.md index 8c35ea8cb05..b88190cda94 100644 --- a/modules/quantumdexBidAdapter.md +++ b/modules/apacdexBidAdapter.md @@ -1,15 +1,15 @@ # Overview ``` -Module Name: Quantum Digital Exchange Bidder Adapter +Module Name: APAC Digital Exchange Bidder Adapter Module Type: Bidder Adapter -Maintainer: ken@quantumdex.io +Maintainer: ken@apacdex.com ``` # Description -Connects to Quantum Digital Exchange for bids. -Quantumdex bid adapter supports Banner and Video (Instream and Outstream) ads. +Connects to APAC Digital Exchange for bids. +Apacdex bid adapter supports Banner and Video (Instream and Outstream) ads. # Test Parameters ``` @@ -23,9 +23,9 @@ var adUnits = [ }, bids: [ { - bidder: 'quantumdex', + bidder: 'apacdex', params: { - siteId: 'quantumdex-site-id', // siteId provided by Quantumdex + siteId: 'apacdex1234', // siteId provided by Apacdex } } ] @@ -46,9 +46,9 @@ var videoAdUnit = { }, bids: [ { - bidder: 'quantumdex', + bidder: 'apacdex', params: { - siteId: 'quantumdex-site-id', // siteId provided by Quantumdex + siteId: 'apacdex1234', // siteId provided by Apacdex } } ] diff --git a/modules/appnexusBidAdapter.js b/modules/appnexusBidAdapter.js index 12bc6a8105c..3a9f37f4ca8 100644 --- a/modules/appnexusBidAdapter.js +++ b/modules/appnexusBidAdapter.js @@ -107,7 +107,19 @@ export const spec = { .filter(param => includes(USER_PARAMS, param)) .forEach((param) => { let uparam = utils.convertCamelToUnderscore(param); - userObj[uparam] = userObjBid.params.user[param] + if (param === 'segments' && utils.isArray(userObjBid.params.user[param])) { + let segs = []; + userObjBid.params.user[param].forEach(val => { + if (utils.isNumber(val)) { + segs.push({'id': val}); + } else if (utils.isPlainObject(val)) { + segs.push(val); + } + }); + userObj[uparam] = segs; + } else if (param !== 'segments') { + userObj[uparam] = userObjBid.params.user[param]; + } }); } @@ -157,6 +169,7 @@ export const spec = { const memberIdBid = find(bidRequests, hasMemberId); const member = memberIdBid ? parseInt(memberIdBid.params.member, 10) : 0; const schain = bidRequests[0].schain; + const omidSupport = find(bidRequests, hasOmidSupport); const payload = { tags: [...tags], @@ -168,6 +181,13 @@ export const spec = { schain: schain }; + if (omidSupport) { + payload['iab_support'] = { + omidpn: 'Appnexus', + omidpv: '$prebid.version$' + } + } + if (member > 0) { payload.member_id = member; } @@ -220,8 +240,8 @@ export const spec = { }); } - let eids = []; const criteoId = utils.deepAccess(bidRequests[0], `userId.criteoId`); + let eids = []; if (criteoId) { eids.push({ source: 'criteo.com', @@ -478,6 +498,12 @@ function formatRequest(payload, bidderRequest) { } } + if (utils.getParameterByName('apn_test').toUpperCase() === 'TRUE' || config.getConfig('apn_test') === true) { + options.customHeaders = { + 'X-Is-Test': 1 + } + } + if (payload.tags.length > MAX_IMPS_PER_REQUEST) { const clonedPayload = utils.deepClone(payload); @@ -764,16 +790,27 @@ function bidToTag(bid) { type = (utils.isArray(type)) ? type[0] : type; tag.video[param] = VIDEO_MAPPING[param][type]; break; + // Deprecating tags[].video.frameworks in favor of tags[].video_frameworks + case 'frameworks': + break; default: tag.video[param] = bid.params.video[param]; } }); + + if (bid.params.video.frameworks && utils.isArray(bid.params.video.frameworks)) { + tag['video_frameworks'] = bid.params.video.frameworks; + } } if (bid.renderer) { tag.video = Object.assign({}, tag.video, { custom_renderer_present: true }); } + if (bid.params.frameworks && utils.isArray(bid.params.frameworks)) { + tag['banner_frameworks'] = bid.params.frameworks; + } + let adUnit = find(auctionManager.getAdUnits(), au => bid.transactionId === au.transactionId); if (adUnit && adUnit.mediaTypes && adUnit.mediaTypes.banner) { tag.ad_types.push(BANNER); @@ -842,6 +879,19 @@ function hasAdPod(bid) { ); } +function hasOmidSupport(bid) { + let hasOmid = false; + const bidderParams = bid.params; + const videoParams = bid.params.video; + if (bidderParams.frameworks && utils.isArray(bidderParams.frameworks)) { + hasOmid = includes(bid.params.frameworks, 6); + } + if (!hasOmid && videoParams && videoParams.frameworks && utils.isArray(videoParams.frameworks)) { + hasOmid = includes(bid.params.video.frameworks, 6); + } + return hasOmid; +} + /** * Expand an adpod placement into a set of request objects according to the * total adpod duration and the range of duration seconds. Sets minduration/ diff --git a/modules/appnexusBidAdapter.md b/modules/appnexusBidAdapter.md index 6ec40e83b41..d1f61836297 100644 --- a/modules/appnexusBidAdapter.md +++ b/modules/appnexusBidAdapter.md @@ -3,7 +3,7 @@ ``` Module Name: Appnexus Bid Adapter Module Type: Bidder Adapter -Maintainer: info@prebid.org +Maintainer: prebid-js@xandr.com ``` # Description diff --git a/modules/atsAnalyticsAdapter.js b/modules/atsAnalyticsAdapter.js index 9811c306738..a5bb3797bbf 100644 --- a/modules/atsAnalyticsAdapter.js +++ b/modules/atsAnalyticsAdapter.js @@ -126,13 +126,16 @@ let atsAnalyticsAdapter = Object.assign(adapter( callHandler(eventType, args); } if (eventType === CONSTANTS.EVENTS.AUCTION_END) { - // send data to ats analytic endpoint - try { - let dataToSend = {'Data': atsAnalyticsAdapter.context.events}; - let strJSON = JSON.stringify(dataToSend); - ajax(atsAnalyticsAdapter.context.host, function () { - }, strJSON, {method: 'POST', contentType: 'application/json'}); - } catch (err) { + if (atsAnalyticsAdapter.shouldFireRequest()) { + // send data to ats analytic endpoint + try { + let dataToSend = {'Data': atsAnalyticsAdapter.context.events}; + let strJSON = JSON.stringify(dataToSend); + utils.logInfo('atsAnalytics tried to send analytics data!'); + ajax(atsAnalyticsAdapter.context.host, function () { + }, strJSON, {method: 'POST', contentType: 'application/json'}); + } catch (err) { + } } } } @@ -141,6 +144,11 @@ 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 () { + return (Math.floor((Math.random() * 11)) === 10); +} + // override enableAnalytics so we can get access to the config passed in from the page atsAnalyticsAdapter.enableAnalytics = function (config) { if (!config.options.pid) { 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/avocetBidAdapter.js b/modules/avocetBidAdapter.js index 1163ac830ba..7a9e5062c0f 100644 --- a/modules/avocetBidAdapter.js +++ b/modules/avocetBidAdapter.js @@ -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/beachfrontBidAdapter.js b/modules/beachfrontBidAdapter.js index 12e78c684ad..44755a78864 100644 --- a/modules/beachfrontBidAdapter.js +++ b/modules/beachfrontBidAdapter.js @@ -6,7 +6,7 @@ import { VIDEO, BANNER } from '../src/mediaTypes.js'; import find from 'core-js-pure/features/array/find.js'; import includes from 'core-js-pure/features/array/includes.js'; -const ADAPTER_VERSION = '1.11'; +const ADAPTER_VERSION = '1.15'; const ADAPTER_NAME = 'BFIO_PREBID'; const OUTSTREAM = 'outstream'; @@ -14,9 +14,14 @@ 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' } +]; + let appId = ''; export const spec = { @@ -56,18 +61,17 @@ 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, @@ -78,6 +82,16 @@ export const spec = { 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`); @@ -257,13 +271,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 = bid.userId && 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) { @@ -274,6 +318,7 @@ function createVideoRequestData(bid, bidderRequest) { let bidfloor = getVideoBidParam(bid, 'bidfloor'); let tagid = getVideoBidParam(bid, 'tagid'); let topLocation = getTopWindowLocation(bidderRequest); + let eids = getEids(bid); let payload = { isPrebid: true, appId: appId, @@ -322,16 +367,8 @@ 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 (eids.length > 0) { + payload.user.ext.eids = eids; } let connection = navigator.connection || navigator.webkitConnection; @@ -350,6 +387,7 @@ function createBannerRequestData(bids, bidderRequest) { slot: bid.adUnitCode, id: getBannerBidParam(bid, 'appId'), bidfloor: getBannerBidParam(bid, 'bidfloor'), + tagid: getBannerBidParam(bid, 'tagid'), sizes: getBannerSizes(bid) }; }); @@ -378,9 +416,12 @@ function createBannerRequestData(bids, bidderRequest) { payload.gdprConsent = consentString; } - if (bids[0] && bids[0].userId && bids[0].userId.tdid) { - payload.tdid = bids[0].userId.tdid; - } + SUPPORTED_USER_IDS.forEach(({ key, queryParam }) => { + let id = bids[0] && bids[0].userId && bids[0].userId[key]; + if (id) { + payload[queryParam] = id; + } + }); return payload; } diff --git a/modules/betweenBidAdapter.js b/modules/betweenBidAdapter.js index c435a5a993e..0ed05717391 100644 --- a/modules/betweenBidAdapter.js +++ b/modules/betweenBidAdapter.js @@ -1,5 +1,7 @@ import {registerBidder} from '../src/adapters/bidderFactory.js'; import { getAdUnitSizes, parseSizesInput } from '../src/utils.js'; +import { getRefererInfo } from '../src/refererDetection.js'; + const BIDDER_CODE = 'between'; export const spec = { @@ -24,6 +26,7 @@ export const spec = { buildRequests: function(validBidRequests, bidderRequest) { let requests = []; const gdprConsent = bidderRequest && bidderRequest.gdprConsent; + const refInfo = getRefererInfo(); validBidRequests.forEach(i => { let params = { @@ -56,6 +59,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; @@ -161,6 +170,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/bizzclickBidAdapter.js b/modules/bizzclickBidAdapter.js new file mode 100644 index 00000000000..80d2f6b5395 --- /dev/null +++ b/modules/bizzclickBidAdapter.js @@ -0,0 +1,307 @@ +import { registerBidder } from '../src/adapters/bidderFactory.js'; +import { BANNER, NATIVE, VIDEO } from '../src/mediaTypes.js'; +import * as utils from '../src/utils.js'; +import {config} from '../src/config.js'; + +const BIDDER_CODE = '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, + cur: ['USD'], + device: { + w: winTop.screen.width, + h: winTop.screen.height, + language: (navigator && navigator.language) ? navigator.language.indexOf('-') != -1 ? navigator.language.split('-')[0] : navigator.language : '', + }, + site: { + page: location.pathname, + host: location.host + }, + source: { + tid: bidRequest.transactionId + }, + tmax: bidRequest.timeout, + imp: [impObject], + }; + bids.push(data) + } + return { + method: 'POST', + url: endpointURL, + data: bids + }; + }, + + /** + * Unpack the response from the server into a list of bids. + * + * @param {*} serverResponse A successful response from the server. + * @return {Bid[]} An array of bids which were nested inside the server. + */ + interpretResponse: (serverResponse) => { + if (!serverResponse || !serverResponse.body) return [] + let 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/bridgewellBidAdapter.js b/modules/bridgewellBidAdapter.js index a2d3a2e70a2..5fca9acc0b3 100644 --- a/modules/bridgewellBidAdapter.js +++ b/modules/bridgewellBidAdapter.js @@ -38,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 = ''; @@ -65,7 +79,8 @@ export const spec = { inIframe: utils.inIframe(), url: topUrl, referrer: getTopWindowReferrer(), - adUnits: adUnits + adUnits: adUnits, + refererInfo: bidderRequest.refererInfo, }, validBidRequests: validBidRequests }; 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..aa1076e798a 100644 --- a/modules/brightMountainMediaBidAdapter.js +++ b/modules/brightMountainMediaBidAdapter.js @@ -79,7 +79,7 @@ export const spec = { if (syncOptions.iframeEnabled) { return [{ type: 'iframe', - url: 'https://console.brightmountainmedia.com:4444/cookieSync' + url: 'https://console.brightmountainmedia.com:8443/cookieSync' }]; } }, 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/browsiRtdProvider.js b/modules/browsiRtdProvider.js index 3aff3c6aac6..4ee338e94cc 100644 --- a/modules/browsiRtdProvider.js +++ b/modules/browsiRtdProvider.js @@ -13,30 +13,21 @@ * @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'; @@ -63,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; @@ -88,59 +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 || {}; - 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]; - rp[adUnitCode] = getKVObject(-1, _predictionsData.kn); - if (!predictionData) { return rp } - - if (predictionData.p) { - if (!isIdMatchingAdUnit(adSlot, predictionData.w)) { - return rp; - } - rp[adUnitCode] = getKVObject(predictionData.p, _predictionsData.kn); + const _predictions = (_predictionsData && _predictionsData.p) || {}; + return adUnitsCodes.reduce((rp, adUnitCode) => { + if (!adUnitCode) { + return rp + } + 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 {}; } } @@ -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); + let ajax = ajaxBuilder(); ajax(url, { @@ -283,38 +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, - init: init + getTargetingData: sendDataToModule, + init: init, }; -function init(config, gdpr, usp) { +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; } -export function beforeInit(config) { - const confListener = config.getConfig(MODULE_NAME, ({realTimeData}) => { - try { - _moduleParams = realTimeData.dataProviders && realTimeData.dataProviders.filter( - pr => pr.name && pr.name.toLowerCase() === 'browsi')[0].params; - confListener(); - _moduleParams.auctionDelay = realTimeData.auctionDelay; - _moduleParams.timeout = realTimeData.timeout || DEF_TIMEOUT; - } catch (e) { - _moduleParams = {}; - } - if (_moduleParams.siteKey && _moduleParams.pubKey && _moduleParams.url) { - collectData(); - } else { - utils.logError('missing params for Browsi provider'); - } - }); -} - function registerSubModule() { submodule('realTimeData', browsiSubmodule); } registerSubModule(); -beforeInit(config); diff --git a/modules/browsiRtdProvider.md b/modules/browsiRtdProvider.md new file mode 100644 index 00000000000..0dd8c1d7609 --- /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": "testUrl.com", + "siteKey": "testKey", + "pubKey": "testPub", + "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/cointrafficBidAdapter.js b/modules/cointrafficBidAdapter.js index aa6860d1fc6..43f5cc01ccf 100644 --- a/modules/cointrafficBidAdapter.js +++ b/modules/cointrafficBidAdapter.js @@ -1,9 +1,15 @@ import * as utils from '../src/utils.js'; -import {registerBidder} from '../src/adapters/bidderFactory.js'; -import {BANNER} from '../src/mediaTypes.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, @@ -29,9 +35,19 @@ export const spec = { 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, diff --git a/modules/cointrafficBidAdapter.md b/modules/cointrafficBidAdapter.md index ad608a1319e..fcbcad1cad7 100644 --- a/modules/cointrafficBidAdapter.md +++ b/modules/cointrafficBidAdapter.md @@ -7,7 +7,10 @@ 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. For additional information on this module, please contact us at ``support@cointraffic.io``. +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 ``` diff --git a/modules/colossussspBidAdapter.js b/modules/colossussspBidAdapter.js index baa60a76a0d..a3beb723528 100644 --- a/modules/colossussspBidAdapter.js +++ b/modules/colossussspBidAdapter.js @@ -102,7 +102,7 @@ export const spec = { if (bid.userId) { getUserId(placement.eids, bid.userId.britepoolid, 'britepool.com'); getUserId(placement.eids, bid.userId.idl_env, 'identityLink'); - getUserId(placement.eids, bid.userId.id5id, 'id5-sync.com') + getUserId(placement.eids, utils.deepAccess(bid, 'userId.id5id.uid'), 'id5-sync.com', utils.deepAccess(bid, 'userId.id5id.ext')); getUserId(placement.eids, bid.userId.tdid, 'adserver.org', { rtiPartner: 'TDID' }); diff --git a/modules/concertBidAdapter.js b/modules/concertBidAdapter.js index d153ddf9ee2..3eb75799705 100644 --- a/modules/concertBidAdapter.js +++ b/modules/concertBidAdapter.js @@ -42,7 +42,7 @@ export const spec = { debug: utils.debugTurnedOn(), uid: getUid(bidderRequest), optedOut: hasOptedOutOfPersonalization(), - adapterVersion: '1.1.0', + adapterVersion: '1.1.1', uspConsent: bidderRequest.uspConsent, gdprConsent: bidderRequest.gdprConsent } @@ -53,9 +53,12 @@ export const spec = { name: bidRequest.adUnitCode, bidId: bidRequest.bidId, transactionId: bidRequest.transactionId, - sizes: bidRequest.sizes, + sizes: bidRequest.params.sizes || bidRequest.sizes, partnerId: bidRequest.params.partnerId, - slotType: bidRequest.params.slotType + slotType: bidRequest.params.slotType, + adSlot: bidRequest.params.slot || bidRequest.adUnitCode, + placementId: bidRequest.params.placementId || '', + site: bidRequest.params.site || bidderRequest.refererInfo.referer } return slot; diff --git a/modules/concertBidAdapter.md b/modules/concertBidAdapter.md index faf774946d1..d8736082e5c 100644 --- a/modules/concertBidAdapter.md +++ b/modules/concertBidAdapter.md @@ -24,10 +24,14 @@ Module that connects to Concert demand sources { bidder: "concert", params: { - partnerId: 'test_partner' + partnerId: 'test_partner', + site: 'site_name', + placementId: 1234567, + slot: 'slot_name', + sizes: [[1030, 590]] } } ] } ]; -``` \ No newline at end of file +``` diff --git a/modules/consentManagement.js b/modules/consentManagement.js index f44fde0554d..67af2baf959 100644 --- a/modules/consentManagement.js +++ b/modules/consentManagement.js @@ -199,29 +199,51 @@ function lookupIabConsent(cmpSuccess, cmpError, hookConfig) { function callCmpWhileInIframe(commandName, cmpFrame, moduleCallback) { let apiName = (cmpVersion === 2) ? '__tcfapi' : '__cmp'; + let callId = Math.random() + ''; + let callName = `${apiName}Call`; + /* Setup up a __cmp function to do the postMessage and stash the callback. This function behaves (from the caller's perspective identicially to the in-frame __cmp call */ - window[apiName] = function (cmd, arg, callback) { - let callId = Math.random() + ''; - let callName = `${apiName}Call`; - let msg = { - [callName]: { - command: cmd, - parameter: arg, - callId: callId - } - }; - if (cmpVersion !== 1) msg[callName].version = cmpVersion; + if (cmpVersion === 2) { + window[apiName] = function (cmd, cmpVersion, callback, arg) { + let msg = { + [callName]: { + command: cmd, + version: cmpVersion, + parameter: arg, + callId: callId + } + }; - cmpCallbacks[callId] = callback; - cmpFrame.postMessage(msg, '*'); - } + cmpCallbacks[callId] = callback; + cmpFrame.postMessage(msg, '*'); + } - /** when we get the return message, call the stashed callback */ - window.addEventListener('message', readPostMessageResponse, false); + /** when we get the return message, call the stashed callback */ + window.addEventListener('message', readPostMessageResponse, false); - // call CMP - window[apiName](commandName, undefined, moduleCallback); + // call CMP + window[apiName](commandName, cmpVersion, moduleCallback); + } else { + window[apiName] = function (cmd, arg, callback) { + let msg = { + [callName]: { + command: cmd, + parameter: arg, + callId: callId + } + }; + + cmpCallbacks[callId] = callback; + cmpFrame.postMessage(msg, '*'); + } + + /** when we get the return message, call the stashed callback */ + window.addEventListener('message', readPostMessageResponse, false); + + // call CMP + window[apiName](commandName, undefined, moduleCallback); + } function readPostMessageResponse(event) { let cmpDataPkgName = `${apiName}Return`; @@ -451,7 +473,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..3edacb41549 100644 --- a/modules/consentManagementUsp.js +++ b/modules/consentManagementUsp.js @@ -269,7 +269,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/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/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/dfpAdServerVideo.js b/modules/dfpAdServerVideo.js index 554c44aa708..13677c90bae 100644 --- a/modules/dfpAdServerVideo.js +++ b/modules/dfpAdServerVideo.js @@ -8,6 +8,7 @@ import { deepAccess, isEmpty, logError, parseSizesInput, formatQS, parseUrl, bui import { config } from '../src/config.js'; import { getHook, submodule } from '../src/hook.js'; import { auctionManager } from '../src/auctionManager.js'; +import { uspDataHandler } from '../src/adapterManager.js'; import events from '../src/events.js'; import CONSTANTS from '../src/constants.json'; @@ -100,6 +101,9 @@ export function buildDfpVideoUrl(options) { const descriptionUrl = getDescriptionUrl(bid, options, 'params'); if (descriptionUrl) { queryParams.description_url = descriptionUrl; } + const uspConsent = uspDataHandler.getConsentData(); + if (uspConsent) { queryParams.us_privacy = uspConsent; } + return buildUrl({ protocol: 'https', host: 'securepubads.g.doubleclick.net', @@ -183,6 +187,9 @@ export function buildAdpodVideoUrl({code, params, callback} = {}) { { cust_params: encodedCustomParams } ); + const uspConsent = uspDataHandler.getConsentData(); + if (uspConsent) { queryParams.us_privacy = uspConsent; } + const masterTag = buildUrl({ protocol: 'https', host: 'securepubads.g.doubleclick.net', diff --git a/modules/districtmDMXBidAdapter.js b/modules/districtmDMXBidAdapter.js index 21f3b7b3586..60f6b9b64b1 100644 --- a/modules/districtmDMXBidAdapter.js +++ b/modules/districtmDMXBidAdapter.js @@ -1,6 +1,6 @@ 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'; @@ -28,20 +28,22 @@ export const spec = { 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.mediaType = bid.mediaTypes && bid.mediaTypes.video ? 'video' : null; + nBid.ttl = 360; + nBid.mediaType = bid.mediaTypes && bid.mediaTypes.video ? 'video' : 'banner'; if (nBid.mediaType) { - nBid.vastXml = cleanVast(nBid.adm); + nBid.vastXml = cleanVast(nBid.adm, nBid.nurl); + nBid.ttl = 3600; } if (nBid.dealid) { nBid.dealId = nBid.dealid; @@ -51,7 +53,6 @@ export const spec = { 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; @@ -61,7 +62,7 @@ export const spec = { oBid.cpm = oBid.price; return oBid; } - }, {price: 0}); + }, { price: 0 }); if (ad.adm) { bid.push(ad) } @@ -98,7 +99,7 @@ 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) { } @@ -106,7 +107,7 @@ export const spec = { let eids = []; 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`), 'id5-sync.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); @@ -128,9 +129,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; @@ -144,7 +148,7 @@ 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; @@ -154,19 +158,16 @@ export const spec = { if (dmx.mediaTypes && dmx.mediaTypes.video) { obj.video = { topframe: 1, - skip: dmx.mediaTypes.video.skippable || 0, + 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: getPlaybackmethod(dmx.mediaTypes.video.playback_method), + playbackmethod: dmx.mediaTypes.video.playbackmethod || [2], api: getApi(dmx.mediaTypes.video), mimes: dmx.mediaTypes.video.mimes || ['video/mp4'], protocols: getProtocols(dmx.mediaTypes.video), - w: dmx.mediaTypes.video.playerSize[0][0], h: dmx.mediaTypes.video.playerSize[0][1], - format: dmx.mediaTypes.video.playerSize.map(s => { - return {w: s[0], h: s[1]}; - }).filter(obj => typeof obj.w === 'number' && typeof obj.h === 'number') + w: dmx.mediaTypes.video.playerSize[0][0] }; } else { obj.banner = { @@ -174,7 +175,7 @@ export const spec = { w: cleanSizes(dmx.sizes, 'w'), h: cleanSizes(dmx.sizes, 'h'), format: cleanSizes(dmx.sizes).map(s => { - return {w: s[0], h: s[1]}; + return { w: s[0], h: s[1] }; }).filter(obj => typeof obj.w === 'number' && typeof obj.h === 'number') }; } @@ -217,7 +218,7 @@ export const spec = { } } -export function getFloor (bid) { +export function getFloor(bid) { let floor = null; if (typeof bid.getFloor === 'function') { const floorInfo = bid.getFloor({ @@ -297,7 +298,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); @@ -343,7 +344,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; } @@ -359,7 +360,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]; @@ -380,20 +381,10 @@ export function bindUserId(eids, value, source, atype) { } } -export function getApi({protocols}) { +export function getApi({ api }) { let defaultValue = [2]; - let listProtocols = [ - {key: 'VPAID_1_0', value: 1}, - {key: 'VPAID_2_0', value: 2}, - {key: 'MRAID_1', value: 3}, - {key: 'ORMMA', value: 4}, - {key: 'MRAID_2', value: 5}, - {key: 'MRAID_3', value: 6}, - ]; - if (protocols) { - return listProtocols.filter(p => { - return protocols.indexOf(p.key) !== -1; - }).map(p => p.value) + if (api && Array.isArray(api) && api.length > 0) { + return api } else { return defaultValue; } @@ -407,37 +398,34 @@ export function getPlaybackmethod(playback) { return [2] } -export function getProtocols({protocols}) { +export function getProtocols({ protocols }) { let defaultValue = [2, 3, 5, 6, 7, 8]; - let listProtocols = [ - {key: 'VAST_1_0', value: 1}, - {key: 'VAST_2_0', value: 2}, - {key: 'VAST_3_0', value: 3}, - {key: 'VAST_1_0_WRAPPER', value: 4}, - {key: 'VAST_2_0_WRAPPER', value: 5}, - {key: 'VAST_3_0_WRAPPER', value: 6}, - {key: 'VAST_4_0', value: 7}, - {key: 'VAST_4_0_WRAPPER', value: 8} - ]; - if (protocols) { - return listProtocols.filter(p => { - return protocols.indexOf(p.key) !== -1 - }).map(p => p.value); + if (protocols && Array.isArray(protocols) && protocols.length > 0) { + return protocols; } else { return defaultValue; } } -export function cleanVast(str) { - 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}`) +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 } - return str; } registerBidder(spec); diff --git a/modules/emx_digitalBidAdapter.js b/modules/emx_digitalBidAdapter.js index fa58481548a..72da18d5691 100644 --- a/modules/emx_digitalBidAdapter.js +++ b/modules/emx_digitalBidAdapter.js @@ -156,6 +156,17 @@ export const emxAdapter = { }; } + return emxData; + }, + getSupplyChain: (bidRequests, emxData) => { + if (bidRequests.schain) { + emxData.source = { + ext: { + schain: bidRequests.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/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..e61b377eefa --- /dev/null +++ b/modules/fabrickIdSystem.js @@ -0,0 +1,147 @@ +/** + * 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 || 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') { + continue; + } + let v = configParams[k]; + if (Array.isArray(v)) { + for (let j in v) { + url += `${k}=${v[j]}&`; + } + } else { + url += `${k}=${v}&`; + } + } + // pull off the trailing & + url = url.slice(0, -1) + const referer = _getRefererInfo(configParams); + const urls = new Set(); + url = truncateAndAppend(urls, url, 'r', referer.referer); + if (referer.stack && referer.stack[0]) { + url = truncateAndAppend(urls, url, 'r', referer.stack[0]); + } + url = truncateAndAppend(urls, url, 'r', referer.canonicalUrl); + url = truncateAndAppend(urls, url, 'r', window.location.href); + + 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 truncateAndAppend(urls, url, paramName, s) { + if (s && url.length < 2000) { + if (s.length > 200) { + s = s.substring(0, 200); + } + // Don't send the same url in multiple params + if (!urls.has(s)) { + urls.add(s); + return `${url}&${paramName}=${s}` + } + } + 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/fidelityBidAdapter.js b/modules/fidelityBidAdapter.js index baf5384fbfe..fac273721ff 100644 --- a/modules/fidelityBidAdapter.js +++ b/modules/fidelityBidAdapter.js @@ -6,7 +6,6 @@ const BIDDER_SERVER = 'x.fidelity-media.com'; const FIDELITY_VENDOR_ID = 408; export const spec = { code: BIDDER_CODE, - aliases: ['kubient'], gvlid: 408, isBidRequestValid: function isBidRequestValid(bid) { return !!(bid && bid.params && bid.params.zoneid); diff --git a/modules/freewheel-sspBidAdapter.js b/modules/freewheel-sspBidAdapter.js index dce678362cb..53f490a0a3c 100644 --- a/modules/freewheel-sspBidAdapter.js +++ b/modules/freewheel-sspBidAdapter.js @@ -407,11 +407,20 @@ 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 []; diff --git a/modules/gamoshiBidAdapter.js b/modules/gamoshiBidAdapter.js index 1316d74e430..48a142a66a6 100644 --- a/modules/gamoshiBidAdapter.js +++ b/modules/gamoshiBidAdapter.js @@ -42,7 +42,7 @@ export const helper = { export const spec = { code: 'gamoshi', - aliases: ['gambid', 'cleanmedia', '9MediaOnline'], + aliases: ['gambid', 'cleanmedia', '9MediaOnline', 'MobfoxX'], supportedMediaTypes: ['banner', 'video'], isBidRequestValid: function (bid) { @@ -157,7 +157,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/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..e4aa046a97d --- /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 let pusblishers to 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 index ca7fb4af32d..48496b52c05 100644 --- a/modules/gjirafaBidAdapter.js +++ b/modules/gjirafaBidAdapter.js @@ -1,4 +1,5 @@ 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'; @@ -7,6 +8,7 @@ const SIZE_SEPARATOR = ';'; export const spec = { code: BIDDER_CODE, + supportedMediaTypes: [BANNER, VIDEO], /** * Determines whether or not the given bid request is valid. * @@ -23,31 +25,48 @@ export const spec = { * @return ServerRequest Info describing the request to the server. */ buildRequests: function (validBidRequests, bidderRequest) { - let response = validBidRequests.map(bidRequest => { - let sizes = generateSizeParam(bidRequest.sizes); - let propertyId = bidRequest.params.propertyId; - let placementId = bidRequest.params.placementId; + let propertyId = ''; + let pageViewGuid = ''; + let storageId = ''; + let bidderRequestId = ''; + let url = ''; + let contents = []; + + 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 } + let adUnitId = bidRequest.adUnitCode; - let pageViewGuid = bidRequest.params.pageViewGuid || ''; - let contents = bidRequest.params.contents || []; - const body = { + let placementId = bidRequest.params.placementId; + let sizes = generateSizeParam(bidRequest.sizes); + + return { sizes: sizes, adUnitId: adUnitId, placementId: placementId, - propertyId: propertyId, - pageViewGuid: pageViewGuid, - url: bidderRequest ? bidderRequest.refererInfo.referer : '', - requestid: bidRequest.bidderRequestId, bidid: bidRequest.bidId, - contents: contents - }; - return { - method: 'POST', - url: ENDPOINT_URL, - data: body }; }); - return response + + let body = { + propertyId: propertyId, + pageViewGuid: pageViewGuid, + storageId: storageId, + url: url, + requestid: bidderRequestId, + placements: placements, + contents: contents + } + + return [{ + method: 'POST', + url: ENDPOINT_URL, + data: body + }]; }, /** * Unpack the response from the server into a list of bids. @@ -55,13 +74,12 @@ export const spec = { * @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) { - window.adnResponse = serverResponse; + interpretResponse: function (serverResponse) { const responses = serverResponse.body; const bidResponses = []; for (var i = 0; i < responses.length; i++) { const bidResponse = { - requestId: bidRequest.data.bidid, + requestId: responses[i].BidId, cpm: responses[i].CPM, width: responses[i].Width, height: responses[i].Height, @@ -70,7 +88,9 @@ export const spec = { netRevenue: responses[i].NetRevenue, ttl: responses[i].TTL, referrer: responses[i].Referrer, - ad: responses[i].Ad + ad: responses[i].Ad, + vastUrl: responses[i].VastUrl, + mediaType: responses[i].MediaType }; bidResponses.push(bidResponse); } diff --git a/modules/gjirafaBidAdapter.md b/modules/gjirafaBidAdapter.md index 53d3a76c5ed..deb06e74a27 100644 --- a/modules/gjirafaBidAdapter.md +++ b/modules/gjirafaBidAdapter.md @@ -28,22 +28,16 @@ var adUnits = [ { code: 'test-div', mediaTypes: { - banner: { - sizes: [[300, 250]] - } + video: { + context: 'instream' + } }, bids: [ { bidder: 'gjirafa', params: { propertyId: '105227', - placementId: '846848', - contents: [ //optional - { - type: 'article', - id: '123' - } - ] + placementId: '846836' } } ] diff --git a/modules/gridBidAdapter.js b/modules/gridBidAdapter.js index 32274fed2e6..3f9d4fba31d 100644 --- a/modules/gridBidAdapter.js +++ b/modules/gridBidAdapter.js @@ -5,8 +5,7 @@ import { VIDEO, BANNER } from '../src/mediaTypes.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,236 @@ 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 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; + } + const {params: {uid, keywords}, mediaTypes, bidId, adUnitCode, rtd} = bid; + bidsMap[bidId] = bid; + if (!pageKeywords && !utils.isEmpty(keywords)) { + pageKeywords = utils.transformBidderParamKeywords(keywords); + } + const bidFloor = _getFloor(mediaTypes || {}, bid); + const jwTargeting = rtd && rtd.jwplayer && rtd.jwplayer.targeting; + if (jwTargeting) { + if (!jwpseg && jwTargeting.segments) { + jwpseg = jwTargeting.segments; + } + if (!content && jwTargeting.content) { + content = jwTargeting.content; + } + } + let impObj = { + id: bidId, + tagid: uid.toString(), + ext: { + divid: adUnitCode + } + }; + + if (bidFloor) { + impObj.bidfloor = bidFloor; + } + + if (!mediaTypes || mediaTypes[BANNER]) { + const banner = createBannerRequest(bid, mediaTypes ? mediaTypes[BANNER] : {}); + if (banner) { + impObj.banner = banner; + } + } + if (mediaTypes && mediaTypes[VIDEO]) { + const video = createVideoRequest(bid, mediaTypes[VIDEO]); + if (video) { + impObj.video = video; + } + } + + if (impObj.banner || impObj.video) { + imp.push(impObj); + } + }); + + const source = { + tid: auctionId, + ext: { + wrapper: 'Prebid_js', + wrapper_version: '$prebid.version$' + } + }; + + if (schain) { + source.ext.schain = schain; + } + + const bidderTimeout = config.getConfig('bidderTimeout') || timeout; + const tmax = timeout ? Math.min(bidderTimeout, timeout) : bidderTimeout; + + let request = { + id: bidderRequestId, + site: { + page: referer + }, + tmax, + source, + imp + }; + + if (content) { + request.site.content = content; + } + + if (jwpseg && jwpseg.length) { + user = { + data: [{ + name: 'iow_labs_pub_data', + segment: jwpseg.map((seg) => { + return {name: 'jwpseg', value: seg}; + }) + }] + }; + } + + if (gdprConsent && gdprConsent.consentString) { + userExt = {consent: gdprConsent.consentString}; + } + + if (userId) { + if (userId.tdid) { + userExt = userExt || {}; + userExt.eids = userExt.eids || []; + userExt.eids.push({ + source: 'adserver.org', // Unified ID + uids: [{ + id: userId.tdid, + ext: { + rtiPartner: 'TDID' + } + }] + }); + } + if (userId.id5id && userId.id5id.uid) { + userExt = userExt || {}; + userExt.eids = userExt.eids || []; + userExt.eids.push({ + source: 'id5-sync.com', + uids: [{ + id: userId.id5id.uid + }], + ext: userId.id5id.ext + }); + } + if (userId.lipb && userId.lipb.lipbid) { + userExt = userExt || {}; + userExt.eids = userExt.eids || []; + userExt.eids.push({ + source: 'liveintent.com', + uids: [{ + id: userId.lipb.lipbid + }] + }); + } + if (userId.idl_env) { + userExt = userExt || {}; + userExt.eids = userExt.eids || []; + userExt.eids.push({ + source: 'identityLink', + uids: [{ + id: userId.idl_env + }] + }); + } + if (userId.criteoId) { + userExt = userExt || {}; + userExt.eids = userExt.eids || []; + userExt.eids.push({ + source: 'criteo.com', + uids: [{ + id: userId.criteoId + }] + }); + } + + if (userId.digitrustid && userId.digitrustid.data && userId.digitrustid.data.id) { + userExt = userExt || {}; + userExt.digitrust = Object.assign({}, userId.digitrustid.data); + } + } + + if (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 }); - const requests = []; - if (newFormatBids.length) { - requests.push(buildNewRequest(newFormatBids, bidderRequest)); + + if (configKeywords.length) { + pageKeywords = (pageKeywords || []).concat(configKeywords); } - if (oldFormatBids.length) { - requests.push(buildOldRequest(oldFormatBids, bidderRequest)); + + 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 requests; + + 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 +324,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,24 +378,7 @@ 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, @@ -184,21 +410,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,294 +417,6 @@ 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.jwTargeting.segments; - } - if (!content && realTimeData.jwTargeting.content) { - content = realTimeData.jwTargeting.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 size = (playerSize || bid.sizes || [])[0]; 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/gumgumBidAdapter.js b/modules/gumgumBidAdapter.js index 9714a3eeeca..2cb5ce61064 100644 --- a/modules/gumgumBidAdapter.js +++ b/modules/gumgumBidAdapter.js @@ -269,6 +269,10 @@ function buildRequests (validBidRequests, bidderRequest) { data.fp = bidFloor; } + if (params.iriscat && typeof params.iriscat === 'string') { + data.iriscat = params.iriscat; + } + if (params.zone) { data.t = params.zone; data.pi = 2; // inscreen @@ -399,6 +403,7 @@ function interpretResponse (serverResponse, bidRequest) { } = Object.assign(defaultResponse, serverResponseBody) let data = bidRequest.data || {} let product = data.pi + let mediaType = (product === 6 || product === 7) ? VIDEO : BANNER let isTestUnit = (product === 3 && data.si === 9) let sizes = utils.parseSizesInput(bidRequest.sizes) let [width, height] = sizes[0].split('x') @@ -420,9 +425,9 @@ function interpretResponse (serverResponse, bidRequest) { bidResponses.push({ // dealId: DEAL_ID, // referrer: REFERER, - ...(product === 7 && { vastXml: markup, mediaType: VIDEO }), ad: wrapper ? getWrapperCode(wrapper, Object.assign({}, serverResponseBody, { bidRequest })) : markup, - ...(product === 6 && {ad: markup}), + ...(mediaType === VIDEO && {ad: markup, vastXml: markup}), + mediaType, cpm: isTestUnit ? 0.1 : cpm, creativeId, currency: cur || 'USD', diff --git a/modules/gumgumBidAdapter.md b/modules/gumgumBidAdapter.md index f47666e9628..7b4f0c98ea7 100644 --- a/modules/gumgumBidAdapter.md +++ b/modules/gumgumBidAdapter.md @@ -8,7 +8,10 @@ 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. +All other products (in-screen, slot, native) should have a mediaType of banner. + # Test Parameters ``` @@ -16,6 +19,11 @@ var adUnits = [ { code: 'test-div', sizes: [[300, 250]], + mediaTypes: { + banner: { + sizes: [[300, 250]], + } + }, bids: [ { bidder: 'gumgum', @@ -28,6 +36,11 @@ var adUnits = [ },{ code: 'test-div', sizes: [[300, 50]], + mediaTypes: { + banner: { + sizes: [[1, 1]], + } + }, bids: [ { bidder: 'gumgum', @@ -40,6 +53,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/haloIdSystem.js b/modules/haloIdSystem.js index 237b502f6a7..d0eb79d4ac2 100644 --- a/modules/haloIdSystem.js +++ b/modules/haloIdSystem.js @@ -30,10 +30,10 @@ export const haloIdSubmodule = { /** * 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 url = `https://id.halo.ad.gt/api/v1/pbhid`; const resp = function (callback) { diff --git a/modules/haloRtdProvider.js b/modules/haloRtdProvider.js new file mode 100644 index 00000000000..1ce44ca6004 --- /dev/null +++ b/modules/haloRtdProvider.js @@ -0,0 +1,197 @@ +/** + * 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 + */ +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'; + +export const storage = getStorageManager(); + +/** @type {string} */ +const MODULE_NAME = 'realTimeData'; +const SUBMODULE_NAME = 'halo'; + +export const HALOID_LOCAL_NAME = 'auHaloId'; +export const SEG_LOCAL_NAME = '__adgntseg'; + +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; +}; + +/** bid adapter format segment augmentation functions */ +const segmentMappers = { + appnexus: function(bid, segments) { + set(bid, 'params.user.segments', []); + let appnexusSegments = []; + segments.forEach(segment => { + if (typeof segment.id != 'undefined' && segment.id != null) { + appnexusSegments.push(parseInt(segment.id)); + } + }) + bid.params.user.segments = bid.params.user.segments.concat(appnexusSegments); + }, + generic: function(bid, segments) { + bid.segments = bid.segments || []; + if (Array.isArray(bid.segments)) { + bid.segments = bid.segments.concat(segments); + } + } +} + +/** + * decorate adUnits with segment data + * @param {adUnit[]} adUnits + * @param {Object} data + */ +export function addSegmentData(adUnits, segmentData, config) { + adUnits.forEach(adUnit => { + if (adUnit.hasOwnProperty('bids')) { + adUnit.bids.forEach(bid => { + try { + set(bid, 'fpd.user.data', []); + if (Array.isArray(bid.fpd.user.data)) { + bid.fpd.user.data.forEach(fpdData => { + let segments = segmentData[fpdData.id] || segmentData[fpdData.name] || []; + fpdData.segment = (fpdData.segment || []).concat(segments); + }); + } + } catch (err) { + utils.logError(err.message); + } + + try { + if (config.params.mapSegments && config.params.mapSegments[bid.bidder] && segmentData[bid.bidder]) { + if (typeof config.params.mapSegments[bid.bidder] == 'function') { + config.params.mapSegments[bid.bidder](bid, segmentData[bid.bidder]); + } else if (segmentMappers[bid.bidder]) { + segmentMappers[bid.bidder](bid, segmentData[bid.bidder]); + } + } + } catch (err) { + utils.logError(err.message); + } + }); + } + }); + + return adUnits; +} + +/** + * segment retrieval from audigent's backends + * @param {Object} reqBidsConfigObj + * @param {function} onDone + * @param {Object} config + * @param {Object} userConsent + */ +export function getSegments(reqBidsConfigObj, onDone, config, userConsent) { + const adUnits = reqBidsConfigObj.adUnits || getGlobal().adUnits; + + if (config.params.segmentCache) { + let jsonData = storage.getDataFromLocalStorage(SEG_LOCAL_NAME); + + if (jsonData) { + let data = JSON.parse(jsonData); + + if (data.audigent_segments) { + addSegmentData(adUnits, data.audigent_segments, config); + onDone(); + return; + } + } + } + + const userIds = (getGlobal()).getUserIds(); + + let haloId = storage.getDataFromLocalStorage(HALOID_LOCAL_NAME); + if (haloId) { + userIds.haloId = haloId; + getSegmentsAsync(adUnits, onDone, config, userConsent, userIds); + } else { + var script = document.createElement('script') + script.type = 'text/javascript'; + + script.onload = function() { + userIds.haloId = storage.getDataFromLocalStorage(HALOID_LOCAL_NAME); + getSegmentsAsync(adUnits, onDone, config, userConsent, userIds); + } + + script.src = 'https://id.halo.ad.gt/api/v1/haloid'; + document.getElementsByTagName('head')[0].appendChild(script); + } +} + +/** + * async segment retrieval from audigent's backends + * @param {adUnit[]} adUnits + * @param {function} onDone + * @param {Object} config + * @param {Object} userConsent + * @param {Object} userIds + */ +export function getSegmentsAsync(adUnits, onDone, config, userConsent, userIds) { + let reqParams = {}; + if (typeof config == 'object' && config != null) { + set(config, 'params.requestParams', {}); + reqParams = config.params.requestParams; + } + + const url = `https://seg.halo.ad.gt/api/v1/rtb_segments`; + ajax(url, { + success: function (response, req) { + if (req.status === 200) { + try { + const data = JSON.parse(response); + if (data && data.audigent_segments) { + addSegmentData(adUnits, data.audigent_segments, config); + onDone(); + storage.setDataInLocalStorage(SEG_LOCAL_NAME, JSON.stringify(data)); + } else { + onDone(); + } + } catch (err) { + utils.logError('unable to parse audigent segment data'); + onDone(); + } + } else if (req.status === 204) { + // unrecognized partner config + onDone(); + } + }, + error: function () { + onDone(); + utils.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: getSegments, + init: init +}; + +submodule(MODULE_NAME, haloSubmodule); diff --git a/modules/haloRtdProvider.md b/modules/haloRtdProvider.md new file mode 100644 index 00000000000..2897a5917fa --- /dev/null +++ b/modules/haloRtdProvider.md @@ -0,0 +1,132 @@ +## 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 user segmentation 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, 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 can be retrieved and supplied to the SSP 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,audigentRtdProvider,appnexusBidAdapter` + +Add the Halo RTD provider to your Prebid config. For any adapters +that you would like to retrieve segments for, add a mapping in the 'mapSegments' +parameter. In this example we will configure publisher 1234 to retrieve +appnexus segments from Audigent. See the "Parameter Descriptions" below for +more detailed information of the configuration parameters. Currently, +OpenRTB compatible fpd data will be added for any bid adapter in the +"mapSegments" objects. Automated bid augmentation exists for some bidders. +Please work with your Audigent Prebid support team (prebid@audigent.com) on +which version of Prebid.js supports which bidders automatically. + +``` +pbjs.setConfig( + ... + realTimeData: { + auctionDelay: auctionDelay, + dataProviders: [ + { + name: "halo", + waitForIt: true, + params: { + mapSegments: { + appnexus: true, + }, + segmentCache: false, + requestParams: { + publisherId: 1234 + } + } + } + ] + } + ... +} +``` + +### Parameter Descriptions for the Halo `dataProviders` 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.mapSegments | Boolean | Dictionary of bidders you would like to supply Audigent segments for. Maps to boolean values, but also allows functions for custom mapping logic. The function signature is (bid, segments) => {}. | Required | +| 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 | + +### Overriding & Adding Segment Mappers +As indicated above, it is possible to provide your own bid augmentation +functions. This is useful if you know a bid adapter's API supports segment +fields which aren't specifically being added to request objects in the Prebid +bid adapter. You can also override segment mappers by passing a function +instead of a boolean to the Halo RTD segment module. This might be useful +if you'd like to use custom logic to determine which segments are sent +to a specific backend. + +Please see the following example, which provides a function to modify bids for +a bid adapter called adBuzz and overrides the appnexus segment mapper. + +``` +pbjs.setConfig( + ... + realTimeData: { + auctionDelay: auctionDelay, + dataProviders: [ + { + name: "halo", + waitForIt: true, + params: { + mapSegments: { + // adding an adBuzz segment mapper + adBuzz: function(bid, segments) { + bid.params.adBuzzCustomSegments = []; + for (var i = 0; i < segments.length; i++) { + bid.params.adBuzzCustomSegments.push(segments[i].id); + } + }, + // overriding the appnexus segment mapper to exclude certain segments + appnexus: function(bid, segments) { + for (var i = 0; i < segments.length; i++) { + if (segments[i].id != 'exclude_segment') { + bid.params.user.segments.push(segments[i].id); + } + } + } + }, + segmentCache: false, + requestParams: { + publisherId: 1234 + } + } + } + ] + } + ... +} +``` + +More examples can be viewed in the haloRtdAdapter_spec.js tests. + +### 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..dd55483ef33 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..17a808badaa 100644 --- a/modules/id5IdSystem.js +++ b/modules/id5IdSystem.js @@ -10,11 +10,17 @@ 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'; +const LOCAL_STORAGE = 'html5'; + +// 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' ]; const storage = getStorageManager(GVLID, MODULE_NAME); @@ -36,47 +42,90 @@ 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 + let abConfig = (config && config.params && config.params.abTesting) || { enabled: false }; + let controlGroup = false; + if ( + abConfig.enabled === true && + (!utils.isNumber(abConfig.controlGroupPct) || + abConfig.controlGroupPct < 0 || + abConfig.controlGroupPct > 1) + ) { + // A/B Testing is enabled, but configured improperly, so skip A/B testing + utils.logError('User ID - ID5 submodule: A/B Testing controlGroupPct must be a number >= 0 and <= 1! Skipping A/B Testing'); + } else if ( + abConfig.enabled === true && + Math.random() < abConfig.controlGroupPct + ) { + // A/B Testing is enabled and user is in the Control Group, so do not share the ID5 ID + utils.logInfo('User ID - ID5 submodule: A/B Testing Enabled - request is in the Control Group, so the ID5 ID is NOT exposed'); + universalUid = linkType = 0; + controlGroup = true; + } else if (abConfig.enabled === true) { + // A/B Testing is enabled but user is not in the Control Group, so ID5 ID is shared + utils.logInfo('User ID - ID5 submodule: A/B Testing Enabled - request 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', controlGroup); + } + + 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 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), + 'gdpr': hasGdpr, + 'gdpr_consent': hasGdpr ? consentData.consentString : '', + 'partner': config.params.partner, + 'nbPage': incrementNb(config.params.partner), 'o': 'pbjs', - 'pd': configParams.pd || '', + 'pd': config.params.pd || '', + 'provider': config.params.provider || '', 'rf': referer.referer, 's': signature, 'top': referer.reachedTop ? 1 : 0, 'u': referer.stack[0] || window.location.href, + 'us_privacy': uspDataHandler.getConsentData() || '', 'v': '$prebid.version$' }; @@ -87,7 +136,13 @@ export const id5IdSubmodule = { if (response) { try { responseObj = JSON.parse(response); - resetNb(configParams); + resetNb(config.params.partner); + + // 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); } @@ -95,7 +150,7 @@ export const id5IdSubmodule = { callback(responseObj); }, error: error => { - utils.logError(`id5Id: ID fetch encountered an error`, error); + utils.logError(`User ID - ID5 submodule getId fetch encountered an error`, error); callback(); } }; @@ -110,43 +165,117 @@ 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 {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, cacheIdObj) { + const partnerId = (config && config.params && config.params.partner) || 0; + incrementNb(partnerId); return cacheIdObj; } }; -function hasRequiredParams(configParams) { - if (!configParams || typeof configParams.partner !== 'number') { +function hasRequiredConfig(config) { + if (!config || !config.params || !config.params.partner || typeof config.params.partner !== 'number') { utils.logError(`User ID - ID5 submodule requires partner to be defined as a number`); return false; } + + if (!config.storage || !config.storage.type || !config.storage.name) { + utils.logError(`User ID - ID5 submodule requires storage to be set`); + return false; + } + + // TODO: in a future release, return false if storage type or name are not set as required + if (config.storage.type !== LOCAL_STORAGE) { + utils.logWarn(`User ID - ID5 submodule recommends storage type to be '${LOCAL_STORAGE}'. In a future release this will become a strict requirement`); + } + // TODO: in a future release, return false if storage type or name are not set as required + if (config.storage.name !== ID5_STORAGE_NAME) { + utils.logWarn(`User ID - ID5 submodule recommends storage name to be '${ID5_STORAGE_NAME}'. In a future release this will 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) { + 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); } submodule('userId', id5IdSubmodule); diff --git a/modules/id5IdSystem.md b/modules/id5IdSystem.md new file mode 100644 index 00000000000..6b2192834fa --- /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://console.id5.io/docs/public/prebid). We also recommend that you sign up for our [release notes](https://id5.io/universal-id/release-notes) to stay up-to-date with any changes to the implementation of the ID5 Universal ID in Prebid. + +## 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 requests you would like to set for the control group. The control group is the set of requests 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 request was in the control group or not. It's important to note that the control group is request based, and not user based. In other words, from one page view to another, a user may be in or out of the control group. diff --git a/modules/idLibrary.js b/modules/idLibrary.js new file mode 100644 index 00000000000..ba3cc0b5efb --- /dev/null +++ b/modules/idLibrary.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 = true; +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' + JSON.stringify(userIds)); + const syncPayload = {}; + syncPayload.hid = MD5(email).toString(); + syncPayload.uids = JSON.stringify(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('idLibrary', config => setConfig(config.idLibrary)); diff --git a/modules/idLibrary.md b/modules/idLibrary.md new file mode 100644 index 00000000000..69b63dc466b --- /dev/null +++ b/modules/idLibrary.md @@ -0,0 +1,24 @@ +## ID Library Configuration Example + + +|Param |Required |Description | +|----------------|-------------------------------|-----------------------------| +|url |Yes | The url endpoint is used to post the hashed email and user ids. | +|target |No |It should contain the element id from which the email can be read. | +|debounce |No | Time in milliseconds before the email and ids are fetched | +|fullscan |No | Option to enable/disable full body scan to get email. By default the full body scan is enabled. | + +### Example +``` + pbjs.setConfig({ + idLibrary:{ + url: , + debounce: 250, + target: 'username', + fullscan: false + }, + }); +``` + + +``` diff --git a/modules/identityLinkIdSystem.js b/modules/identityLinkIdSystem.js index 14c33329b2d..33dd74380ac 100644 --- a/modules/identityLinkIdSystem.js +++ b/modules/identityLinkIdSystem.js @@ -8,6 +8,9 @@ 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'; + +export const storage = getStorageManager(); /** @type {Submodule} */ export const identityLinkSubmodule = { @@ -34,10 +37,11 @@ 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'); return; @@ -75,7 +79,6 @@ export const identityLinkSubmodule = { }; // return envelope from third party endpoint function getEnvelope(url, callback) { - utils.logInfo('A 3P retrieval is attempted!'); const callbacks = { success: response => { let responseObj; @@ -93,7 +96,18 @@ function getEnvelope(url, callback) { callback(); } }; - ajax(url, callbacks, undefined, { method: 'GET', withCredentials: true }); + + if (!storage.getCookie('_lr_retry_request')) { + setRetryCookie(); + utils.logInfo('A 3P retrieval is attempted!'); + 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()); } 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/improvedigitalBidAdapter.js b/modules/improvedigitalBidAdapter.js index 3c000258ede..0e2d8f6f7dd 100644 --- a/modules/improvedigitalBidAdapter.js +++ b/modules/improvedigitalBidAdapter.js @@ -3,6 +3,7 @@ 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'; const BIDDER_CODE = 'improvedigital'; const RENDERER_URL = 'https://acdn.adnxs.com/video/outstream/ANOutstreamVideo.js'; @@ -56,6 +57,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 @@ -552,6 +560,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]; diff --git a/modules/inmarBidAdapter.js b/modules/inmarBidAdapter.js new file mode 100755 index 00000000000..b5ab72266fc --- /dev/null +++ b/modules/inmarBidAdapter.js @@ -0,0 +1,113 @@ +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 && bid.params.adnetId); + }, + + /** + * 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.getConfig('fpd'), + prebidVersion: '$prebid.version$' + }; + + var payloadString = JSON.stringify(payload); + + return { + method: 'POST', + url: 'https://prebid.owneriq.net:8443/bidder/pb/bid', + options: { + withCredentials: false + }, + 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..1bacb30f2dd --- /dev/null +++ b/modules/inmarBidAdapter.md @@ -0,0 +1,46 @@ +# 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, + adnetId: 'ADb1f40rmi', + position: 1 + } + }] + }, + { + code: 'test-div2', + sizes: [[728, 90],[970, 250]], + bids: [{ + bidder: 'inmar', + params: { + partnerId: 12345, + adnetId: 'ADb1f40rmo', + position: 0 + } + }] + } + ]; +``` diff --git a/modules/intentIqIdSystem.js b/modules/intentIqIdSystem.js index 242b227f89f..c22a6dbc7aa 100644 --- a/modules/intentIqIdSystem.js +++ b/modules/intentIqIdSystem.js @@ -11,6 +11,32 @@ import {submodule} from '../src/hook.js' const MODULE_NAME = 'intentIqId'; +const NOT_AVAILABLE = 'NA'; + +/** + * Verify the id is valid - Id value or Not Found (ignore not available response) + * @param id + * @returns {boolean|*|boolean} + */ +function isValidId(id) { + return id && id != '' && id != NOT_AVAILABLE && isValidResponse(id); +} + +/** + * Ignore not available response JSON + * @param obj + * @returns {boolean} + */ +function isValidResponse(obj) { + try { + obj = JSON.parse(obj); + return obj && obj['RESULT'] != NOT_AVAILABLE; + } catch (error) { + utils.logError(error); + return true; + } +} + /** @type {Submodule} */ export const intentIqIdSubmodule = { /** @@ -22,29 +48,37 @@ export const intentIqIdSubmodule = { * decode the stored id value for passing to bid requests * @function * @param {{string}} value - * @returns {{intentIqId:string}} + * @returns {{intentIqId: {string}}|undefined} */ decode(value) { - return (value && value != '') ? { 'intentIqId': value } : undefined; + return isValidId(value) ? { 'intentIqId': value } : undefined; }, /** * performs action to obtain id and return a value in the callback's response argument * @function - * @param {SubmoduleParams} [configParams] + * @param {SubmoduleConfig} [config] * @returns {IdResponse|undefined} */ - getId(configParams) { + getId(config) { + const configParams = (config && config.params) || {}; if (!configParams || typeof configParams.partner !== '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`; + 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) : ''; + const resp = function (callback) { const callbacks = { success: response => { - callback(response); + if (isValidId(response)) { + callback(response); + } else { + callback(); + } }, error: error => { utils.logError(`${MODULE_NAME}: ID fetch encountered an error`, error); diff --git a/modules/ironsourceBidAdapter.js b/modules/ironsourceBidAdapter.js index 34650d46a0f..e23c0cb857d 100644 --- a/modules/ironsourceBidAdapter.js +++ b/modules/ironsourceBidAdapter.js @@ -7,7 +7,11 @@ 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/hb'; +const SELLER_ENDPOINT = 'https://hb.yellowblue.io/'; +const MODES = { + PRODUCTION: 'hb', + TEST: 'hb-test' +} const SUPPORTED_SYNC_METHODS = { IFRAME: 'iframe', PIXEL: 'pixel' @@ -44,7 +48,7 @@ export const spec = { creativeId: body.requestId, currency: body.currency, netRevenue: body.netRevenue, - ttl: TTL, + ttl: body.ttl || TTL, vastXml: body.vastXml, mediaType: VIDEO }; @@ -86,9 +90,10 @@ registerBidder(spec); */ function buildVideoRequest(bid, bidderRequest) { const sellerParams = generateParameters(bid, bidderRequest); + const {params} = bid; return { method: 'GET', - url: SELLER_ENDPOINT, + url: getEndpoint(params.testMode), data: sellerParams }; } @@ -169,6 +174,17 @@ function isSyncMethodAllowed(syncRule, 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} diff --git a/modules/ironsourceBidAdapter.md b/modules/ironsourceBidAdapter.md index 2f9e38b69e8..86756b08809 100644 --- a/modules/ironsourceBidAdapter.md +++ b/modules/ironsourceBidAdapter.md @@ -23,6 +23,7 @@ The adapter supports Video(instream). For the integration, IronSource returns co | `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 @@ -42,6 +43,7 @@ var adUnits = [ 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..cb37e4735c5 100644 --- a/modules/ixBidAdapter.js +++ b/modules/ixBidAdapter.js @@ -235,9 +235,9 @@ 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) { @@ -280,11 +280,10 @@ function buildRequest(validBidRequests, bidderRequest, impressions, version) { // 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 = {}; // if an schain is provided, send it along if (validBidRequests[0].schain) { @@ -358,30 +357,140 @@ 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; + } + + trimImpressions(impressions[transactionIds[i]], MAX_REQ_SIZE - BASE_REQ_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; } +/** + * + * @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) { @@ -496,19 +605,32 @@ 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)); + 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)); } else { utils.logError('Bid size is not included in video playerSize') } @@ -516,27 +638,47 @@ export const spec = { if (validBidRequest.mediaType === BANNER || utils.deepAccess(validBidRequest, 'mediaTypes.banner') || (!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); + 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; diff --git a/modules/ixBidAdapter.md b/modules/ixBidAdapter.md index b5cb0d9d2c1..5b9903c91d2 100644 --- a/modules/ixBidAdapter.md +++ b/modules/ixBidAdapter.md @@ -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 index d3ae090964c..7c6e0027482 100644 --- a/modules/jixieBidAdapter.js +++ b/modules/jixieBidAdapter.js @@ -9,7 +9,7 @@ import { Renderer } from '../src/Renderer.js'; export const storage = getStorageManager(); const BIDDER_CODE = 'jixie'; -const EVENTS_URL = 'https://jxhbtrackers.azurewebsites.net/sync/evt?'; +const EVENTS_URL = 'https://hbtra.jixie.io/sync/hb?'; const JX_OUTSTREAM_RENDERER_URL = 'https://scripts.jixie.io/jxhboutstream.js'; const REQUESTS_URL = 'https://hb.jixie.io/v2/hbpost'; const sidTTLMins_ = 30; diff --git a/modules/jwplayerRtdProvider.js b/modules/jwplayerRtdProvider.js index b7c8879ed8e..197c3c192c8 100644 --- a/modules/jwplayerRtdProvider.js +++ b/modules/jwplayerRtdProvider.js @@ -14,11 +14,12 @@ 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'; -let requestCount = 0; -let requestTimeout = 150; const segCache = {}; +const pendingRequests = {}; +let activeRequestCount = 0; let resumeBidRequest; /** @type {RtdSubmodule} */ @@ -29,12 +30,12 @@ export const jwplayerSubmodule = { */ name: SUBMODULE_NAME, /** - * get data and send back to realTimeData module + * add targeting data to bids and signal completion to realTimeData module * @function - * @param {adUnit[]} adUnits + * @param {Obj} bidReqConfig * @param {function} onDone */ - getData: getSegments, + getBidRequestData: enrichBidRequest, init }; @@ -45,14 +46,12 @@ config.getConfig('realTimeData', ({realTimeData}) => { if (!params) { return; } - const rtdModuleTimeout = params.auctionDelay || params.timeout; - requestTimeout = rtdModuleTimeout === undefined ? requestTimeout : Math.max(rtdModuleTimeout - 1, 0); fetchTargetingInformation(params); }); submodule('realTimeData', jwplayerSubmodule); -function init(config, gdpr, usp) { +function init(provider, userConsent) { return true; } @@ -67,40 +66,57 @@ export function fetchTargetingInformation(jwTargeting) { } export function fetchTargetingForMediaId(mediaId) { - const ajax = ajaxBuilder(requestTimeout); - requestCount++; + 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) { - try { - const data = JSON.parse(response); - if (!data) { - throw ('Empty response'); - } - - const playlist = data.playlist; - if (!playlist || !playlist.length) { - throw ('Empty playlist'); - } - - const jwpseg = playlist[0].jwpseg; - if (jwpseg) { - segCache[mediaId] = jwpseg; - } - } catch (err) { - logError(err); - } - onRequestCompleted(); + const segment = parseSegment(response); + cacheSegments(segment, mediaId); + onRequestCompleted(mediaId, !!segment); }, error: function () { logError('failed to retrieve targeting information'); - onRequestCompleted(); + onRequestCompleted(mediaId, false); } }); } -function onRequestCompleted() { - requestCount--; - if (requestCount > 0) { +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; } @@ -110,67 +126,86 @@ function onRequestCompleted() { } } -function getSegments(adUnits, onDone) { - executeAfterPrefetch(() => { - const realTimeData = adUnits.reduce((data, adUnit) => { - const code = adUnit.code; - const vat = code && getTargetingForBid(adUnit); +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('fpd.context.data.jwTargeting'); + adUnits.forEach(adUnit => { + const onVatResponse = function (vat) { if (!vat) { - return data; + return; } + const targeting = formatTargetingResponse(vat); + addTargetingToBids(adUnit.bids, targeting); + }; - const { segments, mediaID } = vat; - const jwTargeting = {}; - if (segments && segments.length) { - jwTargeting.segments = segments; - } + const jwTargeting = extractPublisherParams(adUnit, fpdFallback); + loadVat(jwTargeting, onVatResponse); + }); +} - if (mediaID) { - const id = 'jw_' + mediaID; - jwTargeting.content = { - id - } - } +export function extractPublisherParams(adUnit, fallback) { + let adUnitTargeting; + try { + adUnitTargeting = adUnit.fpd.context.data.jwTargeting; + } catch (e) {} + return Object.assign({}, fallback, adUnitTargeting); +} - data[code] = { - jwTargeting - }; - return data; - }, {}); - onDone(realTimeData); - }); +function loadVat(params, onCompletion) { + if (!params || !Object.keys(params).length) { + return; + } + + const { playerID, mediaID } = params; + if (pendingRequests[mediaID] !== undefined) { + loadVatForPendingRequest(playerID, mediaID, onCompletion); + return; + } + + const vat = getVatFromCache(mediaID) || getVatFromPlayer(playerID, mediaID) || { mediaID }; + onCompletion(vat); } -function executeAfterPrefetch(callback) { - if (requestCount > 0) { - resumeBidRequest = callback; +function loadVatForPendingRequest(playerID, mediaID, callback) { + const vat = getVatFromPlayer(playerID, mediaID); + if (vat) { + callback(vat); } else { - callback(); + activeRequestCount++; + pendingRequests[mediaID] = callback; } } -/** - * Retrieves the targeting information pertaining to a bid request. - * @param bidRequest {object} - the bid which is passed to a prebid adapter for use in `buildRequests`. It must contain - * a jwTargeting property. - * @returns targetingInformation {object} nullable - contains the media ID as well as the jwpseg targeting segments - * found for the given bidRequest information - */ -export function getTargetingForBid(bidRequest) { - const jwTargeting = bidRequest.jwTargeting; - if (!jwTargeting) { +export function getVatFromCache(mediaID) { + const segments = segCache[mediaID]; + + if (!segments) { return null; } - const playerID = jwTargeting.playerID; - let mediaID = jwTargeting.mediaID; - let segments = segCache[mediaID]; - if (segments) { - return { - segments, - mediaID - }; - } + return { + segments, + mediaID + }; +} + +export function getVatFromPlayer(playerID, mediaID) { const player = getPlayer(playerID); if (!player) { return null; @@ -182,10 +217,8 @@ export function getTargetingForBid(bidRequest) { } mediaID = mediaID || item.mediaid; - segments = item.jwpseg; - if (segments && mediaID) { - segCache[mediaID] = segments; - } + const segments = item.jwpseg; + cacheSegments(segments, mediaID) return { segments, @@ -193,6 +226,37 @@ export function getTargetingForBid(bidRequest) { }; } +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) { diff --git a/modules/jwplayerRtdProvider.md b/modules/jwplayerRtdProvider.md index 06a7f69f497..ae09277979a 100644 --- a/modules/jwplayerRtdProvider.md +++ b/modules/jwplayerRtdProvider.md @@ -1,8 +1,8 @@ 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 VPB product. This prebid module makes JW Player's video ad targeting information accessible +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:** +#Usage for Publishers: Compile the JW Player RTD Provider into your Prebid build: @@ -25,26 +25,22 @@ pbjs.setConfig({ } }); ``` - -In order to prefetch targeting information for certain media, include the media IDs in the `jwplayerDataProvider` var: - -```javascript -const jwplayerDataProvider = { - name: "jwplayer", - params: { - mediaIDs: ['abc', 'def', 'ghi', 'jkl'] - } -}; -``` -Lastly, include the content's media ID and/or the player's ID in the matching AdUnit: +Lastly, include the content's media ID and/or the player's ID in the matching AdUnit's `fpd.context.data`: ```javascript const adUnit = { code: '/19968336/prebid_native_example_1', ... - jwTargeting: { - playerID: 'abcd', - mediaID: '1234' + fpd: { + context: { + data: { + jwTargeting: { + // Note: the following Ids are placeholders and should be replaced with your Ids. + playerID: 'abcd', + mediaID: '1234' + } + } + } } }; @@ -56,25 +52,50 @@ pbjs.que.push(function() { }); ``` -**Usage for Bid Adapters:** +**Note**: You may also include `jwTargeting` information in the prebid config's `fpd.context.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', - ... - realTimeData: { - ..., - jwTargeting: { - segments: ['123', '456'], - content: { - id: 'jw_abc123' - } - } - } + adUnitCode: 'xyz', + bidId: 'abc', + ..., + rtd: { + jwplayer: { + targeting: { + segments: ['123', '456'], + content: { + id: 'jw_abc123' + } + } + } + } } ``` @@ -94,3 +115,5 @@ To view an example: - 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. 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..5941802f97d 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,29 @@ export var spec = { interpretResponse: (response, request) => { return parseRTBResponse(request, response.body); }, + getUserSyncs: (syncOptions, responses, gdprConsent, uspConsent) => { + let syncurl = USER_SYNC + 'pid=' + pubId; + + // Attaching GDPR Consent Params in UserSync url + if (gdprConsent) { + syncurl += '&gdpr=' + (gdprConsent.gdprApplies ? 1 : 0); + syncurl += '&gdpr_consent=' + encodeURIComponent(gdprConsent.consentString || ''); + } + + // CCPA + if (uspConsent) { + syncurl += '&us_privacy=' + encodeURIComponent(uspConsent); + } + + if (syncOptions.iframeEnabled) { + return [{ + type: 'iframe', + url: syncurl + }]; + } else { + utils.logWarn(LOG_WARN_PREFIX + 'Please enable iframe based user sync.'); + } + }, }; function _initConf(refererInfo) { @@ -167,8 +193,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 +209,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 +230,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 { diff --git a/modules/liveIntentIdSystem.js b/modules/liveIntentIdSystem.js index bd2638e5936..4f18c73ad2a 100644 --- a/modules/liveIntentIdSystem.js +++ b/modules/liveIntentIdSystem.js @@ -5,14 +5,32 @@ * @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 { gdprDataHandler, uspDataHandler } from '../src/adapterManager.js'; import { getStorageManager } from '../src/storageManager.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; @@ -64,18 +82,30 @@ 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'; liveConnectConfig.identityResolutionConfig = identityResolutionConfig; liveConnectConfig.identifiersToResolve = configParams.identifiersToResolve || []; + if (configParams.emailHash) { + liveConnectConfig.eventSource = { hash: configParams.emailHash } + } const usPrivacyString = uspDataHandler.getConsentData(); 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 = LiveConnect(liveConnectConfig, storage, calls); return liveConnect; } @@ -100,10 +130,11 @@ export const liveIntentIdSubmodule = { * `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'] }; delete value.unifiedId; @@ -121,20 +152,19 @@ 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 => { + const result = function(callback) { + liveConnect.resolve( + response => { let responseObj = {}; if (response) { try { @@ -145,14 +175,14 @@ export const liveIntentIdSubmodule = { } callback(responseObj); }, - 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 9f571cb5ae0..a872a709ec9 100644 --- a/modules/livewrappedAnalyticsAdapter.js +++ b/modules/livewrappedAnalyticsAdapter.js @@ -36,6 +36,15 @@ let livewrappedAnalyticsAdapter = Object.assign(adapter({EMPTYURL, ANALYTICSTYPE 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, @@ -45,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); @@ -123,10 +136,11 @@ livewrappedAnalyticsAdapter.sendEvents = function() { var events = { publisherId: initOptions.publisherId, gdpr: sentRequests.gdpr, + auctionIds: sentRequests.auctionIds, requests: sentRequests.sentRequests, - responses: getResponses(), - wins: getWins(), - timeouts: getTimeouts(), + responses: getResponses(sentRequests.gdpr, sentRequests.auctionIds), + wins: getWins(sentRequests.gdpr, sentRequests.auctionIds), + timeouts: getTimeouts(sentRequests.auctionIds), bidAdUnits: getbidAdUnits(), rcv: getAdblockerRecovered() }; @@ -150,20 +164,12 @@ function getAdblockerRecovered() { function getSentRequests() { var sentRequests = []; var gdpr = []; + var auctionIds = []; Object.keys(cache.auctions).forEach(auctionId => { let auction = cache.auctions[auctionId]; - 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}; - } + let gdprPos = getGdprPos(gdpr, auction); + let auctionIdPos = getAuctionIdPos(auctionIds, auctionId); Object.keys(cache.auctions[auctionId].bids).forEach(bidId => { let bid = auction.bids[bidId]; @@ -174,21 +180,27 @@ function getSentRequests() { timeStamp: auction.timeStamp, adUnit: bid.adUnit, bidder: bid.bidder, - gdpr: gdprPos + gdpr: gdprPos, + floor: bid.lwFloor, + auctionId: auctionIdPos, + auc: bid.auc, + buc: bid.buc }); } }); }); - return {gdpr: gdpr, sentRequests: 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; @@ -202,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 }); } }); @@ -211,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; @@ -228,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 }); } }); @@ -237,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]; @@ -250,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 50e9eea768b..512fc775d95 100644 --- a/modules/livewrappedBidAdapter.js +++ b/modules/livewrappedBidAdapter.js @@ -221,6 +221,10 @@ function bidToAdRequest(bid) { options: bid.params.options }; + if (bid.auc !== undefined) { + adRequest.auc = bid.auc; + } + adRequest.native = utils.deepAccess(bid, 'mediaTypes.native'); adRequest.video = utils.deepAccess(bid, 'mediaTypes.video'); @@ -276,8 +280,9 @@ 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); + AddExternalUserId(eids, utils.deepAccess(bidRequest, `userId.pubcid`), 'pubcid.org', 1); // Also add this to eids + AddExternalUserId(eids, utils.deepAccess(bidRequest, `userId.id5id.uid`), 'id5-sync.com', 1); + AddExternalUserId(eids, utils.deepAccess(bidRequest, `userId.criteoId`), 'criteo.com', 1); } if (eids.length > 0) { return {user: {ext: {eids}}}; diff --git a/modules/livewrappedBidAdapter.md b/modules/livewrappedBidAdapter.md index c5d867af8fe..10fc2a4778a 100644 --- a/modules/livewrappedBidAdapter.md +++ b/modules/livewrappedBidAdapter.md @@ -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/lotamePanoramaIdSystem.js b/modules/lotamePanoramaIdSystem.js index cdf9131dd68..5c3a9a16b3a 100644 --- a/modules/lotamePanoramaIdSystem.js +++ b/modules/lotamePanoramaIdSystem.js @@ -152,21 +152,22 @@ export const lotamePanoramaIdSubmodule = { * 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) { + 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) { let localCache = getLotameLocalCache(); let refreshNeeded = Date.now() > localCache.expiryTimestampMs; 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/malltvBidAdapter.js b/modules/malltvBidAdapter.js index 4cdb5d45328..846935a5522 100644 --- a/modules/malltvBidAdapter.js +++ b/modules/malltvBidAdapter.js @@ -1,4 +1,5 @@ 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'; @@ -7,6 +8,7 @@ const SIZE_SEPARATOR = ';'; export const spec = { code: BIDDER_CODE, + supportedMediaTypes: [BANNER, VIDEO], /** * Determines whether or not the given bid request is valid. * @@ -23,31 +25,48 @@ export const spec = { * @return ServerRequest Info describing the request to the server. */ buildRequests: function (validBidRequests, bidderRequest) { - let response = validBidRequests.map(bidRequest => { - let sizes = generateSizeParam(bidRequest.sizes); - let propertyId = bidRequest.params.propertyId; - let placementId = bidRequest.params.placementId; + let propertyId = ''; + let pageViewGuid = ''; + let storageId = ''; + let bidderRequestId = ''; + let url = ''; + let contents = []; + + 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 } + let adUnitId = bidRequest.adUnitCode; - let pageViewGuid = bidRequest.params.pageViewGuid || ''; - let contents = bidRequest.params.contents || []; - const body = { + let placementId = bidRequest.params.placementId; + let sizes = generateSizeParam(bidRequest.sizes); + + return { sizes: sizes, adUnitId: adUnitId, placementId: placementId, - propertyId: propertyId, - pageViewGuid: pageViewGuid, - url: bidderRequest ? bidderRequest.refererInfo.referer : '', - requestid: bidRequest.bidderRequestId, bidid: bidRequest.bidId, - contents: contents - }; - return { - method: 'POST', - url: ENDPOINT_URL, - data: body }; }); - return response + + let body = { + propertyId: propertyId, + pageViewGuid: pageViewGuid, + storageId: storageId, + url: url, + requestid: bidderRequestId, + placements: placements, + contents: contents + } + + return [{ + method: 'POST', + url: ENDPOINT_URL, + data: body + }]; }, /** * Unpack the response from the server into a list of bids. @@ -55,13 +74,12 @@ export const spec = { * @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) { - window.adnResponse = serverResponse; + interpretResponse: function (serverResponse) { const responses = serverResponse.body; const bidResponses = []; for (var i = 0; i < responses.length; i++) { const bidResponse = { - requestId: bidRequest.data.bidid, + requestId: responses[i].BidId, cpm: responses[i].CPM, width: responses[i].Width, height: responses[i].Height, @@ -70,7 +88,9 @@ export const spec = { netRevenue: responses[i].NetRevenue, ttl: responses[i].TTL, referrer: responses[i].Referrer, - ad: responses[i].Ad + ad: responses[i].Ad, + vastUrl: responses[i].VastUrl, + mediaType: responses[i].MediaType }; bidResponses.push(bidResponse); } diff --git a/modules/malltvBidAdapter.md b/modules/malltvBidAdapter.md index 72db0cef6c7..3d419fa0916 100644 --- a/modules/malltvBidAdapter.md +++ b/modules/malltvBidAdapter.md @@ -28,22 +28,16 @@ var adUnits = [ { code: 'test-div', mediaTypes: { - banner: { - sizes: [[300, 250], [300, 300]] - } + video: { + context: 'instream' + } }, bids: [ { bidder: 'malltv', params: { propertyId: '105134', - placementId: '846832', - contents: [ //optional - { - type: 'video', - id: '123' - } - ] + placementId: '846841' } } ] diff --git a/modules/mediaforceBidAdapter.js b/modules/mediaforceBidAdapter.js index 7e5c06b1b48..7e29bb519b5 100644 --- a/modules/mediaforceBidAdapter.js +++ b/modules/mediaforceBidAdapter.js @@ -114,18 +114,28 @@ export const spec = { } const referer = bidderRequest && bidderRequest.refererInfo ? encodeURIComponent(bidderRequest.refererInfo.referer) : ''; + const auctionId = bidderRequest && bidderRequest.auctionId; + const timeout = bidderRequest && bidderRequest.timeout; const dnt = utils.getDNT() ? 1 : 0; - let requests = []; + const requestsMap = {}; + const requests = []; + let isTest = false; validBidRequests.forEach(bid => { + isTest = isTest || bid.params.is_test; let tagid = bid.params.placement_id; let bidfloor = bid.params.bidfloor ? parseFloat(bid.params.bidfloor) : 0; - let imp = []; let validImp = false; let impObj = { id: bid.bidId, tagid: tagid, - secure: 1, + secure: window.location.protocol === 'https' ? 1 : 0, bidfloor: bidfloor, + ext: { + mediaforce: { + transactionId: bid.transactionId + } + } + }; for (let mediaTypes in bid.mediaTypes) { switch (mediaTypes) { @@ -140,31 +150,47 @@ export const spec = { default: return; } } - validImp && imp.push(impObj); - let request = { - id: bid.transactionId, - site: { - page: referer, - ref: referer, - id: bid.params.publisher_id, - publisher: { - id: bid.params.publisher_id + let request = requestsMap[bid.params.publisher_id]; + if (!request) { + request = { + id: Math.round(Math.random() * 1e16).toString(16), + site: { + page: window.location.href, + ref: referer, + id: bid.params.publisher_id, + publisher: { + id: bid.params.publisher_id + }, }, - }, - device: { - ua: navigator.userAgent, - js: 1, - dnt: dnt, - language: getLanguage() - }, - imp - }; - requests.push({ - method: 'POST', - url: bid.params.is_test ? TEST_ENDPOINT_URL : ENDPOINT_URL, - data: JSON.stringify(request) - }); + device: { + ua: navigator.userAgent, + js: 1, + dnt: dnt, + language: getLanguage() + }, + ext: { + mediaforce: { + hb_key: auctionId + } + }, + tmax: timeout, + imp: [] + }; + requestsMap[bid.params.publisher_id] = request; + requests.push({ + method: 'POST', + url: ENDPOINT_URL, + data: request + }); + } + validImp && request.imp.push(impObj); + }); + requests.forEach((req) => { + if (isTest) { + req.url = TEST_ENDPOINT_URL; + } + req.data = JSON.stringify(req.data); }); return requests; }, diff --git a/modules/medianetAnalyticsAdapter.js b/modules/medianetAnalyticsAdapter.js index 62958cfbfd2..35c9273f951 100644 --- a/modules/medianetAnalyticsAdapter.js +++ b/modules/medianetAnalyticsAdapter.js @@ -11,6 +11,7 @@ const ENDPOINT = 'https://pb-logs.media.net/log?logid=kfk&evtid=prebid_analytics const CONFIG_URL = 'https://prebid.media.net/rtb/prebid/analytics/config'; const EVENT_PIXEL_URL = 'https://qsearch-a.akamaihd.net/log'; const DEFAULT_LOGGING_PERCENT = 50; +const ANALYTICS_VERSION = '1.0.0'; const PRICE_GRANULARITY = { 'auto': 'pbAg', @@ -39,6 +40,18 @@ const CONFIG_ERROR = 3; const VALID_URL_KEY = ['canonical_url', 'og_url', 'twitter_url']; const DEFAULT_URL_KEY = 'page'; +const LOG_TYPE = { + AP: 'AP', + PR: 'PR', + APPR: 'APPR', + RA: 'RA' +}; + +const BATCHING = { + SINGLE: 'SINGLE', + MULTI: 'MULTI' +} + let auctions = {}; let config; let pageDetails; @@ -73,7 +86,14 @@ class Configure { this.urlToConsume = DEFAULT_URL_KEY; this.debug = false; this.gdprConsent = undefined; + this.gdprApplies = undefined; this.uspConsent = undefined; + this.pixelWaitTime = 0; + this.apLoggingPct = 0; + this.prLoggingPct = 0; + this.batching = BATCHING.SINGLE; + this.shouldBeLogged = {}; + this.mnetDebugConfig = ''; } set publisherLper(plper) { @@ -85,10 +105,12 @@ class Configure { cid: this.cid, lper: Math.round(100 / this.loggingPercent), plper: this.pubLper, - gdpr: this.gdprConsent, + gdpr: this.gdprApplies ? '1' : '0', + gdprConsent: this.gdprConsent, ccpa: this.uspConsent, ajx: this.ajaxState, pbv: PREBID_VERSION, + pbav: ANALYTICS_VERSION, flt: 1, } } @@ -100,10 +122,9 @@ class Configure { _parseResponse(response) { try { response = JSON.parse(response); - if (isNaN(response.percentage)) { - throw new Error('not a number'); - } - this.loggingPercent = response.percentage; + this.setDataFromResponse(response); + this.overrideDomainLevelData(response); + this.overrideToDebug(this.mnetDebugConfig); this.urlToConsume = VALID_URL_KEY.includes(response.urlKey) ? response.urlKey : this.urlToConsume; this.ajaxState = CONFIG_PASS; } catch (e) { @@ -113,6 +134,41 @@ class Configure { } } + setDataFromResponse(response) { + if (!isNaN(parseInt(response.percentage, 10))) { + this.loggingPercent = response.percentage; + } + + if (!isNaN(parseInt(response.pixelwaittime, 10))) { + this.pixelWaitTime = response.pixelwaittime; + } + + if (!isNaN(parseInt(response.aplper, 10))) { + this.apLoggingPct = response.aplper; + this.batching = BATCHING.MULTI; + } + + if (!isNaN(parseInt(response.prlper, 10))) { + this.prLoggingPct = response.prlper; + this.batching = BATCHING.MULTI; + } + } + + overrideDomainLevelData(response) { + const domain = utils.deepAccess(response, 'domain.' + pageDetails.domain); + if (domain) { + this.setDataFromResponse(domain); + } + } + + overrideToDebug(response) { + if (response === '') return; + try { + this.setDataFromResponse(JSON.parse(decodeURIComponent(response))); + } catch (e) { + } + } + _errorFetch() { this.ajaxState = CONFIG_ERROR; /* eslint no-new: "error" */ @@ -128,6 +184,9 @@ class Configure { this.debug = true; return; } + if (utils.deepAccess(urlObj, 'search.mnet_setconfig')) { + this.mnetDebugConfig = utils.deepAccess(urlObj, 'search.mnet_setconfig'); + } ajax( this._configURL(), { @@ -145,7 +204,7 @@ class PageDetail { const twitterUrl = this._getUrlFromSelector('meta[name="twitter:url"]', 'content'); const refererInfo = getRefererInfo(); - this.domain = URL.parseUrl(refererInfo.referer).host; + this.domain = URL.parseUrl(refererInfo.referer).hostname; this.page = refererInfo.referer; this.is_top = refererInfo.reachedTop; this.referrer = this._getTopWindowReferrer(); @@ -202,39 +261,34 @@ class PageDetail { } class AdSlot { - constructor(mediaTypes, allMediaTypeSizes, tmax, supplyAdCode, adext, context, adSize) { - this.mediaTypes = mediaTypes; - this.allMediaTypeSizes = allMediaTypeSizes; + constructor(tmax, supplyAdCode, context, adext) { this.tmax = tmax; this.supplyAdCode = supplyAdCode; + this.context = context; this.adext = adext; - this.logged = false; + this.logged = {}; + this.logged[LOG_TYPE.PR] = false; + this.logged[LOG_TYPE.AP] = false; + this.logged[LOG_TYPE.APPR] = false; this.targeting = undefined; this.medianetPresent = 0; - // shouldBeLogged is assigned when requested, - // since we are waiting for logging percent response - this.shouldBeLogged = undefined; - this.context = context; - this.adSize = adSize; // old ad unit sizes } - getShouldBeLogged() { - if (this.shouldBeLogged === undefined) { - this.shouldBeLogged = isSampled(); + getShouldBeLogged(logType) { + if (!config.shouldBeLogged.hasOwnProperty(logType)) { + config.shouldBeLogged[logType] = isSampled(logType); } - return this.shouldBeLogged; + config.shouldBeLogged[logType] = isSampled(logType); + return config.shouldBeLogged[logType]; } getLoggingData() { return Object.assign({ supcrid: this.supplyAdCode, - mediaTypes: this.mediaTypes && this.mediaTypes.join('|'), - szs: this.allMediaTypeSizes.map(sz => sz.join('x')).join('|'), tmax: this.tmax, targ: JSON.stringify(this.targeting), ismn: this.medianetPresent, vplcmtt: this.context, - sz2: this.adSize.map(sz => sz.join('x')).join('|'), }, this.adext && {'adext': JSON.stringify(this.adext)}, ); @@ -242,12 +296,13 @@ class AdSlot { } class Bid { - constructor(bidId, bidder, src, start, supplyAdCode) { + constructor(bidId, bidder, src, start, adUnitCode, mediaType, allMediaTypeSizes) { this.bidId = bidId; this.bidder = bidder; this.src = src; this.start = start; - this.supplyAdCode = supplyAdCode; + this.adUnitCode = adUnitCode; + this.allMediaTypeSizes = allMediaTypeSizes; this.iwb = 0; this.winner = 0; this.status = bidder === DUMMY_BIDDER ? BID_SUCCESS : BID_TIMEOUT; @@ -257,7 +312,7 @@ class Bid { this.dfpbd = undefined; this.width = undefined; this.height = undefined; - this.mediaType = undefined; + this.mediaType = mediaType; this.timeToRespond = undefined; this.dealId = undefined; this.creativeId = undefined; @@ -268,6 +323,7 @@ class Bid { this.mpvid = undefined; this.floorPrice = undefined; this.floorRule = undefined; + this.serverLatencyMillis = undefined; } get size() { @@ -286,6 +342,7 @@ class Bid { bdp: this.cpm, cbdp: this.dfpbd, dfpbd: this.dfpbd, + szs: this.allMediaTypeSizes.map(sz => sz.join('x')).join('|'), size: this.size, mtype: this.mediaType, dId: this.dealId, @@ -299,7 +356,8 @@ class Bid { mpvid: this.mpvid, bidflr: this.floorPrice, flrrule: this.floorRule, - ext: JSON.stringify(this.ext) + ext: JSON.stringify(this.ext), + rtime: this.serverLatencyMillis, } } } @@ -342,12 +400,10 @@ class Auction { } } - addSlot(supplyAdCode, { mediaTypes, allMediaTypeSizes, tmax, adext, context, adSize }) { - if (supplyAdCode && this.adSlots[supplyAdCode] === undefined) { - this.adSlots[supplyAdCode] = new AdSlot(mediaTypes, allMediaTypeSizes, tmax, supplyAdCode, adext, context, adSize); - this.addBid( - new Bid('-1', DUMMY_BIDDER, 'client', '-1', supplyAdCode) - ); + addSlot({ adUnitCode, supplyAdCode, mediaTypes, allMediaTypeSizes, tmax, adext, context }) { + if (adUnitCode && this.adSlots[adUnitCode] === undefined) { + this.adSlots[adUnitCode] = new AdSlot(tmax, supplyAdCode, context, adext); + this.addBid(new Bid('-1', DUMMY_BIDDER, 'client', '-1', adUnitCode, mediaTypes, allMediaTypeSizes)); } } @@ -363,7 +419,7 @@ class Auction { getAdslotBids(adslot) { return this.bids - .filter((bid) => bid.supplyAdCode === adslot) + .filter((bid) => bid.adUnitCode === adslot) .map((bid) => bid.getLoggingData()); } @@ -382,46 +438,68 @@ class Auction { } } -function auctionInitHandler({auctionId, timestamp, bidderRequests}) { +function auctionInitHandler({auctionId, adUnits, timeout, timestamp, bidderRequests}) { if (auctionId && auctions[auctionId] === undefined) { auctions[auctionId] = new Auction(auctionId); auctions[auctionId].auctionInitTime = timestamp; } + addAddSlots(auctionId, adUnits, timeout); const floorData = utils.deepAccess(bidderRequests, '0.bids.0.floorData'); if (floorData) { auctions[auctionId].floorData = {...floorData}; } } -function bidRequestedHandler({ auctionId, auctionStart, bids, start, timeout, uspConsent, gdpr }) { +function addAddSlots(auctionId, adUnits, tmax) { + adUnits = adUnits || []; + const groupedAdUnits = utils.groupBy(adUnits, 'code'); + Object.keys(groupedAdUnits).forEach((adUnitCode) => { + const adUnits = groupedAdUnits[adUnitCode]; + const supplyAdCode = utils.deepAccess(adUnits, '0.adUnitCode') || adUnitCode; + let context = ''; + let adext = {}; + + const mediaTypeMap = {}; + const oSizes = {banner: [], video: []}; + adUnits.forEach(({mediaTypes, sizes, ext}) => { + mediaTypes = mediaTypes || {}; + adext = Object.assign(adext, ext || utils.deepAccess(mediaTypes, 'banner.ext')); + context = utils.deepAccess(mediaTypes, 'video.context') || context; + Object.keys(mediaTypes).forEach((mediaType) => mediaTypeMap[mediaType] = 1); + const sizeObject = _getSizes(mediaTypes, sizes); + sizeObject.banner.forEach(size => oSizes.banner.push(size)); + sizeObject.video.forEach(size => oSizes.video.push(size)); + }); + + adext = utils.isEmpty(adext) ? undefined : adext; + oSizes.banner = oSizes.banner.filter(utils.uniques); + oSizes.video = oSizes.video.filter(utils.uniques); + oSizes.native = mediaTypeMap.native === 1 ? [[1, 1]] : []; + const allMediaTypeSizes = [].concat(oSizes.banner, oSizes.native, oSizes.video); + const mediaTypes = Object.keys(mediaTypeMap).join('|'); + + auctions[auctionId].addSlot({adUnitCode, supplyAdCode, mediaTypes, allMediaTypeSizes, context, tmax, adext}); + }); +} + +function bidRequestedHandler({ auctionId, auctionStart, bids, start, uspConsent, gdpr }) { if (!(auctions[auctionId] instanceof Auction)) { return; } - if (gdpr && gdpr.gdprApplies) { + config.gdprApplies = !!(gdpr && gdpr.gdprApplies); + if (config.gdprApplies) { config.gdprConsent = gdpr.consentString || ''; } config.uspConsent = config.uspConsent || uspConsent; + auctions[auctionId].auctionStartTime = auctionStart; bids.forEach(bid => { - const { adUnitCode, bidder, mediaTypes, sizes, bidId, src } = bid; - if (!auctions[auctionId].adSlots[adUnitCode]) { - auctions[auctionId].auctionStartTime = auctionStart; - const sizeObject = _getSizes(mediaTypes, sizes); - auctions[auctionId].addSlot( - adUnitCode, - Object.assign({}, - (mediaTypes instanceof Object) && { mediaTypes: Object.keys(mediaTypes) }, - { allMediaTypeSizes: [].concat(sizeObject.banner, sizeObject.native, sizeObject.video) }, - { adext: utils.deepAccess(mediaTypes, 'banner.ext') || '' }, - { tmax: timeout }, - { context: utils.deepAccess(mediaTypes, 'video.context') || '' }, - { adSize: sizeObject.banner } - ) - ); - } - let bidObj = new Bid(bidId, bidder, src, start, adUnitCode); + const { adUnitCode, bidder, bidId, src, mediaTypes, sizes } = bid; + const sizeObject = _getSizes(mediaTypes, sizes); + const requestSizes = [].concat(sizeObject.banner, sizeObject.native, sizeObject.video); + const bidObj = new Bid(bidId, bidder, src, start, adUnitCode, mediaTypes && Object.keys(mediaTypes).join('|'), requestSizes); auctions[auctionId].addBid(bidObj); if (bidder === MEDIANET_BIDDER_CODE) { bidObj.crid = utils.deepAccess(bid, 'params.crid'); @@ -482,6 +560,9 @@ function bidResponseHandler(bid) { bid.ext.crid && { 'crid': bid.ext.crid } ); } + if (typeof bid.serverResponseTimeMs !== 'undefined') { + bidObj.serverLatencyMillis = bid.serverResponseTimeMs; + } } function noBidResponseHandler({ auctionId, bidId }) { @@ -511,12 +592,18 @@ function bidTimeoutHandler(timedOutBids) { }) } -function auctionEndHandler({ auctionId, auctionEnd }) { +function auctionEndHandler({auctionId, auctionEnd, adUnitCodes}) { if (!(auctions[auctionId] instanceof Auction)) { return; } auctions[auctionId].status = AUCTION_COMPLETED; auctions[auctionId].auctionEndTime = auctionEnd; + + if (config.batching === BATCHING.MULTI) { + adUnitCodes.forEach(function (adUnitCode) { + sendEvent(auctionId, adUnitCode, LOG_TYPE.PR); + }); + } } function setTargetingHandler(params) { @@ -530,20 +617,51 @@ function setTargetingHandler(params) { adunitObj.targeting = params[adunit]; auctionObj.setTargetingTime = Date.now(); let targetingObj = Object.keys(params[adunit]).reduce((result, key) => { - if (key.indexOf('hb_adid') !== -1) { + if (key.indexOf(CONSTANTS.TARGETING_KEYS.AD_ID) !== -1) { result[key] = params[adunit][key] } return result; }, {}); + const winnerAdId = params[adunit][CONSTANTS.TARGETING_KEYS.AD_ID]; + let winningBid; let bidAdIds = Object.keys(targetingObj).map(k => targetingObj[k]); auctionObj.bids.filter((bid) => bidAdIds.indexOf(bid.adId) !== -1).map(function(bid) { bid.iwb = 1; + if (bid.adId === winnerAdId) { + winningBid = bid; + } + }); + auctionObj.bids.forEach(bid => { + if (bid.bidder === DUMMY_BIDDER && bid.adUnitCode === adunit) { + bid.iwb = bidAdIds.length === 0 ? 0 : 1; + bid.width = utils.deepAccess(winningBid, 'width'); + bid.height = utils.deepAccess(winningBid, 'height'); + } }); - sendEvent(auctionId, adunit, false); + sendEvent(auctionId, adunit, getLogType()); } } } +function getLogType() { + if (config.batching === BATCHING.SINGLE) { + return LOG_TYPE.APPR; + } + return LOG_TYPE.AP; +} + +function setBidderDone(params) { + if (config.pixelWaitTime != null && config.pixelWaitTime > 0) { + setTimeout(fireApAfterWait, config.pixelWaitTime, params) + } +} + +function fireApAfterWait(params) { + params.bids.forEach(function (adUnit) { + sendEvent(params.auctionId, adUnit.adUnitCode, LOG_TYPE.AP); + }); +} + function bidWonHandler(bid) { const { requestId, auctionId, adUnitCode } = bid; if (!(auctions[auctionId] instanceof Auction)) { @@ -555,26 +673,50 @@ function bidWonHandler(bid) { } auctions[auctionId].bidWonTime = Date.now(); bidObj.winner = 1; - sendEvent(auctionId, adUnitCode, true); + sendEvent(auctionId, adUnitCode, LOG_TYPE.RA); +} + +function isSampled(logType) { + return Math.random() * 100 < parseFloat(getLogPercentage(logType)); } -function isSampled() { - return Math.random() * 100 < parseFloat(config.loggingPercent); +function getLogPercentage(logType) { + let logPercentage = config.loggingPercent; + if (config.batching === BATCHING.MULTI) { + if (logType === LOG_TYPE.AP) { + logPercentage = config.apLoggingPct; + } else if (logType === LOG_TYPE.PR) { + logPercentage = config.prLoggingPct; + } + } + return logPercentage; } function isValidAuctionAdSlot(acid, adtag) { return (auctions[acid] instanceof Auction) && (auctions[acid].adSlots[adtag] instanceof AdSlot); } -function sendEvent(id, adunit, isBidWonEvent) { +function sendEvent(id, adunit, logType) { if (!isValidAuctionAdSlot(id, adunit)) { return; } - if (isBidWonEvent) { - fireAuctionLog(id, adunit, isBidWonEvent); - } else if (auctions[id].adSlots[adunit].getShouldBeLogged() && !auctions[id].adSlots[adunit].logged) { - auctions[id].adSlots[adunit].logged = true; - fireAuctionLog(id, adunit, isBidWonEvent); + if (logType === LOG_TYPE.RA) { + fireAuctionLog(id, adunit, logType); + } else { + fireApPrLog(id, adunit, logType) + } +} + +function fireApPrLog(auctionId, adUnitName, logType) { + const adSlot = auctions[auctionId].adSlots[adUnitName]; + if (adSlot.getShouldBeLogged(logType) && !adSlot.logged[logType]) { + if (config.batching === BATCHING.SINGLE) { + adSlot.logged[LOG_TYPE.AP] = true; + adSlot.logged[LOG_TYPE.PR] = true; + } else { + adSlot.logged[logType] = true; + } + fireAuctionLog(auctionId, adUnitName, logType); } } @@ -585,8 +727,9 @@ function getCommonLoggingData(acid, adtag) { return Object.assign(commonParams, adunitParams, auctionParams); } -function fireAuctionLog(acid, adtag, isBidWonEvent) { +function fireAuctionLog(acid, adtag, logType) { let commonParams = getCommonLoggingData(acid, adtag); + commonParams.lgtp = logType; let targeting = utils.deepAccess(commonParams, 'targ'); Object.keys(commonParams).forEach((key) => (commonParams[key] == null) && delete commonParams[key]); @@ -594,7 +737,7 @@ function fireAuctionLog(acid, adtag, isBidWonEvent) { let bidParams; - if (isBidWonEvent) { + if (logType === LOG_TYPE.RA) { bidParams = auctions[acid].getWinnerAdslotBid(adtag); commonParams.lper = 1; } else { @@ -700,6 +843,10 @@ let medianetAnalytics = Object.assign(adapter({URL, analyticsType}), { setTargetingHandler(args); break; } + case CONSTANTS.EVENTS.BIDDER_DONE : { + setBidderDone(args); + break; + } case CONSTANTS.EVENTS.BID_WON: { bidWonHandler(args); break; @@ -728,7 +875,8 @@ medianetAnalytics.enableAnalytics = function (configuration) { adapterManager.registerAnalyticsAdapter({ adapter: medianetAnalytics, - code: 'medianetAnalytics' + code: 'medianetAnalytics', + gvlid: 142, }); export default medianetAnalytics; diff --git a/modules/medianetBidAdapter.js b/modules/medianetBidAdapter.js index 5decaa148e3..a2dc8bdfd03 100644 --- a/modules/medianetBidAdapter.js +++ b/modules/medianetBidAdapter.js @@ -3,9 +3,11 @@ import * as utils from '../src/utils.js'; import { config } from '../src/config.js'; import { BANNER, NATIVE, VIDEO } from '../src/mediaTypes.js'; import { getRefererInfo } from '../src/refererDetection.js'; +import { Renderer } from '../src/Renderer.js'; const BIDDER_CODE = 'medianet'; const BID_URL = 'https://prebid.media.net/rtb/prebid'; +const PLAYER_URL = 'https://prebid.media.net/video/bundle.js'; const SLOT_VISIBILITY = { NOT_DETERMINED: 0, ABOVE_THE_FOLD: 1, @@ -16,10 +18,14 @@ const EVENTS = { BID_WON_EVENT_NAME: 'client_bid_won' }; const EVENT_PIXEL_URL = 'qsearch-a.akamaihd.net/log'; - +const OUTSTREAM = 'outstream'; let refererInfo = getRefererInfo(); let mnData = {}; + +window.mnet = window.mnet || {}; +window.mnet.queue = window.mnet.queue || []; + mnData.urlData = { domain: utils.parseUrl(refererInfo.referer).hostname, page: refererInfo.referer, @@ -113,8 +119,15 @@ function getWindowSize() { } } -function getCoordinates(id) { - const element = document.getElementById(id); +function getCoordinates(adUnitCode) { + let element = document.getElementById(adUnitCode); + if (!element && adUnitCode.indexOf('/') !== -1) { + // now it means that adUnitCode is GAM AdUnitPath + const {divId} = utils.getGptSlotInfoForAdUnitCode(adUnitCode); + if (utils.isStr(divId)) { + element = document.getElementById(divId); + } + } if (element && element.getBoundingClientRect) { const rect = element.getBoundingClientRect(); let coordinates = {}; @@ -191,7 +204,7 @@ function slotParams(bidRequest) { params.tagid = bidRequest.params.crid.toString(); } - let bidFloor = parseFloat(bidRequest.params.bidfloor); + let bidFloor = parseFloat(bidRequest.params.bidfloor || bidRequest.params.bidFloor); if (bidFloor) { params.bidfloor = bidFloor; } @@ -314,6 +327,40 @@ function clearMnData() { mnData = {}; } +function addRenderer(bid) { + const videoContext = utils.deepAccess(bid, 'context') || ''; + const vastTimeout = utils.deepAccess(bid, 'vto'); + /* Adding renderer only when the context is Outstream + and the provider has responded with a renderer. + */ + if (videoContext == OUTSTREAM && vastTimeout) { + bid.renderer = newVideoRenderer(bid); + } +} + +function newVideoRenderer(bid) { + const renderer = Renderer.install({ + url: PLAYER_URL, + }); + renderer.setRender(function (bid) { + window.mnet.queue.push(function () { + const obj = { + width: bid.width, + height: bid.height, + vastTimeout: bid.vto, + maxAllowedVastTagRedirects: bid.mavtr, + allowVpaid: bid.avp, + autoPlay: bid.ap, + preload: bid.pl, + mute: bid.mt + } + const adUnitCode = bid.dfp_id; + const divId = utils.getGptSlotInfoForAdUnitCode(adUnitCode).divId || adUnitCode; + window.mnet.mediaNetoutstreamPlayer(bid, divId, obj); + }); + }); + return renderer; +} export const spec = { code: BIDDER_CODE, @@ -380,9 +427,10 @@ export const spec = { } validBids = bids.filter(bid => isValidBid(bid)); + validBids.forEach(addRenderer); + return validBids; }, - getUserSyncs: function(syncOptions, serverResponses) { let cookieSyncUrls = fetchCookieSyncUrls(serverResponses); diff --git a/modules/mediasquareBidAdapter.js b/modules/mediasquareBidAdapter.js index 288526d3cc5..87f768d3b77 100644 --- a/modules/mediasquareBidAdapter.js +++ b/modules/mediasquareBidAdapter.js @@ -1,7 +1,7 @@ import {ajax} from '../src/ajax.js'; import {config} from '../src/config.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; -import {BANNER} from '../src/mediaTypes.js'; +import {BANNER, NATIVE, VIDEO} from '../src/mediaTypes.js'; const BIDDER_CODE = 'mediasquare'; const BIDDER_URL_PROD = 'https://pbs-front.mediasquare.fr/' @@ -13,7 +13,7 @@ const BIDDER_ENDPOINT_WINNING = 'winning'; export const spec = { code: BIDDER_CODE, aliases: ['msq'], // short code - supportedMediaTypes: [BANNER], + supportedMediaTypes: [BANNER, NATIVE, VIDEO], /** * Determines whether or not the given bid request is valid. * @@ -99,6 +99,14 @@ export const spec = { 'code': value['code'] } }; + if ('native' in value) { + bidResponse['native'] = value['native']; + bidResponse['mediaType'] = 'native'; + } else if ('video' in value) { + if ('url' in value['video']) { bidResponse['vastUrl'] = value['video']['url'] } + if ('xml' in value['video']) { bidResponse['vastXml'] = value['video']['xml'] } + bidResponse['mediaType'] = 'video'; + } if (value.hasOwnProperty('deal_id')) { bidResponse['dealId'] = value['deal_id']; } bidResponses.push(bidResponse); }); @@ -136,7 +144,7 @@ export const spec = { // fires a pixel to confirm a winning bid let params = []; let endpoint = document.location.search.match(/msq_test=true/) ? BIDDER_URL_TEST : BIDDER_URL_PROD; - let paramsToSearchFor = ['cpm', 'size', 'mediaType', 'currency', 'creativeId', 'adUnitCode', 'timeToRespond', 'auctionId', 'requestId'] + let paramsToSearchFor = ['cpm', 'size', 'mediaType', 'currency', 'creativeId', 'adUnitCode', 'timeToRespond'] if (bid.hasOwnProperty('mediasquare')) { if (bid['mediasquare'].hasOwnProperty('bidder')) { params.push('bidder=' + bid['mediasquare']['bidder']); } if (bid['mediasquare'].hasOwnProperty('code')) { params.push('code=' + bid['mediasquare']['code']); } diff --git a/modules/merkleIdSystem.js b/modules/merkleIdSystem.js index d6bf96618df..c55233af6a0 100644 --- a/modules/merkleIdSystem.js +++ b/modules/merkleIdSystem.js @@ -31,11 +31,12 @@ export const merkleIdSubmodule = { /** * 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 {IdResponse|undefined} */ - getId(configParams, consentData) { + getId(config, consentData) { + const configParams = (config && config.params) || {}; if (!configParams || typeof configParams.pubid !== 'string') { utils.logError('User ID - merkleId submodule requires a valid pubid to be defined'); return; diff --git a/modules/mobfoxpbBidAdapter.js b/modules/mobfoxpbBidAdapter.js new file mode 100644 index 00000000000..c7e96b95179 --- /dev/null +++ b/modules/mobfoxpbBidAdapter.js @@ -0,0 +1,99 @@ +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 = 'mobfoxpb'; +const AD_URL = 'https://bes.mobfox.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) => { + 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; + }, +}; + +registerBidder(spec); diff --git a/modules/mobfoxpbBidAdapter.md b/modules/mobfoxpbBidAdapter.md new file mode 100644 index 00000000000..6eb549919d7 --- /dev/null +++ b/modules/mobfoxpbBidAdapter.md @@ -0,0 +1,72 @@ +# Overview + +``` +Module Name: mobfox Bidder Adapter +Module Type: mobfox Bidder Adapter +Maintainer: platform@mobfox.com +``` + +# Description + +Module that connects to mobfox demand sources + +# Test Parameters +``` + var adUnits = [ + { + code:'1', + mediaTypes:{ + banner: { + sizes: [[300, 250]], + } + }, + bids:[ + { + bidder: 'mobfoxpb', + params: { + placementId: 0 + } + } + ] + }, + { + code:'1', + mediaTypes:{ + video: { + playerSize: [640, 480], + context: 'instream' + } + }, + bids:[ + { + bidder: 'mobfoxpb', + params: { + placementId: 0 + } + } + ] + }, + { + code:'1', + mediaTypes:{ + native: { + title: { + required: true + }, + icon: { + required: true, + size: [64, 64] + } + } + }, + bids:[ + { + bidder: 'mobfoxpb', + params: { + placementId: 0 + } + } + ] + } + ]; +``` \ No newline at end of file diff --git a/modules/netIdSystem.js b/modules/netIdSystem.js index cfe78d9f488..90c8735c993 100644 --- a/modules/netIdSystem.js +++ b/modules/netIdSystem.js @@ -26,12 +26,12 @@ export const netIdSubmodule = { /** * 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] * @param {(Object|undefined)} cacheIdObj * @returns {IdResponse|undefined} */ - getId(configParams) { + getId(config) { /* currently not possible */ return {}; } diff --git a/modules/nobidBidAdapter.js b/modules/nobidBidAdapter.js index 00cb14dc01d..051202cab97 100644 --- a/modules/nobidBidAdapter.js +++ b/modules/nobidBidAdapter.js @@ -6,7 +6,7 @@ import { getStorageManager } from '../src/storageManager.js'; const storage = getStorageManager(); const BIDDER_CODE = 'nobid'; -window.nobidVersion = '1.2.8'; +window.nobidVersion = '1.2.9'; window.nobid = window.nobid || {}; window.nobid.bidResponses = window.nobid.bidResponses || {}; window.nobid.timeoutTotal = 0; @@ -114,6 +114,23 @@ function nobidBuildRequests(bids, bidderRequest) { utils.logWarn('Could not parse screen dimensions, error details:', e); } } + var getEIDs = function(eids) { + if (utils.isArray(eids) && eids.length > 0) { + let src = []; + eids.forEach((eid) => { + let ids = []; + if (eid.uids) { + eid.uids.forEach(value => { + ids.push({'id': value.id + ''}); + }); + } + if (eid.source && ids.length > 0) { + src.push({source: eid.source, uids: ids}); + } + }); + return src; + } + } var state = {}; state['sid'] = siteId; state['l'] = topLocation(bidderRequest); @@ -131,6 +148,8 @@ function nobidBuildRequests(bids, bidderRequest) { if (sch) state['schain'] = sch; const cop = coppa(); if (cop) state['coppa'] = cop; + const eids = getEIDs(utils.deepAccess(bids, '0.userIdAsEids')); + if (eids && eids.length > 0) state['eids'] = eids; return state; } function newAdunit(adunitObject, adunits) { diff --git a/modules/oneVideoBidAdapter.js b/modules/oneVideoBidAdapter.js index c5bc054ac04..8a71910e8fc 100644 --- a/modules/oneVideoBidAdapter.js +++ b/modules/oneVideoBidAdapter.js @@ -4,7 +4,7 @@ import {registerBidder} from '../src/adapters/bidderFactory.js'; const BIDDER_CODE = 'oneVideo'; export const spec = { code: 'oneVideo', - VERSION: '3.0.4', + VERSION: '3.0.5', ENDPOINT: 'https://ads.adaptv.advertising.com/rtb/openrtb?ext_id=', E2ETESTENDPOINT: 'https://ads-wc.v.ssp.yahoo.com/rtb/openrtb?ext_id=', SYNC_ENDPOINT1: 'https://pixel.advertising.com/ups/57304/sync?gdpr=&gdpr_consent=&_origin=0&redir=true', @@ -99,7 +99,7 @@ export const spec = { width: size.width, height: size.height, currency: response.cur, - ttl: 100, + ttl: (bidRequest.params.video.ttl > 0 && bidRequest.params.video.ttl <= 3600) ? bidRequest.params.video.ttl : 300, netRevenue: true, adUnitCode: bidRequest.adUnitCode }; @@ -113,7 +113,6 @@ export const spec = { } else if (bid.adm) { bidResponse.vastXml = bid.adm; } - if (bidRequest.mediaTypes.video) { bidResponse.renderer = (bidRequest.mediaTypes.video.context === 'outstream') ? newRenderer(bidRequest, bidResponse) : undefined; } @@ -302,6 +301,14 @@ function getRequestData(bid, consentData, bidRequest) { bidData.site.ref = 'https://verizonmedia.com'; bidData.tmax = 1000; } + if (bid.params.video.custom && Object.prototype.toString.call(bid.params.video.custom) === '[object Object]') { + bidData.imp[0].ext.custom = {}; + for (const key in bid.params.video.custom) { + if (typeof bid.params.video.custom[key] === 'string' || typeof bid.params.video.custom[key] === 'number') { + bidData.imp[0].ext.custom[key] = bid.params.video.custom[key]; + } + } + } return bidData; } diff --git a/modules/oneVideoBidAdapter.md b/modules/oneVideoBidAdapter.md index 72f251aac04..d413c9d64e5 100644 --- a/modules/oneVideoBidAdapter.md +++ b/modules/oneVideoBidAdapter.md @@ -40,6 +40,11 @@ Connects to Verizon Media's Video SSP (AKA ONE Video / Adap.tv) demand source to inventoryid: 123, minduration: 10, maxduration: 30, + ttl: 300, + custom: { + key1: "value1", + key2: 123345 + } }, site: { id: 1, @@ -85,6 +90,7 @@ Connects to Verizon Media's Video SSP (AKA ONE Video / Adap.tv) demand source to inventoryid: 123, minduration: 10, maxduration: 30, + ttl: 250 }, site: { id: 1, diff --git a/modules/onetagBidAdapter.js b/modules/onetagBidAdapter.js index fd66c8ce69f..16b8096646f 100644 --- a/modules/onetagBidAdapter.js +++ b/modules/onetagBidAdapter.js @@ -6,12 +6,14 @@ import { Renderer } from '../src/Renderer.js'; import find from 'core-js-pure/features/array/find.js'; import { getStorageManager } from '../src/storageManager.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; - -const storage = getStorageManager(); +import { createEidsArray } from './userId/eids.js'; const ENDPOINT = 'https://onetag-sys.com/prebid-request'; const USER_SYNC_ENDPOINT = 'https://onetag-sys.com/usync/'; const BIDDER_CODE = 'onetag'; +const GVLID = 241; + +const storage = getStorageManager(GVLID); /** * Determines whether or not the given bid request is valid. @@ -62,8 +64,8 @@ function buildRequests(validBidRequests, bidderRequest) { if (bidderRequest && bidderRequest.uspConsent) { payload.usPrivacy = bidderRequest.uspConsent; } - if (bidderRequest && bidderRequest.userId) { - payload.userId = bidderRequest.userId; + if (validBidRequests && validBidRequests.length !== 0 && validBidRequests[0].userId) { + payload.userId = createEidsArray(validBidRequests[0].userId); } try { if (storage.hasLocalStorage()) { @@ -87,47 +89,34 @@ function interpretResponse(serverResponse, bidderRequest) { if (!body.bids || !Array.isArray(body.bids) || body.bids.length === 0) { return bids; } - body.bids.forEach(({ - requestId, - cpm, - width, - height, - creativeId, - dealId, - currency, - mediaType, - ttl, - rendererUrl, - ad, - vastUrl, - videoCacheKey - }) => { + body.bids.forEach(bid => { const responseBid = { - requestId, - cpm, - width, - height, - creativeId, - dealId: dealId == null ? dealId : '', - currency, - netRevenue: false, + requestId: bid.requestId, + cpm: bid.cpm, + width: bid.width, + height: bid.height, + creativeId: bid.creativeId, + dealId: bid.dealId == null ? bid.dealId : '', + currency: bid.currency, + netRevenue: bid.netRevenue || false, + mediaType: bid.mediaType, meta: { - mediaType + mediaType: bid.mediaType }, - ttl: ttl || 300 + ttl: bid.ttl || 300 }; - if (mediaType === BANNER) { - responseBid.ad = ad; - } else if (mediaType === VIDEO) { - const {context, adUnitCode} = find(requestData.bids, (item) => item.bidId === requestId); + if (bid.mediaType === BANNER) { + responseBid.ad = bid.ad; + } else if (bid.mediaType === VIDEO) { + const {context, adUnitCode} = find(requestData.bids, (item) => item.bidId === bid.requestId); if (context === INSTREAM) { - responseBid.vastUrl = vastUrl; - responseBid.videoCacheKey = videoCacheKey; + responseBid.vastUrl = bid.vastUrl; + responseBid.videoCacheKey = bid.videoCacheKey; } else if (context === OUTSTREAM) { - responseBid.vastXml = ad; - responseBid.vastUrl = vastUrl; - if (rendererUrl) { - responseBid.renderer = createRenderer({requestId, rendererUrl, adUnitCode}); + responseBid.vastXml = bid.ad; + responseBid.vastUrl = bid.vastUrl; + if (bid.rendererUrl) { + responseBid.renderer = createRenderer({ ...bid, adUnitCode }); } } } @@ -145,25 +134,24 @@ function createRenderer(bid, rendererOptions = {}) { loaded: false }); try { - renderer.setRender(onetagRenderer); + renderer.setRender(({renderer, width, height, vastXml, adUnitCode}) => { + renderer.push(() => { + window.onetag.Player.init({ + ...bid, + width, + height, + vastXml, + nodeId: adUnitCode, + config: renderer.getConfig() + }); + }); + }); } catch (e) { } return renderer; } -function onetagRenderer({renderer, width, height, vastXml, adUnitCode}) { - renderer.push(() => { - window.onetag.Player.init({ - width, - height, - vastXml, - nodeId: adUnitCode, - config: renderer.getConfig() - }); - }); -} - function getFrameNesting() { let topmostFrame = window; let parent = window.parent; @@ -231,7 +219,7 @@ function getPageInfo() { timing: getTiming(), version: { prebid: '$prebid.version$', - adapter: '1.0.0' + adapter: '1.1.0' } }; } @@ -379,6 +367,7 @@ function getUserSyncs(syncOptions, serverResponses, gdprConsent, uspConsent) { export const spec = { code: BIDDER_CODE, + gvlid: GVLID, supportedMediaTypes: [BANNER, VIDEO], isBidRequestValid: isBidRequestValid, buildRequests: buildRequests, diff --git a/modules/ooloAnalyticsAdapter.js b/modules/ooloAnalyticsAdapter.js new file mode 100644 index 00000000000..7195d6ac177 --- /dev/null +++ b/modules/ooloAnalyticsAdapter.js @@ -0,0 +1,547 @@ +import adapter from '../src/AnalyticsAdapter.js' +import adapterManager from '../src/adapterManager.js' +import CONSTANTS from '../src/constants.json' +import * as utils from '../src/utils.js' +import { ajax } from '../src/ajax.js' +import { config } from '../src/config.js' + +const baseUrl = 'https://pbdata.oolo.io/' +const ENDPOINTS = { + CONFIG: 'https://config-pbdata.oolo.io', + PAGE_DATA: baseUrl + 'page', + AUCTION: baseUrl + 'auctionData', + BID_WON: baseUrl + 'bidWonData', + AD_RENDER_FAILED: baseUrl + 'adRenderFailedData', + RAW: baseUrl + 'raw', + HBCONFIG: baseUrl + 'hbconfig', +} + +const pbModuleVersion = '1.0.0' +const prebidVersion = '$prebid.version$' +const analyticsType = 'endpoint' +const ADAPTER_CODE = 'oolo' +const AUCTION_END_SEND_TIMEOUT = 1500 +export const PAGEVIEW_ID = +generatePageViewId() + +const { + AUCTION_INIT, + AUCTION_END, + BID_REQUESTED, + BID_RESPONSE, + NO_BID, + BID_WON, + BID_TIMEOUT, + AD_RENDER_FAILED +} = CONSTANTS.EVENTS + +const SERVER_EVENTS = { + AUCTION: 'auction', + WON: 'bidWon', + AD_RENDER_FAILED: 'adRenderFailed' +} + +const SERVER_BID_STATUS = { + NO_BID: 'noBid', + BID_REQUESTED: 'bidRequested', + BID_RECEIVED: 'bidReceived', + BID_TIMEDOUT: 'bidTimedOut', + BID_WON: 'bidWon' +} + +let auctions = {} +let initOptions = {} +let eventsQueue = [] + +const onAuctionInit = (args) => { + const { auctionId, adUnits, timestamp } = args + + let auction = auctions[auctionId] = { + ...args, + adUnits: {}, + auctionStart: timestamp, + _sentToServer: false + } + + handleCustomFields(auction, AUCTION_INIT, args) + + utils._each(adUnits, adUnit => { + auction.adUnits[adUnit.code] = { + ...adUnit, + auctionId, + adunid: adUnit.code, + bids: {}, + } + }) +} + +const onBidRequested = (args) => { + const { auctionId, bids, start, timeout } = args + const _start = start || Date.now() + const auction = auctions[auctionId] + const auctionAdUnits = auction.adUnits + + bids.forEach(bid => { + const { adUnitCode } = bid + const bidId = parseBidId(bid) + + auctionAdUnits[adUnitCode].bids[bidId] = { + ...bid, + timeout, + start: _start, + rs: _start - auction.auctionStart, + bidStatus: SERVER_BID_STATUS.BID_REQUESTED, + } + }) +} + +const onBidResponse = (args) => { + const { auctionId, adUnitCode } = args + const auction = auctions[auctionId] + const bidId = parseBidId(args) + let bid = auction.adUnits[adUnitCode].bids[bidId] + + Object.assign(bid, args, { + bidStatus: SERVER_BID_STATUS.BID_RECEIVED, + end: args.responseTimestamp, + re: args.responseTimestamp - auction.auctionStart + }) +} + +const onNoBid = (args) => { + const { auctionId, adUnitCode } = args + const bidId = parseBidId(args) + const end = Date.now() + const auction = auctions[auctionId] + let bid = auction.adUnits[adUnitCode].bids[bidId] + + Object.assign(bid, args, { + bidStatus: SERVER_BID_STATUS.NO_BID, + end, + re: end - auction.auctionStart + }) +} + +const onBidWon = (args) => { + const { auctionId, adUnitCode } = args + const bidId = parseBidId(args) + const bid = auctions[auctionId].adUnits[adUnitCode].bids[bidId] + + Object.assign(bid, args, { + bidStatus: SERVER_BID_STATUS.BID_WON, + isW: true, + isH: true + }) + + if (auctions[auctionId]._sentToServer) { + const payload = { + auctionId, + adunid: adUnitCode, + bid: mapBid(bid, BID_WON) + } + + sendEvent(SERVER_EVENTS.WON, payload) + } +} + +const onBidTimeout = (args) => { + utils._each(args, bid => { + const { auctionId, adUnitCode } = bid + const bidId = parseBidId(bid) + let bidCache = auctions[auctionId].adUnits[adUnitCode].bids[bidId] + + Object.assign(bidCache, bid, { + bidStatus: SERVER_BID_STATUS.BID_TIMEDOUT, + }) + }) +} + +const onAuctionEnd = (args) => { + const { auctionId, adUnits, ...restAuctionEnd } = args + + Object.assign(auctions[auctionId], restAuctionEnd, { + auctionEnd: args.auctionEnd || Date.now() + }) + + // wait for bidWon before sending to server + setTimeout(() => { + auctions[auctionId]._sentToServer = true + const finalAuctionData = buildAuctionData(auctions[auctionId]) + + sendEvent(SERVER_EVENTS.AUCTION, finalAuctionData) + }, initOptions.serverConfig.BID_WON_TIMEOUT || AUCTION_END_SEND_TIMEOUT) +} + +const onAdRenderFailed = (args) => { + const data = utils.deepClone(args) + data.timestamp = Date.now() + + if (data.bid) { + data.bid = mapBid(data.bid, AD_RENDER_FAILED) + } + + sendEvent(SERVER_EVENTS.AD_RENDER_FAILED, data) +} + +var ooloAdapter = Object.assign( + adapter({ analyticsType }), { + track({ eventType, args }) { + // wait for server configuration before processing the events + if (typeof initOptions.serverConfig !== 'undefined' && eventsQueue.length === 0) { + handleEvent(eventType, args) + } else { + eventsQueue.push({ eventType, args }) + } + } + } +) + +function handleEvent(eventType, args) { + try { + const { sendRaw } = initOptions.serverConfig.events[eventType] + if (sendRaw) { + sendEvent(eventType, args, true) + } + } catch (e) { } + + switch (eventType) { + case AUCTION_INIT: + onAuctionInit(args) + break + case BID_REQUESTED: + onBidRequested(args) + break + case NO_BID: + onNoBid(args) + break + case BID_RESPONSE: + onBidResponse(args) + break + case BID_WON: + onBidWon(args) + break + case BID_TIMEOUT: + onBidTimeout(args) + break + case AUCTION_END: + onAuctionEnd(args) + break + case AD_RENDER_FAILED: + onAdRenderFailed(args) + break + } +} + +function sendEvent(eventType, args, isRaw) { + let data = utils.deepClone(args) + + Object.assign(data, buildCommonDataProperties(), { + eventType + }) + + if (isRaw) { + let rawEndpoint + try { + const { endpoint, omitRawFields } = initOptions.serverConfig.events[eventType] + rawEndpoint = endpoint + handleCustomRawFields(data, omitRawFields) + } catch (e) { } + ajaxCall(rawEndpoint || ENDPOINTS.RAW, () => { }, JSON.stringify(data)) + } else { + let endpoint + if (eventType === SERVER_EVENTS.AD_RENDER_FAILED) { + endpoint = ENDPOINTS.AD_RENDER_FAILED + } else if (eventType === SERVER_EVENTS.WON) { + endpoint = ENDPOINTS.BID_WON + } else { + endpoint = ENDPOINTS.AUCTION + } + + ajaxCall(endpoint, () => { }, JSON.stringify(data)) + } +} + +function checkEventsQueue() { + while (eventsQueue.length) { + const event = eventsQueue.shift() + handleEvent(event.eventType, event.args) + } +} + +function buildAuctionData(auction) { + const auctionData = utils.deepClone(auction) + const keysToRemove = ['adUnitCodes', 'auctionStatus', 'bidderRequests', 'bidsReceived', 'noBids', 'winningBids', 'timestamp', 'config'] + + keysToRemove.forEach(key => { + delete auctionData[key] + }) + + handleCustomFields(auctionData, AUCTION_END, auction) + + // turn bids object into array of objects + Object.keys(auctionData.adUnits).forEach(adUnit => { + const adUnitObj = auctionData.adUnits[adUnit] + adUnitObj.bids = Object.keys(adUnitObj.bids).map(key => mapBid(adUnitObj.bids[key])) + delete adUnitObj['adUnitCode'] + delete adUnitObj['code'] + delete adUnitObj['transactionId'] + }) + + // turn adUnits objects into array of adUnits + auctionData.adUnits = Object.keys(auctionData.adUnits).map(key => auctionData.adUnits[key]) + + return auctionData +} + +function buildCommonDataProperties() { + return { + pvid: PAGEVIEW_ID, + pid: initOptions.pid, + pbModuleVersion + } +} + +function buildLogMessage(message) { + return `oolo: ${message}` +} + +function parseBidId(bid) { + return bid.bidId || bid.requestId +} + +function mapBid({ + bidStatus, + start, + end, + mediaType, + creativeId, + originalCpm, + originalCurrency, + source, + netRevenue, + currency, + width, + height, + timeToRespond, + responseTimestamp, + ...rest +}, eventType) { + const bidObj = { + bst: bidStatus, + s: start, + e: responseTimestamp || end, + mt: mediaType, + crId: creativeId, + oCpm: originalCpm, + oCur: originalCurrency, + src: source, + nrv: netRevenue, + cur: currency, + w: width, + h: height, + ttr: timeToRespond, + ...rest, + } + + delete bidObj['bidRequestsCount'] + delete bidObj['bidderRequestId'] + delete bidObj['bidderRequestsCount'] + delete bidObj['bidderWinsCount'] + delete bidObj['schain'] + delete bidObj['refererInfo'] + delete bidObj['statusMessage'] + delete bidObj['status'] + delete bidObj['adUrl'] + delete bidObj['ad'] + delete bidObj['usesGenericKeys'] + delete bidObj['requestTimestamp'] + + try { + handleCustomFields(bidObj, eventType || BID_RESPONSE, rest) + } catch (e) { } + + return bidObj +} + +function handleCustomFields(obj, eventType, args) { + try { + const { pickFields, omitFields } = initOptions.serverConfig.events[eventType] + + if (pickFields && obj && args) { + Object.assign(obj, utils.pick(args, pickFields)) + } + + if (omitFields && obj && args) { + omitFields.forEach(field => { + utils.deepSetValue(obj, field, undefined) + }) + } + } catch (e) { } +} + +function handleCustomRawFields(obj, omitRawFields) { + try { + if (omitRawFields && obj) { + omitRawFields.forEach(field => { + utils.deepSetValue(obj, field, undefined) + }) + } + } catch (e) { } +} + +function getServerConfig() { + const defaultConfig = { events: {} } + + ajaxCall( + ENDPOINTS.CONFIG + '?pid=' + initOptions.pid, + { + success: function (data) { + try { + initOptions.serverConfig = JSON.parse(data) || defaultConfig + } catch (e) { + initOptions.serverConfig = defaultConfig + } + checkEventsQueue() + }, + error: function () { + initOptions.serverConfig = defaultConfig + checkEventsQueue() + } + }, + null + ) +} + +function sendPage() { + setTimeout(() => { + const payload = { + timestamp: Date.now(), + screenWidth: window.screen.width, + screenHeight: window.screen.height, + url: window.location.href, + protocol: window.location.protocol, + origin: utils.getOrigin(), + referrer: getTopWindowReferrer(), + pbVersion: prebidVersion, + } + + Object.assign(payload, buildCommonDataProperties(), getPagePerformance()) + ajaxCall(ENDPOINTS.PAGE_DATA, () => { }, JSON.stringify(payload)) + }, 0) +} + +function sendHbConfigData() { + const conf = {} + const pbjsConfig = config.getConfig() + + Object.keys(pbjsConfig).forEach(key => { + if (key[0] !== '_') { + conf[key] = pbjsConfig[key] + } + }) + + ajaxCall(ENDPOINTS.HBCONFIG, () => { }, JSON.stringify(conf)) +} + +function getPagePerformance() { + let timing + + try { + timing = window.top.performance.timing + } catch (e) { } + + if (!timing) { + return + } + + const { navigationStart, domContentLoadedEventEnd, loadEventEnd } = timing + const domContentLoadTime = domContentLoadedEventEnd - navigationStart + const pageLoadTime = loadEventEnd - navigationStart + + return { + domContentLoadTime, + pageLoadTime, + } +} + +function getTopWindowReferrer() { + try { + return window.top.document.referrer + } catch (e) { + return '' + } +} + +function generatePageViewId(min = 10000, max = 90000) { + var randomNumber = Math.floor((Math.random() * max) + min) + var currentdate = new Date() + var currentTime = { + getDate: currentdate.getDate(), + getMonth: currentdate.getMonth(), + getFullYear: currentdate.getFullYear(), + getHours: currentdate.getHours(), + getMinutes: currentdate.getMinutes(), + getSeconds: currentdate.getSeconds(), + getMilliseconds: currentdate.getMilliseconds() + } + return ((currentTime.getDate <= 9) ? '0' + (currentTime.getDate) : (currentTime.getDate)) + '' + + (currentTime.getMonth + 1 <= 9 ? '0' + (currentTime.getMonth + 1) : (currentTime.getMonth + 1)) + '' + + currentTime.getFullYear % 100 + '' + + (currentTime.getHours <= 9 ? '0' + currentTime.getHours : currentTime.getHours) + '' + + (currentTime.getMinutes <= 9 ? '0' + currentTime.getMinutes : currentTime.getMinutes) + '' + + (currentTime.getSeconds <= 9 ? '0' + currentTime.getSeconds : currentTime.getSeconds) + '' + + (currentTime.getMilliseconds % 100 <= 9 ? '0' + (currentTime.getMilliseconds % 100) : (currentTime.getMilliseconds % 100)) + '' + + (randomNumber) +} + +function ajaxCall(endpoint, callback, data, options = {}) { + if (data) { + options.contentType = 'application/json' + } + + return ajax(endpoint, callback, data, options) +} + +ooloAdapter.originEnableAnalytics = ooloAdapter.enableAnalytics +ooloAdapter.enableAnalytics = function (config) { + ooloAdapter.originEnableAnalytics(config) + initOptions = config ? config.options : {} + + if (!initOptions.pid) { + utils.logError(buildLogMessage('enableAnalytics missing config object with "pid"')) + return + } + + getServerConfig() + sendHbConfigData() + + if (document.readyState === 'complete') { + sendPage() + } else { + window.addEventListener('load', sendPage) + } + + utils.logInfo(buildLogMessage('enabled analytics adapter'), config) + ooloAdapter.enableAnalytics = function () { + utils.logInfo(buildLogMessage('Analytics adapter already enabled..')) + } +} + +ooloAdapter.originDisableAnalytics = ooloAdapter.disableAnalytics +ooloAdapter.disableAnalytics = function () { + auctions = {} + initOptions = {} + ooloAdapter.originDisableAnalytics() +} + +adapterManager.registerAnalyticsAdapter({ + adapter: ooloAdapter, + code: ADAPTER_CODE +}) + +// export for testing +export { + buildAuctionData, + generatePageViewId +} + +export default ooloAdapter diff --git a/modules/ooloAnalyticsAdapter.md b/modules/ooloAnalyticsAdapter.md new file mode 100644 index 00000000000..1ffb9cbe050 --- /dev/null +++ b/modules/ooloAnalyticsAdapter.md @@ -0,0 +1,24 @@ +# Overview + +Module Name: oolo Analytics Adapter +Module Type: Analytics Adapter +Maintainer: admin@oolo.io + +# Description + +Analytics adapter for oolo. + +oolo is an anomaly detection based monitoring solution for web publishers, built by adops for adops. Its purpose is to eliminate most of the daily manual work that teams are required to do, while increasing the overall performance, due to full & faster detection of problems and opportunities. + +Contact admin@oolo.io for information. + +# Usage + +```javascript +pbjs.enableAnalytics({ + provider: 'oolo', + options: { + pid: 12345 // id provided by oolo + } +}) +``` diff --git a/modules/openxBidAdapter.js b/modules/openxBidAdapter.js index f4b6288cd55..bbf3d6fdea1 100644 --- a/modules/openxBidAdapter.js +++ b/modules/openxBidAdapter.js @@ -281,6 +281,9 @@ function appendUserIdsToQueryParams(queryParams, userIds) { case 'parrableId': queryParams[key] = userIdObjectOrValue.eid; break; + case 'id5id': + queryParams[key] = userIdObjectOrValue.uid; + break; default: queryParams[key] = userIdObjectOrValue; } diff --git a/modules/ozoneBidAdapter.js b/modules/ozoneBidAdapter.js index 451ab654c53..4246a39bc69 100644 --- a/modules/ozoneBidAdapter.js +++ b/modules/ozoneBidAdapter.js @@ -539,7 +539,7 @@ export const spec = { */ findAllUserIds(bidRequest) { var ret = {}; - let searchKeysSingle = ['pubcid', 'tdid', 'id5id', 'parrableId', 'idl_env', 'digitrustid', 'criteortus']; + let searchKeysSingle = ['pubcid', 'tdid', 'parrableId', 'idl_env', 'digitrustid', 'criteortus']; if (bidRequest.hasOwnProperty('userId')) { for (let arrayId in searchKeysSingle) { let key = searchKeysSingle[arrayId]; @@ -551,6 +551,10 @@ export const spec = { if (lipbid) { ret['lipb'] = {'lipbid': lipbid}; } + var id5id = utils.deepAccess(bidRequest.userId, 'id5id.uid'); + if (id5id) { + ret['id5id'] = id5id; + } } if (!ret.hasOwnProperty('pubcid')) { var pubcid = utils.deepAccess(bidRequest, 'crumbs.pubcid'); @@ -675,7 +679,7 @@ export const spec = { 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.id5id.uid`), 'id5-sync.com', 1); this.addExternalUserId(eids, utils.deepAccess(bidRequest, `userId.criteortus.${BIDDER_CODE}.userid`), 'criteortus', 1); this.addExternalUserId(eids, utils.deepAccess(bidRequest, `userId.idl_env`), 'liveramp.com', 1); this.addExternalUserId(eids, utils.deepAccess(bidRequest, `userId.lipb.lipbid`), 'liveintent.com', 1); diff --git a/modules/parrableIdSystem.js b/modules/parrableIdSystem.js index 2d6a2d6d6e5..7587962c62b 100644 --- a/modules/parrableIdSystem.js +++ b/modules/parrableIdSystem.js @@ -261,11 +261,12 @@ 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) { + getId(config, gdprConsentData, currentStoredId) { + const configParams = (config && config.params) || {}; return fetchId(configParams); } }; diff --git a/modules/prebidServerBidAdapter/index.js b/modules/prebidServerBidAdapter/index.js index b153d0bf8db..d90572d1093 100644 --- a/modules/prebidServerBidAdapter/index.js +++ b/modules/prebidServerBidAdapter/index.js @@ -369,6 +369,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() @@ -676,6 +688,9 @@ const OPEN_RTB_PROTOCOL = { interpretResponse(response, bidderRequests) { 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 => { @@ -698,6 +713,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; @@ -733,8 +750,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 diff --git a/modules/prebidmanagerAnalyticsAdapter.js b/modules/prebidmanagerAnalyticsAdapter.js index b98ca864cd5..994ce4989f5 100644 --- a/modules/prebidmanagerAnalyticsAdapter.js +++ b/modules/prebidmanagerAnalyticsAdapter.js @@ -83,7 +83,7 @@ function collectUtmTagData() { if (newUtm === false) { utmTags.forEach(function (utmKey) { let itemValue = localStorage.getItem(`pm_${utmKey}`); - if (itemValue.length !== 0) { + if (itemValue && itemValue.length !== 0) { pmUtmTags[utmKey] = itemValue; } }); @@ -99,6 +99,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 +121,7 @@ function flush() { bundleId: initOptions.bundleId, events: _eventQueue, utmTags: collectUtmTagData(), + pageInfo: collectPageInfo(), }; ajax( diff --git a/modules/priceFloors.js b/modules/priceFloors.js index 1b865e05c0a..7c8834c2ae2 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,7 +98,7 @@ 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}`); @@ -109,10 +109,12 @@ export function getFirstMatchingFloor(floorData, bidObject, responseObject = {}) 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; @@ -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}, 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..cb0c07cefa8 100644 --- a/modules/pubCommonIdSystem.js +++ b/modules/pubCommonIdSystem.js @@ -8,12 +8,150 @@ 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'; 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'); + } + } +} + +/** + * 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) { + return function (callback) { + if (typeof pixelCallback === 'function') { + pixelCallback(); + } + ajax(SHAREDID_URL, handleResponse(pubcid, callback, config), undefined, {method: 'GET', withCredentials: true}); + } +} /** @type {Submodule} */ export const pubCommonIdSubmodule = { @@ -22,6 +160,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 +189,93 @@ 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()}; + getId: function (config = {}, consentData, storedId) { + 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) { } - } 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) : 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 {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 = {}, storedId) { + 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 +287,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..5c750e66c25 100644 --- a/modules/pubgeniusBidAdapter.js +++ b/modules/pubgeniusBidAdapter.js @@ -1,7 +1,7 @@ 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, @@ -12,15 +12,16 @@ import { 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,16 +147,44 @@ export const spec = { }, }; +function buildVideoParams(videoMediaType, videoParams) { + videoMediaType = videoMediaType || {}; + const params = pick(videoMediaType, ['api', 'mimes', 'protocols', 'playbackmethod']); + + 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), }; + 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); + } + const bidFloor = bid.params.bidFloor; if (isNumber(bidFloor)) { imp.bidfloor = bidFloor; @@ -197,7 +231,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 +242,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 +272,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..ff23a433331 100644 --- a/modules/pubgeniusBidAdapter.md +++ b/modules/pubgeniusBidAdapter.md @@ -50,7 +50,40 @@ var adUnits = [ } } ] - } + }, + { + code: 'test-video', + mediaTypes: { + video: { + context: 'instream', + playerSize: [640, 360], + mimes: ['video/mp4'], + protocols: [3], + } + }, + bids: [ + { + bidder: 'pubgenius', + params: { + adUnitId: '1001', + bidFloor: 1, + test: true, + + // other video parameters as in OpenRTB v2.5 spec + video: { + skip: 1 + + // the following overrides mediaTypes.video of the ad unit + placement: 1, + w: 640, + h: 360, + mimes: ['video/mp4'], + protocols: [3], + } + } + } + ] + }, ]; ``` 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 1651f1f5cc0..b5514ab0344 100644 --- a/modules/pubmaticBidAdapter.js +++ b/modules/pubmaticBidAdapter.js @@ -847,7 +847,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 diff --git a/modules/pubmaticBidAdapter.md b/modules/pubmaticBidAdapter.md index cd9398477f4..0ef89a22cbd 100644 --- a/modules/pubmaticBidAdapter.md +++ b/modules/pubmaticBidAdapter.md @@ -24,7 +24,7 @@ var adUnits = [ bids: [{ bidder: 'pubmatic', params: { - publisherId: '156209', // required + publisherId: '156209', // required, must be wrapped in quotes oustreamAU: 'renderer_test_pubmatic', // required if mediaTypes-> video-> context is 'outstream'. This value can be get by BlueBillyWig Team. adSlot: 'pubmatic_test2', // optional pmzoneid: 'zone1, zone11', // optional diff --git a/modules/pubwiseBidAdapter.js b/modules/pubwiseBidAdapter.js new file mode 100644 index 00000000000..f450a8bede8 --- /dev/null +++ b/modules/pubwiseBidAdapter.js @@ -0,0 +1,777 @@ +import * as utils from '../src/utils.js'; +import { config } from '../src/config.js'; +import { registerBidder } from '../src/adapters/bidderFactory.js'; +import { BANNER, NATIVE } from '../src/mediaTypes.js'; +const VERSION = '0.1.0'; +const GVLID = 842; +const NET_REVENUE = true; +const UNDEFINED = undefined; +const DEFAULT_CURRENCY = 'USD'; +const AUCTION_TYPE = 1; +const BIDDER_CODE = 'pwbid'; +const ENDPOINT_URL = 'https://bid.pubwise.io/prebid'; +const DEFAULT_WIDTH = 0; +const DEFAULT_HEIGHT = 0; +const PREBID_NATIVE_HELP_LINK = 'https://prebid.org/dev-docs/show-native-ads.html'; +// const USERSYNC_URL = '//127.0.0.1:8080/usersync' + +const CUSTOM_PARAMS = { + 'gender': '', // User gender + 'yob': '', // User year of birth + 'lat': '', // User location - Latitude + 'lon': '', // User Location - Longitude +}; + +// rtb native types are meant to be dynamic and extendable +// the extendable data asset types are nicely aligned +// in practice we set an ID that is distinct for each real type of return +const NATIVE_ASSETS = { + 'TITLE': { ID: 1, KEY: 'title', TYPE: 0 }, + 'IMAGE': { ID: 2, KEY: 'image', TYPE: 0 }, + 'ICON': { ID: 3, KEY: 'icon', TYPE: 0 }, + 'SPONSOREDBY': { ID: 4, KEY: 'sponsoredBy', TYPE: 1 }, + 'BODY': { ID: 5, KEY: 'body', TYPE: 2 }, + 'CLICKURL': { ID: 6, KEY: 'clickUrl', TYPE: 0 }, + 'VIDEO': { ID: 7, KEY: 'video', TYPE: 0 }, + 'EXT': { ID: 8, KEY: 'ext', TYPE: 0 }, + 'DATA': { ID: 9, KEY: 'data', TYPE: 0 }, + 'LOGO': { ID: 10, KEY: 'logo', TYPE: 0 }, + 'SPONSORED': { ID: 11, KEY: 'sponsored', TYPE: 1 }, + 'DESC': { ID: 12, KEY: 'data', TYPE: 2 }, + 'RATING': { ID: 13, KEY: 'rating', TYPE: 3 }, + 'LIKES': { ID: 14, KEY: 'likes', TYPE: 4 }, + 'DOWNLOADS': { ID: 15, KEY: 'downloads', TYPE: 5 }, + 'PRICE': { ID: 16, KEY: 'price', TYPE: 6 }, + 'SALEPRICE': { ID: 17, KEY: 'saleprice', TYPE: 7 }, + 'PHONE': { ID: 18, KEY: 'phone', TYPE: 8 }, + 'ADDRESS': { ID: 19, KEY: 'address', TYPE: 9 }, + 'DESC2': { ID: 20, KEY: 'desc2', TYPE: 10 }, + 'DISPLAYURL': { ID: 21, KEY: 'displayurl', TYPE: 11 }, + 'CTA': { ID: 22, KEY: 'cta', TYPE: 12 } +}; + +const NATIVE_ASSET_IMAGE_TYPE = { + 'ICON': 1, + 'LOGO': 2, + 'IMAGE': 3 +} + +// to render any native unit we have to have a few items +const NATIVE_MINIMUM_REQUIRED_IMAGE_ASSETS = [ + { + id: NATIVE_ASSETS.SPONSOREDBY.ID, + required: true, + data: { + type: 1 + } + }, + { + id: NATIVE_ASSETS.TITLE.ID, + required: true, + }, + { + id: NATIVE_ASSETS.IMAGE.ID, + required: true, + } +] + +let isInvalidNativeRequest = false +let NATIVE_ASSET_ID_TO_KEY_MAP = {}; +let NATIVE_ASSET_KEY_TO_ASSET_MAP = {}; + +// together allows traversal of NATIVE_ASSETS_LIST in any direction +// id -> key +utils._each(NATIVE_ASSETS, anAsset => { NATIVE_ASSET_ID_TO_KEY_MAP[anAsset.ID] = anAsset.KEY }); +// key -> asset +utils._each(NATIVE_ASSETS, anAsset => { NATIVE_ASSET_KEY_TO_ASSET_MAP[anAsset.KEY] = anAsset }); + +export const spec = { + code: BIDDER_CODE, + gvlid: GVLID, + supportedMediaTypes: [BANNER, NATIVE], + /** + * Determines whether or not the given bid request is valid. + * + * @param {BidRequest} bid The bid params to validate. + * @return boolean True if this is a valid bid, and false otherwise. + */ + isBidRequestValid: function (bid) { + // siteId is required + if (bid.params && bid.params.siteId) { + // it must be a string + if (!utils.isStr(bid.params.siteId)) { + _logWarn('siteId is required for bid', bid); + return false; + } + } else { + return false; + } + + return true; + }, + /** + * Make a server request from the list of BidRequests. + * + * @param {validBidRequests[]} - an array of bids + * @return ServerRequest Info describing the request to the server. + */ + buildRequests: function (validBidRequests, bidderRequest) { + var refererInfo; + if (bidderRequest && bidderRequest.refererInfo) { + refererInfo = bidderRequest.refererInfo; + } + var conf = _initConf(refererInfo); + var payload = _createOrtbTemplate(conf); + var bidCurrency = ''; + var bid; + var blockedIabCategories = []; + + validBidRequests.forEach(originalBid => { + bid = utils.deepClone(originalBid); + bid.params.adSlot = bid.params.adSlot || ''; + _parseAdSlot(bid); + + conf = _handleCustomParams(bid.params, conf); + conf.transactionId = bid.transactionId; + bidCurrency = bid.params.currency || UNDEFINED; + bid.params.currency = bidCurrency; + + if (bid.params.hasOwnProperty('bcat') && utils.isArray(bid.params.bcat)) { + blockedIabCategories = blockedIabCategories.concat(bid.params.bcat); + } + + var impObj = _createImpressionObject(bid, conf); + if (impObj) { + payload.imp.push(impObj); + } + }); + + // no payload imps, no rason to continue + if (payload.imp.length == 0) { + return; + } + + // test bids can also be turned on here + if (window.location.href.indexOf('pubwiseTestBid=true') !== -1) { + payload.test = 1; + } + + if (bid.params.isTest) { + payload.test = Number(bid.params.isTest) // should be 1 or 0 + } + payload.site.publisher.id = bid.params.siteId.trim(); + payload.user.gender = (conf.gender ? conf.gender.trim() : UNDEFINED); + payload.user.geo = {}; + payload.user.geo.lat = _parseSlotParam('lat', conf.lat); + payload.user.geo.lon = _parseSlotParam('lon', conf.lon); + payload.user.yob = _parseSlotParam('yob', conf.yob); + payload.device.geo = payload.user.geo; + payload.site.page = payload.site.page.trim(); + payload.site.domain = _getDomainFromURL(payload.site.page); + + // add the content object from config in request + if (typeof config.getConfig('content') === 'object') { + payload.site.content = config.getConfig('content'); + } + + // merge the device from config.getConfig('device') + if (typeof config.getConfig('device') === 'object') { + payload.device = Object.assign(payload.device, config.getConfig('device')); + } + + // passing transactionId in source.tid + utils.deepSetValue(payload, 'source.tid', conf.transactionId); + + // schain + if (validBidRequests[0].schain) { + utils.deepSetValue(payload, 'source.ext.schain', validBidRequests[0].schain); + } + + // gdpr consent + if (bidderRequest && bidderRequest.gdprConsent) { + utils.deepSetValue(payload, 'user.ext.consent', bidderRequest.gdprConsent.consentString); + utils.deepSetValue(payload, 'regs.ext.gdpr', (bidderRequest.gdprConsent.gdprApplies ? 1 : 0)); + } + + // ccpa on the root object + if (bidderRequest && bidderRequest.uspConsent) { + utils.deepSetValue(payload, 'regs.ext.us_privacy', bidderRequest.uspConsent); + } + + // if coppa is in effect then note it + if (config.getConfig('coppa') === true) { + utils.deepSetValue(payload, 'regs.coppa', 1); + } + + var options = {contentType: 'text/plain'} + + _logInfo('buildRequests payload', payload); + _logInfo('buildRequests bidderRequest', bidderRequest); + + return { + method: 'POST', + url: ENDPOINT_URL, + data: payload, + options: options, + bidderRequest: bidderRequest, + }; + }, + /** + * Unpack the response from the server into a list of bids. + * + * @param {ServerResponse} serverResponse A successful response from the server. + * @return {Bid[]} An array of bids which were nested inside the server. + */ + interpretResponse: function (response, request) { + const bidResponses = []; + var respCur = DEFAULT_CURRENCY; + _logInfo('interpretResponse request', request); + let parsedRequest = request.data; // not currently stringified + // let parsedReferrer = parsedRequest.site && parsedRequest.site.ref ? parsedRequest.site.ref : ''; + + // try { + if (response.body && response.body.seatbid && utils.isArray(response.body.seatbid)) { + // Supporting multiple bid responses for same adSize + respCur = response.body.cur || respCur; + response.body.seatbid.forEach(seatbidder => { + seatbidder.bid && + utils.isArray(seatbidder.bid) && + seatbidder.bid.forEach(bid => { + let newBid = { + requestId: bid.impid, + cpm: (parseFloat(bid.price) || 0).toFixed(2), + width: bid.w, + height: bid.h, + creativeId: bid.crid || bid.id, + currency: respCur, + netRevenue: NET_REVENUE, + ttl: 300, + ad: bid.adm, + pw_seat: seatbidder.seat || null, + pw_dspid: bid.ext && bid.ext.dspid ? bid.ext.dspid : null, + partnerImpId: bid.id || '' // partner impression Id + }; + if (parsedRequest.imp && parsedRequest.imp.length > 0) { + parsedRequest.imp.forEach(req => { + if (bid.impid === req.id) { + _checkMediaType(bid.adm, newBid); + switch (newBid.mediaType) { + case BANNER: + break; + case NATIVE: + _parseNativeResponse(bid, newBid); + break; + } + } + }); + } + + newBid.meta = {}; + if (bid.ext && bid.ext.dspid) { + newBid.meta.networkId = bid.ext.dspid; + } + if (bid.ext && bid.ext.advid) { + newBid.meta.buyerId = bid.ext.advid; + } + if (bid.adomain && bid.adomain.length > 0) { + newBid.meta.advertiserDomains = bid.adomain; + newBid.meta.clickUrl = bid.adomain[0]; + } + + bidResponses.push(newBid); + }); + }); + } + // } catch (error) { + // _logError(error); + // } + return bidResponses; + } +} + +function _checkMediaType(adm, newBid) { + // Create a regex here to check the strings + var admJSON = ''; + if (adm.indexOf('"ver":') >= 0) { + try { + admJSON = JSON.parse(adm.replace(/\\/g, '')); + if (admJSON && admJSON.assets) { + newBid.mediaType = NATIVE; + } + } catch (e) { + _logWarn('Error: Cannot parse native reponse for ad response: ' + adm); + } + } else { + newBid.mediaType = BANNER; + } +} + +function _parseNativeResponse(bid, newBid) { + newBid.native = {}; + if (bid.hasOwnProperty('adm')) { + var adm = ''; + try { + adm = JSON.parse(bid.adm.replace(/\\/g, '')); + } catch (ex) { + _logWarn('Error: Cannot parse native reponse for ad response: ' + newBid.adm); + return; + } + if (adm && adm.assets && adm.assets.length > 0) { + newBid.mediaType = NATIVE; + for (let i = 0, len = adm.assets.length; i < len; i++) { + switch (adm.assets[i].id) { + case NATIVE_ASSETS.TITLE.ID: + newBid.native.title = adm.assets[i].title && adm.assets[i].title.text; + break; + case NATIVE_ASSETS.IMAGE.ID: + newBid.native.image = { + url: adm.assets[i].img && adm.assets[i].img.url, + height: adm.assets[i].img && adm.assets[i].img.h, + width: adm.assets[i].img && adm.assets[i].img.w, + }; + break; + case NATIVE_ASSETS.ICON.ID: + newBid.native.icon = { + url: adm.assets[i].img && adm.assets[i].img.url, + height: adm.assets[i].img && adm.assets[i].img.h, + width: adm.assets[i].img && adm.assets[i].img.w, + }; + break; + case NATIVE_ASSETS.SPONSOREDBY.ID: + case NATIVE_ASSETS.BODY.ID: + case NATIVE_ASSETS.LIKES.ID: + case NATIVE_ASSETS.DOWNLOADS.ID: + case NATIVE_ASSETS.PRICE: + case NATIVE_ASSETS.SALEPRICE.ID: + case NATIVE_ASSETS.PHONE.ID: + case NATIVE_ASSETS.ADDRESS.ID: + case NATIVE_ASSETS.DESC2.ID: + case NATIVE_ASSETS.CTA.ID: + case NATIVE_ASSETS.RATING.ID: + case NATIVE_ASSETS.DISPLAYURL.ID: + newBid.native[NATIVE_ASSET_ID_TO_KEY_MAP[adm.assets[i].id]] = adm.assets[i].data && adm.assets[i].data.value; + break; + } + } + newBid.clickUrl = adm.link && adm.link.url; + newBid.clickTrackers = (adm.link && adm.link.clicktrackers) || []; + newBid.impressionTrackers = adm.imptrackers || []; + newBid.jstracker = adm.jstracker || []; + if (!newBid.width) { + newBid.width = DEFAULT_WIDTH; + } + if (!newBid.height) { + newBid.height = DEFAULT_HEIGHT; + } + } + } +} + +function _getDomainFromURL(url) { + let anchor = document.createElement('a'); + anchor.href = url; + return anchor.hostname; +} + +function _handleCustomParams(params, conf) { + var key, value, entry; + for (key in CUSTOM_PARAMS) { + if (CUSTOM_PARAMS.hasOwnProperty(key)) { + value = params[key]; + if (value) { + entry = CUSTOM_PARAMS[key]; + + if (typeof entry === 'object') { + // will be used in future when we want to + // process a custom param before using + // 'keyname': {f: function() {}} + value = entry.f(value, conf); + } + + if (utils.isStr(value)) { + conf[key] = value; + } else { + _logWarn('Ignoring param : ' + key + ' with value : ' + CUSTOM_PARAMS[key] + ', expects string-value, found ' + typeof value); + } + } + } + } + return conf; +} + +function _createOrtbTemplate(conf) { + return { + id: '' + new Date().getTime(), + at: AUCTION_TYPE, + cur: [DEFAULT_CURRENCY], + imp: [], + site: { + page: conf.pageURL, + ref: conf.refURL, + publisher: {} + }, + device: { + ua: navigator.userAgent, + js: 1, + dnt: (navigator.doNotTrack == 'yes' || navigator.doNotTrack == '1' || navigator.msDoNotTrack == '1') ? 1 : 0, + h: screen.height, + w: screen.width, + language: navigator.language + }, + user: {}, + ext: { + version: VERSION + } + }; +} + +function _createImpressionObject(bid, conf) { + var impObj = {}; + var bannerObj; + var nativeObj = {}; + var mediaTypes = ''; + + impObj = { + id: bid.bidId, + tagid: bid.params.adUnit || undefined, + bidfloor: _parseSlotParam('bidFloor', bid.params.bidFloor), // capitalization dicated by 3.2.4 spec + secure: 1, + bidfloorcur: bid.params.currency ? _parseSlotParam('currency', bid.params.currency) : DEFAULT_CURRENCY // capitalization dicated by 3.2.4 spec + }; + + if (bid.hasOwnProperty('mediaTypes')) { + for (mediaTypes in bid.mediaTypes) { + switch (mediaTypes) { + case BANNER: + bannerObj = _createBannerRequest(bid); + if (bannerObj !== UNDEFINED) { + impObj.banner = bannerObj; + } + break; + case NATIVE: + nativeObj['request'] = JSON.stringify(_createNativeRequest(bid.nativeParams)); + if (!isInvalidNativeRequest) { + impObj.native = nativeObj; + } else { + _logWarn('Error: Error in Native adunit ' + bid.params.adUnit + '. Ignoring the adunit. Refer to ' + PREBID_NATIVE_HELP_LINK + ' for more details.'); + } + break; + } + } + } else { + _logWarn('MediaTypes are Required for all Adunit Configs', bid) + } + + _addFloorFromFloorModule(impObj, bid); + + return impObj.hasOwnProperty(BANNER) || + impObj.hasOwnProperty(NATIVE) ? impObj : UNDEFINED; +} + +function _parseSlotParam(paramName, paramValue) { + if (!utils.isStr(paramValue)) { + paramValue && _logWarn('Ignoring param key: ' + paramName + ', expects string-value, found ' + typeof paramValue); + return UNDEFINED; + } + + switch (paramName) { + case 'bidFloor': + return parseFloat(paramValue) || UNDEFINED; + case 'lat': + return parseFloat(paramValue) || UNDEFINED; + case 'lon': + return parseFloat(paramValue) || UNDEFINED; + case 'yob': + return parseInt(paramValue) || UNDEFINED; + default: + return paramValue; + } +} + +function _parseAdSlot(bid) { + _logInfo('parseAdSlot bid', bid) + bid.params.adUnit = ''; + bid.params.width = 0; + bid.params.height = 0; + bid.params.adSlot = _cleanSlotName(bid.params.adSlot); + + if (bid.hasOwnProperty('mediaTypes')) { + if (bid.mediaTypes.hasOwnProperty(BANNER) && + bid.mediaTypes.banner.hasOwnProperty('sizes')) { // if its a banner, has mediaTypes and sizes + var i = 0; + var sizeArray = []; + for (;i < bid.mediaTypes.banner.sizes.length; i++) { + if (bid.mediaTypes.banner.sizes[i].length === 2) { // sizes[i].length will not be 2 in case where size is set as fluid, we want to skip that entry + sizeArray.push(bid.mediaTypes.banner.sizes[i]); + } + } + bid.mediaTypes.banner.sizes = sizeArray; + if (bid.mediaTypes.banner.sizes.length >= 1) { + // if there is more than one size then pop one onto the banner params width + // pop the first into the params, then remove it from mediaTypes + bid.params.width = bid.mediaTypes.banner.sizes[0][0]; + bid.params.height = bid.mediaTypes.banner.sizes[0][1]; + bid.mediaTypes.banner.sizes = bid.mediaTypes.banner.sizes.splice(1, bid.mediaTypes.banner.sizes.length - 1); + } + } + } else { + _logWarn('MediaTypes are Required for all Adunit Configs', bid) + } +} + +function _cleanSlotName(slotName) { + if (utils.isStr(slotName)) { + return slotName.replace(/^\s+/g, '').replace(/\s+$/g, ''); + } + return ''; +} + +function _initConf(refererInfo) { + return { + pageURL: (refererInfo && refererInfo.referer) ? refererInfo.referer : window.location.href, + refURL: window.document.referrer + }; +} + +function _commonNativeRequestObject(nativeAsset, params) { + var key = nativeAsset.KEY; + return { + id: nativeAsset.ID, + required: params[key].required ? 1 : 0, + data: { + type: nativeAsset.TYPE, + len: params[key].len, + ext: params[key].ext + } + }; +} + +function _addFloorFromFloorModule(impObj, bid) { + let bidFloor = -1; // indicates no floor + + // get lowest floor from floorModule + if (typeof bid.getFloor === 'function' && !config.getConfig('pubwise.disableFloors')) { + [BANNER, NATIVE].forEach(mediaType => { + if (impObj.hasOwnProperty(mediaType)) { + let floorInfo = bid.getFloor({ currency: impObj.bidFloorCur, mediaType: mediaType, size: '*' }); + if (typeof floorInfo === 'object' && floorInfo.currency === impObj.bidFloorCur && !isNaN(parseInt(floorInfo.floor))) { + let mediaTypeFloor = parseFloat(floorInfo.floor); + bidFloor = (bidFloor == -1 ? mediaTypeFloor : Math.min(mediaTypeFloor, bidFloor)) + } + } + }); + } + + // get highest, if none then take the default -1 + if (impObj.bidfloor) { + bidFloor = Math.max(bidFloor, impObj.bidfloor) + } + + // assign if it has a valid floor - > 0 + impObj.bidfloor = ((!isNaN(bidFloor) && bidFloor > 0) ? bidFloor : UNDEFINED); +} + +function _createNativeRequest(params) { + var nativeRequestObject = { + assets: [] + }; + for (var key in params) { + if (params.hasOwnProperty(key)) { + var assetObj = {}; + if (!(nativeRequestObject.assets && nativeRequestObject.assets.length > 0 && nativeRequestObject.assets.hasOwnProperty(key))) { + switch (key) { + case NATIVE_ASSETS.TITLE.KEY: + if (params[key].len || params[key].length) { + assetObj = { + id: NATIVE_ASSETS.TITLE.ID, + required: params[key].required ? 1 : 0, + title: { + len: params[key].len || params[key].length, + ext: params[key].ext + } + }; + } else { + _logWarn('Error: Title Length is required for native ad: ' + JSON.stringify(params)); + } + break; + case NATIVE_ASSETS.IMAGE.KEY: + if (params[key].sizes && params[key].sizes.length > 0) { + assetObj = { + id: NATIVE_ASSETS.IMAGE.ID, + required: params[key].required ? 1 : 0, + img: { + type: NATIVE_ASSET_IMAGE_TYPE.IMAGE, + w: params[key].w || params[key].width || (params[key].sizes ? params[key].sizes[0] : UNDEFINED), + h: params[key].h || params[key].height || (params[key].sizes ? params[key].sizes[1] : UNDEFINED), + wmin: params[key].wmin || params[key].minimumWidth || (params[key].minsizes ? params[key].minsizes[0] : UNDEFINED), + hmin: params[key].hmin || params[key].minimumHeight || (params[key].minsizes ? params[key].minsizes[1] : UNDEFINED), + mimes: params[key].mimes, + ext: params[key].ext, + } + }; + } else { + _logWarn('Error: Image sizes is required for native ad: ' + JSON.stringify(params)); + } + break; + case NATIVE_ASSETS.ICON.KEY: + if (params[key].sizes && params[key].sizes.length > 0) { + assetObj = { + id: NATIVE_ASSETS.ICON.ID, + required: params[key].required ? 1 : 0, + img: { + type: NATIVE_ASSET_IMAGE_TYPE.ICON, + w: params[key].w || params[key].width || (params[key].sizes ? params[key].sizes[0] : UNDEFINED), + h: params[key].h || params[key].height || (params[key].sizes ? params[key].sizes[1] : UNDEFINED), + } + }; + } else { + _logWarn('Error: Icon sizes is required for native ad: ' + JSON.stringify(params)); + }; + break; + case NATIVE_ASSETS.VIDEO.KEY: + assetObj = { + id: NATIVE_ASSETS.VIDEO.ID, + required: params[key].required ? 1 : 0, + video: { + minduration: params[key].minduration, + maxduration: params[key].maxduration, + protocols: params[key].protocols, + mimes: params[key].mimes, + ext: params[key].ext + } + }; + break; + case NATIVE_ASSETS.EXT.KEY: + assetObj = { + id: NATIVE_ASSETS.EXT.ID, + required: params[key].required ? 1 : 0, + }; + break; + case NATIVE_ASSETS.LOGO.KEY: + assetObj = { + id: NATIVE_ASSETS.LOGO.ID, + required: params[key].required ? 1 : 0, + img: { + type: NATIVE_ASSET_IMAGE_TYPE.LOGO, + w: params[key].w || params[key].width || (params[key].sizes ? params[key].sizes[0] : UNDEFINED), + h: params[key].h || params[key].height || (params[key].sizes ? params[key].sizes[1] : UNDEFINED) + } + }; + break; + case NATIVE_ASSETS.SPONSOREDBY.KEY: + case NATIVE_ASSETS.BODY.KEY: + case NATIVE_ASSETS.RATING.KEY: + case NATIVE_ASSETS.LIKES.KEY: + case NATIVE_ASSETS.DOWNLOADS.KEY: + case NATIVE_ASSETS.PRICE.KEY: + case NATIVE_ASSETS.SALEPRICE.KEY: + case NATIVE_ASSETS.PHONE.KEY: + case NATIVE_ASSETS.ADDRESS.KEY: + case NATIVE_ASSETS.DESC2.KEY: + case NATIVE_ASSETS.DISPLAYURL.KEY: + case NATIVE_ASSETS.CTA.KEY: + assetObj = _commonNativeRequestObject(NATIVE_ASSET_KEY_TO_ASSET_MAP[key], params); + break; + } + } + } + if (assetObj && assetObj.id) { + nativeRequestObject.assets[nativeRequestObject.assets.length] = assetObj; + } + }; + + // for native image adtype prebid has to have few required assests i.e. title,sponsoredBy, image + // if any of these are missing from the request then request will not be sent + var requiredAssetCount = NATIVE_MINIMUM_REQUIRED_IMAGE_ASSETS.length; + var presentrequiredAssetCount = 0; + NATIVE_MINIMUM_REQUIRED_IMAGE_ASSETS.forEach(ele => { + var lengthOfExistingAssets = nativeRequestObject.assets.length; + for (var i = 0; i < lengthOfExistingAssets; i++) { + if (ele.id == nativeRequestObject.assets[i].id) { + presentrequiredAssetCount++; + break; + } + } + }); + if (requiredAssetCount == presentrequiredAssetCount) { + isInvalidNativeRequest = false; + } else { + isInvalidNativeRequest = true; + } + return nativeRequestObject; +} + +function _createBannerRequest(bid) { + var sizes = bid.mediaTypes.banner.sizes; + var format = []; + var bannerObj; + if (sizes !== UNDEFINED && utils.isArray(sizes)) { + bannerObj = {}; + if (!bid.params.width && !bid.params.height) { + if (sizes.length === 0) { + // i.e. since bid.params does not have width or height, and length of sizes is 0, need to ignore this banner imp + bannerObj = UNDEFINED; + _logWarn('Error: mediaTypes.banner.size missing for adunit: ' + bid.params.adUnit + '. Ignoring the banner impression in the adunit.'); + return bannerObj; + } else { + bannerObj.w = parseInt(sizes[0][0], 10); + bannerObj.h = parseInt(sizes[0][1], 10); + sizes = sizes.splice(1, sizes.length - 1); + } + } else { + bannerObj.w = bid.params.width; + bannerObj.h = bid.params.height; + } + if (sizes.length > 0) { + format = []; + sizes.forEach(function (size) { + if (size.length > 1) { + format.push({ w: size[0], h: size[1] }); + } + }); + if (format.length > 0) { + bannerObj.format = format; + } + } + bannerObj.pos = 0; + bannerObj.topframe = utils.inIframe() ? 0 : 1; + } else { + _logWarn('Error: mediaTypes.banner.size missing for adunit: ' + bid.params.adUnit + '. Ignoring the banner impression in the adunit.'); + bannerObj = UNDEFINED; + } + return bannerObj; +} + +// various error levels are not always used +// eslint-disable-next-line no-unused-vars +function _logMessage(textValue, objectValue) { + utils.logMessage('PubWise: ' + textValue, objectValue); +} + +// eslint-disable-next-line no-unused-vars +function _logInfo(textValue, objectValue) { + utils.logInfo('PubWise: ' + textValue, objectValue); +} + +// eslint-disable-next-line no-unused-vars +function _logWarn(textValue, objectValue) { + utils.logWarn('PubWise: ' + textValue, objectValue); +} + +// eslint-disable-next-line no-unused-vars +function _logError(textValue, objectValue) { + utils.logError('PubWise: ' + textValue, objectValue); +} + +// function _decorateLog() { +// arguments[0] = 'PubWise' + arguments[0]; +// return arguments +// } + +// these are exported only for testing so maintaining the JS convention of _ to indicate the intent +export { + _checkMediaType, + _parseAdSlot +} + +registerBidder(spec); diff --git a/modules/pubwiseBidAdapter.md b/modules/pubwiseBidAdapter.md new file mode 100644 index 00000000000..8cf38a63913 --- /dev/null +++ b/modules/pubwiseBidAdapter.md @@ -0,0 +1,78 @@ +# Overview + +``` +Module Name: PubWise Bid Adapter +Module Type: Bidder Adapter +Maintainer: info@pubwise.io +``` + +# Description + +Connects to PubWise exchange for bids. + +# Sample Banner Ad Unit: For Publishers + +With isTest parameter the system will respond in whatever dimensions provided. + +## Params + + + +## Banner +``` +var adUnits = [ + { + code: "div-gpt-ad-1460505748561-0", + mediaTypes: { + banner: { + sizes: [[300, 250]] + } + }, + bids: [{ + bidder: 'pubwise', + params: { + siteId: "xxxxxx", + isTest: true + } + }] + } +] +``` +## Native +``` +var adUnits = [ + { + code: 'div-gpt-ad-1460505748561-1', + sizes: [[1, 1]], + mediaTypes: { + native: { + title: { + required: true, + len: 80 + }, + body: { + required: true + }, + image: { + required: true, + sizes: [150, 50] + }, + sponsoredBy: { + required: true + }, + icon: { + required: false + } + } + }, + bids: [{ + bidder: 'pubwise', + params: { + siteId: "xxxxxx", + isTest: true, + }, + }] + } +] +``` + diff --git a/modules/pubxaiAnalyticsAdapter.js b/modules/pubxaiAnalyticsAdapter.js new file mode 100644 index 00000000000..7e2f5061621 --- /dev/null +++ b/modules/pubxaiAnalyticsAdapter.js @@ -0,0 +1,168 @@ +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.0.0'; +const defaultHost = 'api.pbxai.com'; +const auctionPath = '/analytics/auction'; +const winningBidPath = '/analytics/bidwon'; + +let initOptions; +let auctionTimestamp; +let events = { + bids: [] +}; + +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; + auctionTimestamp = args.timestamp; + } else if (eventType === CONSTANTS.EVENTS.BID_REQUESTED) { + mapBidRequests(args).forEach(item => { events.bids.push(item) }); + } 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 mapBidRequests(params) { + let arr = []; + if (typeof params.bids !== 'undefined' && params.bids.length) { + params.bids.forEach(function (bid) { + arr.push({ + bidderCode: bid.bidder, + bidId: bid.bidId, + adUnitCode: bid.adUnitCode, + requestId: bid.bidderRequestId, + auctionId: bid.auctionId, + transactionId: bid.transactionId, + sizes: utils.parseSizesInput(bid.mediaTypes.banner.sizes).toString(), + renderStatus: 1, + requestTimestamp: params.auctionStart + }); + }); + } + return arr; +} + +function mapBidResponse(bidResponse, status) { + if (status !== 'bidwon') { + let bid = events.bids.filter(o => o.bidId === bidResponse.bidId || o.bidId === bidResponse.requestId)[0]; + Object.assign(bid, { + bidderCode: bidResponse.bidder, + bidId: status === 'timeout' ? bidResponse.bidId : bidResponse.requestId, + adUnitCode: bidResponse.adUnitCode, + auctionId: bidResponse.auctionId, + creativeId: bidResponse.creativeId, + transactionId: bidResponse.transactionId, + currency: bidResponse.currency, + cpm: bidResponse.cpm, + netRevenue: bidResponse.netRevenue, + mediaType: bidResponse.mediaType, + statusMessage: bidResponse.statusMessage, + floorData: bidResponse.floorData, + status: bidResponse.status, + renderStatus: status === 'timeout' ? 3 : 2, + timeToRespond: bidResponse.timeToRespond, + requestTimestamp: bidResponse.requestTimestamp, + responseTimestamp: bidResponse.responseTimestamp, + platform: navigator.platform, + deviceType: getDeviceType() + }); + } else { + return { + bidderCode: bidResponse.bidder, + bidId: bidResponse.requestId, + adUnitCode: bidResponse.adUnitCode, + auctionId: bidResponse.auctionId, + creativeId: bidResponse.creativeId, + transactionId: bidResponse.transactionId, + currency: bidResponse.currency, + cpm: bidResponse.cpm, + netRevenue: bidResponse.netRevenue, + floorData: bidResponse.floorData, + renderedSize: bidResponse.size, + mediaType: bidResponse.mediaType, + statusMessage: bidResponse.statusMessage, + status: bidResponse.status, + renderStatus: 4, + timeToRespond: bidResponse.timeToRespond, + requestTimestamp: bidResponse.requestTimestamp, + responseTimestamp: bidResponse.responseTimestamp, + platform: navigator.platform, + deviceType: getDeviceType() + } + } +} + +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'; +} + +// 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(); + if (typeof data !== 'undefined' && typeof data.auctionInit !== 'undefined') { + Object.assign(data.auctionInit, { host: location.host, path: location.pathname, search: location.search }); + } + data.initOptions = initOptions; + + 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 + } + }); + + ajax(pubxaiAnalyticsRequestUrl, undefined, JSON.stringify(data), { method: 'POST', contentType: 'text/plain' }); + } +} + +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 894bb991a71..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 = {}; @@ -101,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` @@ -188,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); @@ -271,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') ); @@ -283,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/qwarryBidAdapter.js b/modules/qwarryBidAdapter.js new file mode 100644 index 00000000000..7cb83520979 --- /dev/null +++ b/modules/qwarryBidAdapter.js @@ -0,0 +1,78 @@ +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 + }) + }) + + return { + method: 'POST', + url: ENDPOINT, + data: { requestId: bidderRequest.bidderRequestId, bids }, + options: { + contentType: 'application/json', + customHeaders: { + 'Rtb-Direct': true + } + } + }; + }, + + 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; + } +} + +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/reconciliationRtdProvider.js b/modules/reconciliationRtdProvider.js new file mode 100644 index 00000000000..bd3d8861c5b --- /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.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.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..c77afbe6ec5 100644 --- a/modules/relaidoBidAdapter.js +++ b/modules/relaidoBidAdapter.js @@ -6,17 +6,13 @@ 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.2'; 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; @@ -64,11 +60,11 @@ function buildRequests(validBidRequests, bidderRequest) { }; if (hasVideoMediaType(bidRequest)) { - const playerSize = utils.deepAccess(bidRequest, 'mediaTypes.video.playerSize'); + const playerSize = getValidSizes(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'); + const sizes = getValidSizes(utils.deepAccess(bidRequest, 'mediaTypes.banner.sizes')); payload.width = sizes[0][0]; payload.height = sizes[0][1]; } @@ -101,10 +97,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 +133,6 @@ function getUserSyncs(syncOptions, serverResponses) { if (serverResponses.length > 0) { syncUrl = utils.deepAccess(serverResponses, '0.body.syncUrl') || syncUrl; } - receiveMessage(); return [{ type: 'iframe', url: syncUrl @@ -219,37 +210,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 +232,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() { @@ -291,6 +265,22 @@ 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 >= 300 && height >= 250) || (width == 1 && height == 1)) { + result.push([width, height]); + } + } + } + } + return result; +} + export const spec = { code: BIDDER_CODE, supportedMediaTypes: [BANNER, VIDEO], diff --git a/modules/richaudienceBidAdapter.js b/modules/richaudienceBidAdapter.js index e51cc79eb82..a6b4202fc91 100755 --- a/modules/richaudienceBidAdapter.js +++ b/modules/richaudienceBidAdapter.js @@ -9,6 +9,7 @@ let REFERER = ''; export const spec = { code: BIDDER_CODE, + gvlid: 108, aliases: ['ra'], supportedMediaTypes: [BANNER, VIDEO], @@ -201,7 +202,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`)); diff --git a/modules/rtdModule/index.js b/modules/rtdModule/index.js index 9acd484cec8..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 */ /** @@ -33,42 +66,36 @@ * @function * @summary init sub module * @name RtdSubmodule#init - * @param {Object} config - * @param {Object} gdpr settings - * @param {Object} usp settings + * @param {SubmoduleConfig} config + * @param {UserConsentData} user consent * @return {boolean} false to remove sub module */ /** * @function? * @summary on auction init event - * @name RtdSubmodule#auctionInit + * @name RtdSubmodule#onAuctionInitEvent * @param {Object} data * @param {SubmoduleConfig} config + * @param {UserConsentData} userConsent */ /** * @function? * @summary on auction end event - * @name RtdSubmodule#auctionEnd - * @param {Object} data - * @param {SubmoduleConfig} config - */ - -/** - * @function? - * @summary on bid request event - * @name RtdSubmodule#updateBidRequest + * @name RtdSubmodule#onAuctionEndEvent * @param {Object} data * @param {SubmoduleConfig} config + * @param {UserConsentData} userConsent */ /** * @function? * @summary on bid response event - * @name RtdSubmodule#updateBidResponse + * @name RtdSubmodule#onBidResponseEvent * @param {Object} data * @param {SubmoduleConfig} config + * @param {UserConsentData} userConsent */ /** @@ -82,13 +109,6 @@ * @type {number} */ -/** - * @property - * @summary timeout (if no auction dealy) - * @name ModuleConfig#timeout - * @type {number} - */ - /** * @property * @summary list of sub modules @@ -121,33 +141,34 @@ * @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 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) { @@ -159,35 +180,35 @@ export function init(config) { confListener(); // unsubscribe config listener _moduleConfig = realTimeData; _dataProviders = realTimeData.dataProviders; - getHook('makeBidRequests').before(initSubModules); setEventsListeners(); - 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); - } + 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 */ -export function initSubModules(next, adUnits, auctionStart, auctionId, cbTimeout, labels) { +function initSubModules() { + _userConsent = getConsentData(); let subModulesByOrder = []; _dataProviders.forEach(provider => { - const sm = find(subModules, s => s.name === provider.name); - const initResponse = sm && sm.init && sm.init(provider, gdprDataHandler.getConsentData(), uspDataHandler.getConsentData()); + 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; - next(adUnits, auctionStart, auctionId, cbTimeout, labels) } /** @@ -195,94 +216,117 @@ export function initSubModules(next, adUnits, auctionStart, auctionId, cbTimeout */ function setEventsListeners() { events.on(CONSTANTS.EVENTS.AUCTION_INIT, (args) => { - subModules.forEach(sm => { sm.auctionInit && sm.auctionInit(args, sm.config) }) + subModules.forEach(sm => { sm.onAuctionInitEvent && sm.onAuctionInitEvent(args, sm.config, _userConsent) }) }); events.on(CONSTANTS.EVENTS.AUCTION_END, (args) => { - subModules.forEach(sm => { sm.auctionEnd && sm.auctionEnd(args, sm.config) }) - }); - events.on(CONSTANTS.EVENTS.BEFORE_REQUEST_BIDS, (args) => { - subModules.forEach(sm => { sm.updateBidRequest && sm.updateBidRequest(args, sm.config) }) + getAdUnitTargeting(args); + subModules.forEach(sm => { sm.onAuctionEndEvent && sm.onAuctionEndEvent(args, sm.config, _userConsent) }) }); events.on(CONSTANTS.EVENTS.BID_RESPONSE, (args) => { - subModules.forEach(sm => { sm.updateBidResponse && sm.updateBidResponse(args, sm.config) }) + 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 */ -export function getProviderData(adUnits, callback) { - /** - * invoke callback if one of the conditions met: - * timeout reached - * all submodules answered - * all sub modules configured "waitForIt:true" answered (as long as there is at least one configured) - */ - - const waitForSubModulesLength = subModules.filter(sm => sm.config && sm.config.waitForIt).length; - let callbacksExpected = waitForSubModulesLength || subModules.length; - const shouldWaitForAllSubModules = waitForSubModulesLength === 0; - let dataReceived = {}; - let processDone = false; - const dataWaitTimeout = setTimeout(done, _moduleConfig.auctionDelay || _moduleConfig.timeout || DEF_TIMEOUT); +export function setBidRequestsData(fn, reqBidsConfigObj) { + _userConsent = getConsentData(); + + const relevantSubModules = []; + const prioritySubModules = []; subModules.forEach(sm => { - sm.getData(adUnits, onDataReceived.bind(sm)); + 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[this.name] = data; - if (shouldWaitForAllSubModules || (this.config && this.config.waitForIt)) { - callbacksExpected-- + if (this.config && this.config.waitForIt) { + callbacksExpected--; } if (callbacksExpected <= 0) { - clearTimeout(dataWaitTimeout); - done(); + return exitHook(); } } - function done() { - processDone = true; - callback(dataReceived); + function exitHook() { + isDone = true; + clearTimeout(waitTimeout); + fn.call(this, reqBidsConfigObj); } } /** - * 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 + * 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 setTargetsAfterRequestBids(next, adUnits) { - getProviderData(adUnits, (data) => { - if (data && Object.keys(data).length) { - const _mergedData = deepMerge(setDataOrderByProvider(subModules, data)); - if (Object.keys(_mergedData).length) { - setDataForPrimaryAdServer(_mergedData); - } - } - next(adUnits); - }); -} +export function getAdUnitTargeting(auction) { + const relevantSubModules = subModules.filter(sm => typeof sm.getTargetingData === 'function'); + if (!relevantSubModules.length) { + return; + } -/** - * return an array providers data in reverse order,so the data merge will be according to correct config order - * @param {Submodule[]} modules - * @param {Object} data - data retrieved from providers - * @return {array} reversed order ready for merge - */ -function setDataOrderByProvider(modules, data) { - let rd = []; - for (let i = modules.length; i--; i > 0) { - if (data[modules[i].name]) { - rd.push(data[modules[i].name]) + // 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 rd; + // place data on auction adUnits + const mergedTargeting = deepMerge(targeting); + auction.adUnits.forEach(adUnit => { + const kv = adUnit.code && mergedTargeting[adUnit.code]; + if (!kv) { + return + } + adUnit[CONSTANTS.JSON_MAPPING.ADSERVER_TARGETING] = Object.assign(adUnit[CONSTANTS.JSON_MAPPING.ADSERVER_TARGETING] || {}, kv); + }); + return auction.adUnits; } /** @@ -311,53 +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(setDataOrderByProvider(subModules, 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) { - 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}); - }) - }); -} - 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 72648f8feb5..38d83a40bc7 100644 --- a/modules/rubiconAnalyticsAdapter.js +++ b/modules/rubiconAnalyticsAdapter.js @@ -13,6 +13,14 @@ 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: { @@ -51,27 +59,16 @@ const cache = { const BID_REJECTED_IPF = 'rejected-ipf'; -let fpkvs = {}; -function updateFpkvs(fpkvs, newKvs) { - const isValid = typeof newKvs === 'object' && Object.keys(newKvs).every(key => typeof newKvs[key] === 'string'); - if (!isValid) { - utils.logError('Rubicon Analytics: fpkvs must be object with string keys and values'); - return fpkvs; - } else { - return {...fpkvs, ...newKvs}; - } -} - -let integration, ruleId, wrapperName; -// listen for any rubicon setConfig events and save them to appropriate fields! +export let rubiConf = { + pvid: utils.generateUUID().slice(0, 8) +}; // 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 => { - let rubiConf = config.rubicon; - integration = rubiConf.int_type || integration || DEFAULT_INTEGRATION; - ruleId = rubiConf.rule_name || ruleId; - wrapperName = rubiConf.wrapperName || wrapperName; - fpkvs = rubiConf.fpkvs ? updateFpkvs(fpkvs, rubiConf.fpkvs) : fpkvs + utils.mergeDeep(rubiConf, config.rubicon); + if (utils.deepAccess(config, 'rubicon.updatePageView') === true) { + rubiConf.pvid = utils.generateUUID().slice(0, 8) + } }); export function getHostNameFromReferer(referer) { @@ -128,7 +125,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', @@ -140,6 +137,7 @@ function sendMessage(auctionId, bidWonId) { 'dimensions', 'mediaType', 'floorValue', + 'floorRuleValue', 'floorRule' ]) : undefined ]); @@ -163,13 +161,13 @@ function sendMessage(auctionId, bidWonId) { let referrer = config.getConfig('pageUrl') || (auctionCache && auctionCache.referrer); let message = { eventTimeMillis: Date.now(), - integration, - ruleId, + integration: rubiConf.int_type || DEFAULT_INTEGRATION, + ruleId: rubiConf.rule_name, version: '$prebid.version$', referrerUri: referrer, referrerHostname: rubiconAdapter.referrerHostname || getHostNameFromReferer(referrer), channel: 'web', - wrapperName + wrapperName: rubiConf.wrapperName }; if (auctionCache && !auctionCache.sent) { let adUnitMap = Object.keys(auctionCache.bids).reduce((adUnits, bidId) => { @@ -239,11 +237,14 @@ function sendMessage(auctionId, bidWonId) { auction.floors = utils.pick(auctionCache.floorData, [ 'location', 'modelVersion as modelName', + 'modelWeight', + 'modelTimestamp', 'skipped', 'enforcement', () => utils.deepAccess(auctionCache.floorData, 'enforcements.enforceJS'), 'dealsEnforced', () => utils.deepAccess(auctionCache.floorData, 'enforcements.floorDeals'), 'skipRate', 'fetchStatus', + 'floorMin', 'floorProvider as provider' ]); } @@ -355,14 +356,36 @@ export function parseBidResponse(bid, previousBidResponse, auctionFloorData) { }, 'seatBidId', 'floorValue', () => utils.deepAccess(bid, 'floorData.floorValue'), + 'floorRuleValue', () => utils.deepAccess(bid, 'floorData.floorRuleValue'), 'floorRule', () => utils.debugTurnedOn() ? utils.deepAccess(bid, 'floorData.floorRule') : undefined ]); } -function getPageViewId() { - if (prebidGlobal.rp && typeof prebidGlobal.rp.generatePageViewId === 'function') { - return prebidGlobal.rp.generatePageViewId(false); +/* + 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()); + + const isValid = rubiConf.fpkvs && typeof rubiConf.fpkvs === 'object' && Object.keys(rubiConf.fpkvs).every(key => typeof rubiConf.fpkvs[key] === 'string'); + return isValid ? rubiConf.fpkvs : {}; } let samplingFactor = 1; @@ -420,8 +443,8 @@ function updateRpaCookie() { // 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, ...fpkvs}; - decodedRpaCookie.pvid = getPageViewId(); + decodedRpaCookie.fpkvs = {...decodedRpaCookie.fpkvs, ...getFpkvs()}; + decodedRpaCookie.pvid = rubiConf.pvid; setRpaCookie(decodedRpaCookie) } return decodedRpaCookie; @@ -495,8 +518,8 @@ let rubiconAdapter = Object.assign({}, baseAdapter, { }, disableAnalytics() { this.getUrl = baseAdapter.getUrl; - accountId = integration = ruleId = wrapperName = undefined; - fpkvs = {}; + accountId = undefined; + rubiConf = {}; cache.gpt.registered = false; baseAdapter.disableAnalytics.apply(this, arguments); }, @@ -534,7 +557,7 @@ let rubiconAdapter = Object.assign({}, baseAdapter, { '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 @@ -640,10 +663,22 @@ let rubiconAdapter = Object.assign({}, baseAdapter, { 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'; @@ -684,10 +719,14 @@ let rubiconAdapter = Object.assign({}, baseAdapter, { 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; } diff --git a/modules/rubiconBidAdapter.js b/modules/rubiconBidAdapter.js index 9d4b6203a86..e439f7fd2a4 100644 --- a/modules/rubiconBidAdapter.js +++ b/modules/rubiconBidAdapter.js @@ -7,38 +7,14 @@ import find from 'core-js-pure/features/array/find.js'; const DEFAULT_INTEGRATION = 'pbjs_lite'; const DEFAULT_PBS_INTEGRATION = 'pbjs'; -// always use https, regardless of whether or not current page is secure -export let fastlaneEndpoint = `https://fastlane.rubiconproject.com/a/api/fastlane.json`; -export let videoEndpoint = `https://prebid-server.rubiconproject.com/openrtb2/auction`; -export let syncEndpoint = `https://eus.rubiconproject.com/usync.html`; -let returnVast = false; - -let bannerHost = 'fastlane'; -let videoHost = 'prebid-server'; -let syncHost = 'eus'; +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 => { - let rubiConf = config.rubicon; - bannerHost = rubiConf.bannerHost || bannerHost; - fastlaneEndpoint = `https://${bannerHost}.rubiconproject.com/a/api/fastlane.json`; - videoHost = rubiConf.videoHost || videoHost; - videoEndpoint = `https://${videoHost}.rubiconproject.com/openrtb2/auction`; - syncHost = rubiConf.syncHost || syncHost; - syncEndpoint = `https://${syncHost}.rubiconproject.com/usync.html`; - returnVast = rubiConf.returnVast === true; // anything other than true is false + 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', @@ -126,7 +102,8 @@ var sizeMap = { 274: '1800x200', 278: '320x500', 282: '320x400', - 288: '640x380' + 288: '640x380', + 548: '500x1000' }; utils._each(sizeMap, (item, key) => sizeMap[item] = key); @@ -178,9 +155,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: { @@ -192,7 +169,7 @@ export const spec = { prebid: { cache: { vastxml: { - returnCreative: returnVast + returnCreative: rubiConf.returnVast === true } }, targeting: { @@ -203,7 +180,7 @@ export const spec = { }, bidders: { rubicon: { - integration: config.getConfig('rubicon.int_type') || DEFAULT_PBS_INTEGRATION + integration: rubiConf.int_type || DEFAULT_PBS_INTEGRATION } } } @@ -218,7 +195,7 @@ export const spec = { } let bidFloor; - if (typeof bidRequest.getFloor === 'function' && !config.getConfig('rubicon.disableFloors')) { + if (typeof bidRequest.getFloor === 'function' && !rubiConf.disableFloors) { let floorInfo; try { floorInfo = bidRequest.getFloor({ @@ -243,11 +220,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; @@ -265,17 +237,7 @@ export const spec = { const eids = utils.deepAccess(bidderRequest, 'bids.0.userIdAsEids'); if (eids && eids.length) { - // filter out unsupported id systems - utils.deepSetValue(data, 'user.ext.eids', eids.filter(eid => ['adserver.org', 'pubcid.org', 'liveintent.com', 'liveramp.com', 'sharedid.org'].indexOf(eid.source) !== -1)); - - // liveintent requires additional props to be set - const liveIntentEid = find(data.user.ext.eids, eid => eid.source === 'liveintent.com'); - if (liveIntentEid) { - utils.deepSetValue(data, 'user.ext.tpid', { source: liveIntentEid.source, uid: liveIntentEid.uids[0].id }); - if (liveIntentEid.ext && liveIntentEid.ext.segments) { - utils.deepSetValue(data, 'rp.target.LIseg', liveIntentEid.ext.segments); - } - } + utils.deepSetValue(data, 'user.ext.eids', eids); } // set user.id value from config value @@ -343,19 +305,19 @@ export const spec = { return { method: 'POST', - url: videoEndpoint, + 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: fastlaneEndpoint, + 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; @@ -386,7 +348,7 @@ export const spec = { // SRA request returns grouped bidRequest arrays not a plain bidRequest aggregate.push({ method: 'GET', - url: fastlaneEndpoint, + 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; @@ -403,6 +365,7 @@ export const spec = { getOrderedParams: function(params) { const containsTgV = /^tg_v/ const containsTgI = /^tg_i/ + const containsUId = /^eid_|^tpid_/ const orderedParams = [ 'account_id', @@ -415,17 +378,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', @@ -493,17 +454,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(), @@ -515,7 +474,7 @@ 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')) { + if (typeof bidRequest.getFloor === 'function' && !rubiConf.disableFloors) { let floorInfo; try { floorInfo = bidRequest.getFloor({ @@ -533,33 +492,48 @@ 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.userIdAsEids && bidRequest.userIdAsEids.length) { - const unifiedId = find(bidRequest.userIdAsEids, eid => eid.source === 'adserver.org'); - if (unifiedId) { - data['tpid_tdid'] = unifiedId.uids[0].id; - } - const liveintentId = find(bidRequest.userIdAsEids, eid => eid.source === 'liveintent.com'); - if (liveintentId) { - data['tpid_liveintent.com'] = liveintentId.uids[0].id; - if (liveintentId.ext && Array.isArray(liveintentId.ext.segments) && liveintentId.ext.segments.length) { - data['tg_v.LIseg'] = liveintentId.ext.segments.join(','); - } - } - const liverampId = find(bidRequest.userIdAsEids, eid => eid.source === 'liveramp.com'); - if (liverampId) { - data['x_liverampidl'] = liverampId.uids[0].id; - } - const sharedId = find(bidRequest.userIdAsEids, eid => eid.source === 'sharedid.org'); - if (sharedId) { - data['eid_sharedid.org'] = `${sharedId.uids[0].id}^${sharedId.uids[0].atype}^${sharedId.uids[0].ext.third}`; - } - } - - // set ppuid value from config value + // 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.ext && eid.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); + } + }); + } if (bidderRequest.gdprConsent) { // add 'gdpr' only if 'gdprApplies' is defined @@ -617,10 +591,6 @@ export const spec = { data['tg_i.dfp_ad_unit_code'] = gamAdUnit.replace(/^\/+/, ''); } - // digitrust properties - const digitrustParams = _getDigiTrustQueryParams(bidRequest, 'FASTLANE'); - Object.assign(data, digitrustParams); - if (config.getConfig('coppa') === true) { data['coppa'] = 1; } @@ -687,7 +657,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'), }; @@ -700,6 +670,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; @@ -707,6 +685,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' @@ -767,12 +746,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 } }; @@ -780,6 +759,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; @@ -829,7 +812,7 @@ export const spec = { hasSynced = true; return { type: 'iframe', - url: syncEndpoint + params + url: `https://${rubiConf.syncHost || 'eus'}.rubiconproject.com/usync.html` + params }; } }, @@ -852,38 +835,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 @@ -1075,6 +1026,7 @@ function bidType(bid, log = false) { } } +export const resetRubiConf = () => rubiConf = {}; export function masSizeOrdering(sizes) { const MAS_SIZE_PRIORITY = [15, 2, 9]; diff --git a/modules/sharedIdSystem.js b/modules/sharedIdSystem.js index 5c2a3df0595..49cac46f1df 100644 --- a/modules/sharedIdSystem.js +++ b/modules/sharedIdSystem.js @@ -21,6 +21,7 @@ const TIME_MAX = Math.pow(2, 48) - 1; const TIME_LEN = 10; const RANDOM_LEN = 16; const id = factory(); +const GVLID = 887; /** * Constructs cookie value * @param value @@ -283,6 +284,11 @@ export const sharedIdSubmodule = { */ name: MODULE_NAME, + /** + * Vendor id of Prebid + * @type {Number} + */ + gvlid: GVLID, /** * decode the stored id value for passing to bid requests * @function @@ -296,10 +302,10 @@ export const sharedIdSubmodule = { /** * performs action to obtain id and return a value. * @function - * @param {SubmoduleParams} [configParams] + * @param {SubmoduleConfig} [config] * @returns {sharedId} */ - getId(configParams) { + getId(config) { const resp = function (callback) { utils.logInfo('SharedId: Sharedid doesnt exists, new cookie creation'); ajax(ID_SVC, idGenerationCallback(callback), undefined, {method: 'GET', withCredentials: true}); @@ -309,11 +315,12 @@ export const sharedIdSubmodule = { /** * performs actions even if the id exists and returns a value - * @param configParams + * @param config * @param storedId * @returns {{callback: *}} */ - extendId(configParams, storedId) { + extendId(config, storedId) { + const configParams = (config && config.params) || {}; utils.logInfo('SharedId: Existing shared id ' + storedId.id); const resp = function (callback) { const needSync = isIdSynced(configParams, storedId); diff --git a/modules/sharethroughBidAdapter.js b/modules/sharethroughBidAdapter.js index 0d183be05df..89484b1c68b 100644 --- a/modules/sharethroughBidAdapter.js +++ b/modules/sharethroughBidAdapter.js @@ -1,7 +1,7 @@ import { registerBidder } from '../src/adapters/bidderFactory.js'; import * as utils from '../src/utils.js'; -const VERSION = '3.2.1'; +const VERSION = '3.3.0'; const BIDDER_CODE = 'sharethrough'; const STR_ENDPOINT = 'https://btlr.sharethrough.com/WYu2BXv1/v1'; const DEFAULT_SIZE = [1, 1]; @@ -50,6 +50,16 @@ export const sharethroughAdapterSpec = { query.ttduid = bidRequest.userId.tdid; } + if (bidRequest.userId && bidRequest.userId.pubcid) { + query.pubcid = bidRequest.userId.pubcid; + } else if (bidRequest.crumbs && bidRequest.crumbs.pubcid) { + query.pubcid = bidRequest.crumbs.pubcid; + } + + if (bidRequest.userId && bidRequest.userId.idl_env) { + query.idluid = bidRequest.userId.idl_env; + } + if (bidRequest.schain) { query.schain = JSON.stringify(bidRequest.schain); } @@ -58,6 +68,14 @@ export const sharethroughAdapterSpec = { query.bidfloor = parseFloat(bidRequest.bidfloor); } + if (bidRequest.params.badv) { + query.badv = bidRequest.params.badv; + } + + if (bidRequest.params.bcat) { + query.bcat = bidRequest.params.bcat; + } + // Data that does not need to go to the server, // but we need as part of interpretResponse() const strData = { @@ -67,7 +85,7 @@ export const sharethroughAdapterSpec = { }; return { - method: 'GET', + method: 'POST', url: STR_ENDPOINT, data: query, strData: strData diff --git a/modules/sharethroughBidAdapter.md b/modules/sharethroughBidAdapter.md index 2290e370cae..396b8164577 100644 --- a/modules/sharethroughBidAdapter.md +++ b/modules/sharethroughBidAdapter.md @@ -29,7 +29,13 @@ Module that connects to Sharethrough's demand sources // OPTIONAL - If iframeSize is provided, we'll use this size for the iframe // otherwise we'll grab the largest size from the sizes array // This is ignored if iframe: false - iframeSize: [250, 250] + iframeSize: [250, 250], + + // OPTIONAL - Blocked Advertiser Domains + badv: ['domain1.com', 'domain2.com'], + + // OPTIONAL - Blocked Categories (IAB codes) + bcat: ['IAB1-1', 'IAB1-2'], } } ] diff --git a/modules/sizeMappingV2.js b/modules/sizeMappingV2.js index 4df537e0eb3..ffd242e57ac 100644 --- a/modules/sizeMappingV2.js +++ b/modules/sizeMappingV2.js @@ -66,7 +66,7 @@ export function isUsingNewSizeMapping(adUnits) { }); // checks for the presence of sizeConfig property at the adUnit.bids[].bidder object - adUnit.bids.forEach(bidder => { + adUnit.bids && utils.isArray(adUnit.bids) && adUnit.bids.forEach(bidder => { if (bidder.sizeConfig) { if (isUsingSizeMappingBool === false) { isUsingSizeMappingBool = true; @@ -168,8 +168,15 @@ export function checkAdUnitSetupHook(adUnits) { } const validatedAdUnits = []; adUnits.forEach(adUnit => { + const bids = adUnit.bids; const mediaTypes = adUnit.mediaTypes; let validatedBanner, validatedVideo, validatedNative; + + if (!bids || !utils.isArray(bids)) { + utils.logError(`Detected adUnit.code '${adUnit.code}' did not have 'adUnit.bids' defined or 'adUnit.bids' is not an array. Removing adUnit from auction.`); + return; + } + if (!mediaTypes || Object.keys(mediaTypes).length === 0) { utils.logError(`Detected adUnit.code '${adUnit.code}' did not have a 'mediaTypes' object defined. This is a required field for the auction, so this adUnit has been removed.`); return; diff --git a/modules/smaatoBidAdapter.js b/modules/smaatoBidAdapter.js index ce0edb1e19c..93915689cee 100644 --- a/modules/smaatoBidAdapter.js +++ b/modules/smaatoBidAdapter.js @@ -110,6 +110,18 @@ const buildOpenRtbBidRequestPayload = (validBidRequests, bidderRequest) => { utils.deepSetValue(request, 'regs.ext.us_privacy', bidderRequest.uspConsent); } + if (utils.deepAccess(validBidRequests[0], 'params.app')) { + const geo = utils.deepAccess(validBidRequests[0], 'params.app.geo'); + utils.deepSetValue(request, 'device.geo', geo); + const ifa = utils.deepAccess(validBidRequests[0], 'params.app.ifa') + utils.deepSetValue(request, 'device.ifa', ifa); + } + + const eids = utils.deepAccess(validBidRequests[0], 'userIdAsEids'); + if (eids && eids.length) { + utils.deepSetValue(request, 'user.ext.eids', eids); + } + utils.logInfo('[SMAATO] OpenRTB Request:', request); return JSON.stringify(request); } @@ -185,7 +197,7 @@ export const spec = { ttl: ttlSec, creativeId: b.crid, dealId: b.dealid || null, - netRevenue: true, + netRevenue: utils.deepAccess(b, 'ext.net', true), currency: res.cur, meta: { advertiserDomains: b.adomain, diff --git a/modules/smartadserverBidAdapter.js b/modules/smartadserverBidAdapter.js index 97dd43fc5ba..ed9003e3b4d 100644 --- a/modules/smartadserverBidAdapter.js +++ b/modules/smartadserverBidAdapter.js @@ -13,8 +13,10 @@ import { createEidsArray } from './userId/eids.js'; const BIDDER_CODE = 'smartadserver'; +const GVL_ID = 45; export const spec = { code: BIDDER_CODE, + gvlid: GVL_ID, aliases: ['smart'], // short code supportedMediaTypes: [BANNER, VIDEO], /** @@ -83,10 +85,11 @@ export const spec = { w: size[0], h: size[1] })); - } else if (videoMediaType && videoMediaType.context === 'instream') { + } else if (videoMediaType && (videoMediaType.context === 'instream' || videoMediaType.context === 'outstream')) { // Specific attributes for instream. let playerSize = videoMediaType.playerSize[0]; - payload.isVideo = true; + payload.isVideo = videoMediaType.context === 'instream'; + payload.mediaType = VIDEO; payload.videoData = { videoProtocol: bid.params.video.protocol, playerWidth: playerSize[0], @@ -98,6 +101,7 @@ export const spec = { } if (bidderRequest && bidderRequest.gdprConsent) { + payload.addtl_consent = bidderRequest.gdprConsent.addtlConsent; payload.gdpr_consent = bidderRequest.gdprConsent.consentString; payload.gdpr = bidderRequest.gdprConsent.gdprApplies; // we're handling the undefined case server side } @@ -146,10 +150,11 @@ export const spec = { ttl: response.ttl }; - if (bidRequest.isVideo) { + if (bidRequest.mediaType === VIDEO) { bidResponse.mediaType = VIDEO; bidResponse.vastUrl = response.adUrl; bidResponse.vastXml = response.ad; + bidResponse.content = response.ad; } else { bidResponse.adUrl = response.adUrl; bidResponse.ad = response.ad; diff --git a/modules/smartadserverBidAdapter.md b/modules/smartadserverBidAdapter.md index c6f68363d7c..05e29359fd2 100644 --- a/modules/smartadserverBidAdapter.md +++ b/modules/smartadserverBidAdapter.md @@ -94,4 +94,43 @@ Please reach out to your Technical account manager for more information. } }] }; +``` + +## Outstream Video + +``` + var outstreamVideoAdUnit = { + code: 'test-div', + mediaTypes: { + video: { + context: 'outstream', + playerSize: [640, 480] + } + }, + renderer: { + url: 'https://acdn.adnxs.com/video/outstream/ANOutstreamVideo.js', + render: function (bid) { + bid.renderer.push(() => { + ANOutstreamVideo.renderAd({ + targetId: bid.adUnitCode, + adResponse: bid + }); + }); + } + }, + bids: [{ + bidder: "smart", + params: { + domain: 'https://prg.smartadserver.com', + siteId: 207435, + pageId: 896536, + formatId: 85089, + bidfloor: 5, + video: { + protocol: 6, + startDelay: 1 + } + } + }] + }; ``` \ No newline at end of file diff --git a/modules/sovrnBidAdapter.js b/modules/sovrnBidAdapter.js index f3260668b74..176b090fbe5 100644 --- a/modules/sovrnBidAdapter.js +++ b/modules/sovrnBidAdapter.js @@ -1,10 +1,12 @@ import * as utils from '../src/utils.js' import { registerBidder } from '../src/adapters/bidderFactory.js' import { BANNER } from '../src/mediaTypes.js' +import { createEidsArray } from './userId/eids.js'; export const spec = { code: 'sovrn', supportedMediaTypes: [BANNER], + gvlid: 13, /** * Check if the bid is a valid zone ID in either number or string form @@ -25,11 +27,21 @@ export const spec = { let sovrnImps = []; let iv; let schain; - let unifiedID; + let eids; + let tpid = [] + let criteoId; utils._each(bidReqs, function (bid) { - if (!unifiedID) { - unifiedID = utils.deepAccess(bid, 'userId.tdid'); + if (!eids && bid.userId) { + eids = createEidsArray(bid.userId) + eids.forEach(function (id) { + if (id.uids && id.uids[0]) { + if (id.source === 'criteo.com') { + criteoId = id.uids[0].id + } + tpid.push({source: id.source, uid: id.uids[0].id}) + } + }) } if (bid.schain) { @@ -41,7 +53,7 @@ export const spec = { bidSizes = ((utils.isArray(bidSizes) && utils.isArray(bidSizes[0])) ? bidSizes : [bidSizes]) bidSizes = bidSizes.filter(size => utils.isArray(size)) const processedSizes = bidSizes.map(size => ({w: parseInt(size[0], 10), h: parseInt(size[1], 10)})) - sovrnImps.push({ + const imp = { adunitcode: bid.adUnitCode, id: bid.bidId, banner: { @@ -51,7 +63,17 @@ export const spec = { }, tagid: String(utils.getBidIdParameter('tagid', bid.params)), bidfloor: utils.getBidIdParameter('bidfloor', bid.params) - }); + } + + const segmentsString = utils.getBidIdParameter('segments', bid.params) + + if (segmentsString) { + imp.ext = { + deals: segmentsString.split(',').map(deal => deal.trim()) + } + } + + sovrnImps.push(imp); }); const page = bidderRequest.refererInfo.referer @@ -84,19 +106,12 @@ export const spec = { utils.deepSetValue(sovrnBidReq, 'regs.ext.us_privacy', bidderRequest.uspConsent); } - if (unifiedID) { - const idArray = [{ - source: 'adserver.org', - uids: [ - { - id: unifiedID, - ext: { - rtiPartner: 'TDID' - } - } - ] - }] - utils.deepSetValue(sovrnBidReq, 'user.ext.eids', idArray) + if (eids) { + utils.deepSetValue(sovrnBidReq, 'user.ext.eids', eids) + utils.deepSetValue(sovrnBidReq, 'user.ext.tpid', tpid) + if (criteoId) { + utils.deepSetValue(sovrnBidReq, 'user.ext.prebid_criteoid', criteoId) + } } let url = `https://ap.lijit.com/rtb/bid?src=$$REPO_AND_VERSION$$`; @@ -138,7 +153,7 @@ export const spec = { netRevenue: true, mediaType: BANNER, ad: decodeURIComponent(`${sovrnBid.adm}`), - ttl: 60 + ttl: sovrnBid.ext ? (sovrnBid.ext.ttl || 90) : 90 }); }); } diff --git a/modules/spotxBidAdapter.js b/modules/spotxBidAdapter.js index 6a80fd6dc0d..6104fce1d97 100644 --- a/modules/spotxBidAdapter.js +++ b/modules/spotxBidAdapter.js @@ -1,4 +1,5 @@ import * as utils from '../src/utils.js'; +import { config } from '../src/config.js'; import { Renderer } from '../src/Renderer.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; import { VIDEO } from '../src/mediaTypes.js'; @@ -19,7 +20,7 @@ export const spec = { * From Prebid.js: isBidRequestValid - Verify the the AdUnits.bids, respond with true (valid) or false (invalid). * * @param {object} bid The bid to validate. - * @return boolean True if this is a valid bid, and false otherwise. + * @return {boolean} True if this is a valid bid, and false otherwise. */ isBidRequestValid: function(bid) { if (bid && typeof bid.params !== 'object') { @@ -64,14 +65,24 @@ export const spec = { * from Prebid.js: buildRequests - Takes an array of valid bid requests, all of which are guaranteed to have passed the isBidRequestValid() test. * * @param {BidRequest[]} bidRequests A non-empty list of bid requests which should be sent to the Server. - * @return ServerRequest Info describing the request to the server. + * @param {object} bidderRequest - The master bidRequest object. + * @return {ServerRequest} Info describing the request to the server. */ buildRequests: function(bidRequests, bidderRequest) { - const page = bidderRequest.refererInfo.referer; - const isPageSecure = !!page.match(/^https:/) + const referer = bidderRequest.refererInfo.referer; + const isPageSecure = !!referer.match(/^https:/); const siteId = ''; const spotxRequests = bidRequests.map(function(bid) { + let page; + if (utils.getBidIdParameter('page', bid.params)) { + page = utils.getBidIdParameter('page', bid.params); + } else if (config.getConfig('pageUrl')) { + page = config.getConfig('pageUrl'); + } else { + page = referer; + } + const channelId = utils.getBidIdParameter('channel_id', bid.params); let pubcid = null; @@ -228,14 +239,15 @@ export const spec = { } // ID5 fied - if (bid && bid.userId && bid.userId.id5id) { + if (utils.deepAccess(bid, 'userId.id5id.uid')) { userExt.eids = userExt.eids || []; userExt.eids.push( { source: 'id5-sync.com', uids: [{ - id: bid.userId.id5id - }] + id: bid.userId.id5id.uid + }], + ext: bid.userId.id5id.ext || {} } ) } @@ -435,11 +447,11 @@ function createOutstreamScript(bid) { const customOverride = utils.getBidIdParameter('custom_override', bid.renderer.config.outstream_options); if (customOverride && utils.isPlainObject(customOverride)) { - utils.logMessage('[SPOTX][renderer] Custom beahavior.'); + utils.logMessage('[SPOTX][renderer] Custom behavior.'); for (let name in customOverride) { if (customOverride.hasOwnProperty(name)) { if (name === 'channel_id' || name === 'vast_url' || name === 'content_page_url' || name === 'ad_unit') { - utils.logWarn('[SPOTX][renderer] Custom beahavior: following option cannot be overrided: ' + name); + utils.logWarn('[SPOTX][renderer] Custom behavior: following option cannot be overridden: ' + name); } else { dataSpotXParams['data-spotx_' + name] = customOverride[name]; } diff --git a/modules/sspBCAdapter.js b/modules/sspBCAdapter.js index ef89fb08449..4069c722e9d 100644 --- a/modules/sspBCAdapter.js +++ b/modules/sspBCAdapter.js @@ -1,14 +1,17 @@ import * as utils from '../src/utils.js'; +import { ajax } from '../src/ajax.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; import { BANNER } from '../src/mediaTypes.js'; const BIDDER_CODE = 'sspBC'; const BIDDER_URL = 'https://ssp.wp.pl/bidder/'; const SYNC_URL = 'https://ssp.wp.pl/bidder/usersync'; +const NOTIFY_URL = 'https://ssp.wp.pl/bidder/notify'; const TMAX = 450; -const BIDDER_VERSION = '4.5'; +const BIDDER_VERSION = '4.6'; const W = window; const { navigator } = W; +var consentApiVersion; const cookieSupport = () => { const isSafari = /^((?!chrome|android|crios|fxios).)*safari/i.test(navigator.userAgent); @@ -53,6 +56,7 @@ const applyClientHints = ortbRequest => { function applyGdpr(bidderRequest, ortbRequest) { if (bidderRequest && bidderRequest.gdprConsent) { + consentApiVersion = bidderRequest.gdprConsent.apiVersion; ortbRequest.regs = Object.assign(ortbRequest.regs, { '[ortb_extensions.gdpr]': bidderRequest.gdprConsent.gdprApplies ? 1 : 0 }); ortbRequest.user = Object.assign(ortbRequest.user, { '[ortb_extensions.consent]': bidderRequest.gdprConsent.consentString }); } @@ -68,6 +72,14 @@ function setOnAny(collection, key) { } } +function sendNotification(payload) { + ajax(NOTIFY_URL, null, JSON.stringify(payload), { + withCredentials: false, + method: 'POST', + crossOrigin: true + }); +} + /** * @param {object} slot Ad Unit Params by Prebid * @returns {object} Banner by OpenRTB 2.5 §3.2.6 @@ -278,6 +290,7 @@ const spec = { mediaType: 'banner', meta: { advertiserDomains: serverBid.adomain, + networkName: seat, }, netRevenue: true, ad: renderCreative(site, response.id, serverBid, seat, request.bidderRequest), @@ -303,12 +316,44 @@ const spec = { if (syncOptions.iframeEnabled) { return [{ type: 'iframe', - url: SYNC_URL, + url: SYNC_URL + '?tcf=' + consentApiVersion, }]; } utils.logWarn('sspBC adapter requires iframe based user sync.'); }, - onTimeout() { + + onTimeout(timeoutData) { + var adSlots = []; + const bid = timeoutData && timeoutData[0]; + if (bid) { + timeoutData.forEach(bid => { adSlots.push(bid.params[0] && bid.params[0].id) }) + const payload = { + event: 'timeout', + requestId: bid.auctionId, + siteId: bid.params ? [bid.params[0].siteId] : [], + slotId: adSlots, + timeout: bid.timeout, + } + sendNotification(payload); + return payload; + } + }, + + onBidWon(bid) { + if (bid && bid.auctionId) { + const payload = { + event: 'bidWon', + requestId: bid.auctionId, + siteId: bid.params ? [bid.params[0].siteId] : [], + slotId: bid.params ? [bid.params[0].id] : [], + cpm: bid.cpm, + creativeId: bid.creativeId, + adomain: (bid.meta && bid.meta.advertiserDomains) ? bid.meta.advertiserDomains[0] : '', + networkName: (bid.meta && bid.meta.networkName) ? bid.meta.networkName : '', + } + sendNotification(payload); + return payload; + } }, }; diff --git a/modules/stroeerCoreBidAdapter.js b/modules/stroeerCoreBidAdapter.js new file mode 100644 index 00000000000..ec442f5125a --- /dev/null +++ b/modules/stroeerCoreBidAdapter.js @@ -0,0 +1,196 @@ +import {registerBidder} from '../src/adapters/bidderFactory.js'; +import {BANNER} from '../src/mediaTypes.js'; +import * as utils from '../src/utils.js'; + +const GVL_ID = 136; +const BIDDER_CODE = 'stroeerCore'; +const DEFAULT_HOST = 'hb.adscale.de'; +const DEFAULT_PATH = '/dsh'; +const DEFAULT_PORT = ''; +const FIVE_MINUTES_IN_SECONDS = 300; +const USER_SYNC_IFRAME_URL = 'https://js.adscale.de/pbsync.html'; + +const isSecureWindow = () => utils.getWindowSelf().location.protocol === 'https:'; +const isMainPageAccessible = () => getMostAccessibleTopWindow() === utils.getWindowTop(); + +function getTopWindowReferrer() { + try { + return utils.getWindowTop().document.referrer; + } catch (e) { + return utils.getWindowSelf().referrer; + } +} + +function getMostAccessibleTopWindow() { + let res = utils.getWindowSelf(); + + try { + while (utils.getWindowTop().top !== res && res.parent.location.href.length) { + res = res.parent; + } + } catch (ignore) { + } + + return res; +} + +function elementInView(elementId) { + const resolveElement = (elId) => { + const win = utils.getWindowSelf(); + + return win.document.getElementById(elId); + }; + + const visibleInWindow = (el, win) => { + const rect = el.getBoundingClientRect(); + const inView = (rect.top + rect.height >= 0) && (rect.top <= win.innerHeight); + + if (win !== win.parent) { + return inView && visibleInWindow(win.frameElement, win.parent); + } + + return inView; + }; + + try { + return visibleInWindow(resolveElement(elementId), utils.getWindowSelf()); + } catch (e) { + // old browser, element not found, cross-origin etc. + } + return undefined; +} + +function buildUrl({host: hostname = DEFAULT_HOST, port = DEFAULT_PORT, securePort, path: pathname = DEFAULT_PATH}) { + if (securePort) { + port = securePort; + } + + return utils.buildUrl({protocol: 'https', hostname, port, pathname}); +} + +function getGdprParams(gdprConsent) { + if (gdprConsent) { + const consentString = encodeURIComponent(gdprConsent.consentString || '') + const isGdpr = gdprConsent.gdprApplies ? 1 : 0; + + return `?gdpr=${isGdpr}&gdpr_consent=${consentString}` + } else { + return ''; + } +} + +export const spec = { + code: BIDDER_CODE, + gvlid: GVL_ID, + supportedMediaTypes: [BANNER], + + isBidRequestValid: (function () { + const validators = []; + + const createValidator = (checkFn, errorMsgFn) => { + return (bidRequest) => { + if (checkFn(bidRequest)) { + return true; + } else { + utils.logError(`invalid bid: ${errorMsgFn(bidRequest)}`, 'ERROR'); + return false; + } + } + }; + + function isBanner(bidReq) { + return (!bidReq.mediaTypes && !bidReq.mediaType) || + (bidReq.mediaTypes && bidReq.mediaTypes.banner) || + bidReq.mediaType === BANNER; + } + + validators.push(createValidator((bidReq) => isBanner(bidReq), + bidReq => `bid request ${bidReq.bidId} is not a banner`)); + validators.push(createValidator((bidReq) => typeof bidReq.params === 'object', + bidReq => `bid request ${bidReq.bidId} does not have custom params`)); + validators.push(createValidator((bidReq) => utils.isStr(bidReq.params.sid), + bidReq => `bid request ${bidReq.bidId} does not have a sid string field`)); + + return function (bidRequest) { + return validators.every(f => f(bidRequest)); + } + }()), + + buildRequests: function (validBidRequests = [], bidderRequest) { + const anyBid = bidderRequest.bids[0]; + + const payload = { + id: bidderRequest.auctionId, + bids: [], + ref: getTopWindowReferrer(), + ssl: isSecureWindow(), + mpa: isMainPageAccessible(), + timeout: bidderRequest.timeout - (Date.now() - bidderRequest.auctionStart) + }; + + const userIds = anyBid.userId; + + if (!utils.isEmpty(userIds)) { + payload.user = { + euids: userIds + }; + } + + const gdprConsent = bidderRequest.gdprConsent; + + if (gdprConsent && gdprConsent.consentString != null && gdprConsent.gdprApplies != null) { + payload.gdpr = { + consent: bidderRequest.gdprConsent.consentString, applies: bidderRequest.gdprConsent.gdprApplies + }; + } + + function bidSizes(bid) { + return utils.deepAccess(bid, 'mediaTypes.banner.sizes') || bid.sizes /* for prebid < 3 */ || []; + } + + validBidRequests.forEach(bid => { + payload.bids.push({ + bid: bid.bidId, sid: bid.params.sid, siz: bidSizes(bid), viz: elementInView(bid.adUnitCode) + }); + }); + + return { + method: 'POST', url: buildUrl(anyBid.params), data: payload + } + }, + + interpretResponse: function (serverResponse) { + const bids = []; + + if (serverResponse.body && typeof serverResponse.body === 'object') { + serverResponse.body.bids.forEach(bidResponse => { + bids.push({ + requestId: bidResponse.bidId, + cpm: bidResponse.cpm || 0, + width: bidResponse.width || 0, + height: bidResponse.height || 0, + ad: bidResponse.ad, + ttl: FIVE_MINUTES_IN_SECONDS, + currency: 'EUR', + netRevenue: true, + creativeId: '', + }); + }); + } + + return bids; + }, + + getUserSyncs: function (syncOptions, serverResponses, gdprConsent) { + if (serverResponses.length > 0 && syncOptions.iframeEnabled) { + return [{ + type: 'iframe', + url: USER_SYNC_IFRAME_URL + getGdprParams(gdprConsent) + }]; + } + + return []; + } +}; + +registerBidder(spec); diff --git a/modules/stroeerCoreBidAdapter.md b/modules/stroeerCoreBidAdapter.md new file mode 100644 index 00000000000..fe6e92057c6 --- /dev/null +++ b/modules/stroeerCoreBidAdapter.md @@ -0,0 +1,31 @@ +## Overview + +``` +Module Name: Stroeer Bidder Adapter +Module Type: Bidder Adapter +Maintainer: help@cz.stroeer-labs.com +``` + + +## Ad unit configuration for publishers + +```javascript +const adUnits = [{ + code: 'div-gpt-ad-1460505748561-0', + mediaTypes: { + banner: { + sizes: [[300, 250]], + } + }, + bids: [{ + bidder: 'stroeerCore', + params: { + sid: "06b782cc-091b-4f53-9cd2-0291679aa1ac" + } + }] +}]; +``` +### Config Notes + +* Slot id (`sid`) is required. The adapter will ignore bid requests from prebid if `sid` is not provided. This must be in the decoded form. For example, "1234" as opposed to "MTM0ODA=". +* The server ignores dimensions that are not supported by the slot or by the platform (such as 987x123). diff --git a/modules/sublimeBidAdapter.js b/modules/sublimeBidAdapter.js index 1f8cb59f442..7a573ca4c4b 100644 --- a/modules/sublimeBidAdapter.js +++ b/modules/sublimeBidAdapter.js @@ -9,7 +9,7 @@ const DEFAULT_CURRENCY = 'EUR'; const DEFAULT_PROTOCOL = 'https'; const DEFAULT_TTL = 600; const SUBLIME_ANTENNA = 'antenna.ayads.co'; -const SUBLIME_VERSION = '0.5.2'; +const SUBLIME_VERSION = '0.7.0'; /** * Debug log message @@ -23,7 +23,8 @@ export function log(msg, obj) { // Default state export const state = { zoneId: '', - transactionId: '' + transactionId: '', + notifyId: '' }; /** @@ -47,9 +48,9 @@ export function sendEvent(eventName) { z: state.zoneId, e: eventName, src: 'pa', - puid: state.transactionId, - trId: state.transactionId, - ver: SUBLIME_VERSION, + puid: state.transactionId || state.notifyId, + trId: state.transactionId || state.notifyId, + pbav: SUBLIME_VERSION, }; log('Sending pixel for event: ' + eventName, eventObject); @@ -101,6 +102,7 @@ function buildRequests(validBidRequests, bidderRequest) { setState({ transactionId: bid.transactionId, + notifyId: bid.params.notifyId, zoneId: bid.params.zoneId, debug: bid.params.debug || false, }); @@ -117,6 +119,7 @@ function buildRequests(validBidRequests, bidderRequest) { h: size[1], })), transactionId: bid.transactionId, + notifyId: bid.params.notifyId, zoneId: bid.params.zoneId, }; @@ -125,10 +128,10 @@ function buildRequests(validBidRequests, bidderRequest) { return { method: 'POST', url: protocol + '://' + bidHost + '/bid', - data: payload, + data: JSON.stringify(payload), options: { - contentType: 'application/json', - withCredentials: true + contentType: 'text/plain', + withCredentials: false }, } }); @@ -207,6 +210,7 @@ export const spec = { code: BIDDER_CODE, gvlid: BIDDER_GVLID, aliases: [], + sendEvent: sendEvent, isBidRequestValid: isBidRequestValid, buildRequests: buildRequests, interpretResponse: interpretResponse, diff --git a/modules/sublimeBidAdapter.md b/modules/sublimeBidAdapter.md index e57f4a1fdb0..5cd1c95b682 100644 --- a/modules/sublimeBidAdapter.md +++ b/modules/sublimeBidAdapter.md @@ -9,7 +9,7 @@ Maintainer: pbjs@sublimeskinz.com # Description Connects to Sublime for bids. -Sublime bid adapter supports Skinz and M-Skinz formats. +Sublime bid adapter supports Skinz. # Nota Bene @@ -53,10 +53,13 @@ var adUnits = [{ bids: [{ bidder: 'sublime', params: { - zoneId: + zoneId: , + notifyId: } }] }]; ``` -Where you replace `` by your Sublime Zone id +Where you replace: +- `` by your Sublime Zone id; +- `` by your Sublime Notify id diff --git a/modules/synacormediaBidAdapter.js b/modules/synacormediaBidAdapter.js index e0d017a6c51..6608107c93f 100644 --- a/modules/synacormediaBidAdapter.js +++ b/modules/synacormediaBidAdapter.js @@ -37,7 +37,7 @@ export const spec = { const openRtbBidRequest = { id: bidderRequest.auctionId, site: { - domain: location.hostname, + domain: config.getConfig('publisherDomain') || location.hostname, page: refererInfo.referer, ref: document.referrer }, diff --git a/modules/teadsBidAdapter.js b/modules/teadsBidAdapter.js index 08ae1854669..aad7f6762c4 100644 --- a/modules/teadsBidAdapter.js +++ b/modules/teadsBidAdapter.js @@ -1,6 +1,7 @@ import {registerBidder} from '../src/adapters/bidderFactory.js'; const utils = require('../src/utils.js'); const BIDDER_CODE = 'teads'; +const GVL_ID = 132; const ENDPOINT_URL = 'https://a.teads.tv/hb/bid-request'; const gdprStatus = { GDPR_APPLIES_PUBLISHER: 12, @@ -11,6 +12,7 @@ const gdprStatus = { export const spec = { code: BIDDER_CODE, + gvlid: GVL_ID, supportedMediaTypes: ['video', 'banner'], /** * Determines whether or not the given bid request is valid. @@ -133,8 +135,8 @@ function getTimeToFirstByte(win) { performance.getEntriesByType('navigation')[0] && performance.getEntriesByType('navigation')[0].responseStart && performance.getEntriesByType('navigation')[0].requestStart && - performance.getEntriesByType('navigation')[0].responseStart >= 0 && - performance.getEntriesByType('navigation')[0].requestStart >= 0 && + performance.getEntriesByType('navigation')[0].responseStart > 0 && + performance.getEntriesByType('navigation')[0].requestStart > 0 && Math.round( performance.getEntriesByType('navigation')[0].responseStart - performance.getEntriesByType('navigation')[0].requestStart ); @@ -146,8 +148,8 @@ function getTimeToFirstByte(win) { const ttfbWithTimingV1 = performance && performance.timing.responseStart && performance.timing.requestStart && - performance.timing.responseStart >= 0 && - performance.timing.requestStart >= 0 && + performance.timing.responseStart > 0 && + performance.timing.requestStart > 0 && performance.timing.responseStart - performance.timing.requestStart; return ttfbWithTimingV1 ? ttfbWithTimingV1.toString() : ''; diff --git a/modules/telariaBidAdapter.js b/modules/telariaBidAdapter.js index acc20f6b183..71651b5af94 100644 --- a/modules/telariaBidAdapter.js +++ b/modules/telariaBidAdapter.js @@ -124,7 +124,7 @@ function getDefaultSrcPageUrl() { } function getEncodedValIfNotEmpty(val) { - return !utils.isEmpty(val) ? encodeURIComponent(val) : ''; + return (val !== '' && val !== undefined) ? encodeURIComponent(val) : ''; } /** diff --git a/modules/tripleliftBidAdapter.js b/modules/tripleliftBidAdapter.js index b003de7785f..4679c1faf62 100644 --- a/modules/tripleliftBidAdapter.js +++ b/modules/tripleliftBidAdapter.js @@ -3,20 +3,17 @@ import { registerBidder } from '../src/adapters/bidderFactory.js'; import * as utils from '../src/utils.js'; import { config } from '../src/config.js'; +const GVLID = 28; const BIDDER_CODE = 'triplelift'; const STR_ENDPOINT = 'https://tlx.3lift.com/header/auction?'; let gdprApplies = true; let consentString = null; export const tripleliftAdapterSpec = { - + gvlid: GVLID, code: BIDDER_CODE, supportedMediaTypes: [BANNER, VIDEO], isBidRequestValid: function (bid) { - if (bid.mediaTypes.video) { - let video = _getORTBVideo(bid); - if (!video.w || !video.h) return false; - } return typeof bid.params.inventoryCode !== 'undefined'; }, @@ -119,11 +116,15 @@ function _buildPostBody(bidRequests) { tagid: bidRequest.params.inventoryCode, floor: _getFloor(bidRequest) }; - if (bidRequest.mediaTypes.video) { + // remove the else to support multi-imp + if (_isInstreamBidRequest(bidRequest)) { imp.video = _getORTBVideo(bidRequest); } else if (bidRequest.mediaTypes.banner) { imp.banner = { format: _sizes(bidRequest.sizes) }; }; + if (!utils.isEmpty(bidRequest.fpd)) { + imp.fpd = _getAdUnitFpd(bidRequest.fpd); + } return imp; }); @@ -147,6 +148,16 @@ function _buildPostBody(bidRequests) { return data; } +function _isInstreamBidRequest(bidRequest) { + if (!bidRequest.mediaTypes.video) return false; + if (!bidRequest.mediaTypes.video.context) return false; + if (bidRequest.mediaTypes.video.context.toLowerCase() === 'instream') { + return true; + } else { + return false; + } +} + function _getORTBVideo(bidRequest) { // give precedent to mediaTypes.video let video = { ...bidRequest.params.video, ...bidRequest.mediaTypes.video }; @@ -175,12 +186,34 @@ function _getFloor (bid) { } function _getGlobalFpd() { - let fpd = {}; + const fpd = {}; + const context = {} + const user = {}; + const fpdContext = Object.assign({}, config.getConfig('fpd.context')); const fpdUser = Object.assign({}, config.getConfig('fpd.user')); - _addEntries(fpd, fpdContext); - _addEntries(fpd, fpdUser); + _addEntries(context, fpdContext); + _addEntries(user, fpdUser); + + if (!utils.isEmpty(context)) { + fpd.context = context; + } + if (!utils.isEmpty(user)) { + fpd.user = user; + } + return fpd; +} + +function _getAdUnitFpd(adUnitFpd) { + const fpd = {}; + const context = {}; + + _addEntries(context, adUnitFpd.context); + + if (!utils.isEmpty(context)) { + fpd.context = context; + } return fpd; } @@ -277,7 +310,7 @@ function _buildResponseObject(bidderRequest, bid) { meta: {} }; - if (breq.mediaTypes.video) { + if (_isInstreamBidRequest(breq)) { bidResponse.vastXml = bid.ad; bidResponse.mediaType = 'video'; }; @@ -285,6 +318,10 @@ function _buildResponseObject(bidderRequest, bid) { if (bid.advertiser_name) { bidResponse.meta.advertiserName = bid.advertiser_name; } + + if (bid.adomain && bid.adomain.length) { + bidResponse.meta.advertiserDomains = bid.adomain; + } }; return bidResponse; } diff --git a/modules/truereachBidAdapter.js b/modules/truereachBidAdapter.js index 2de7edbc04d..92f9cc9951e 100755 --- a/modules/truereachBidAdapter.js +++ b/modules/truereachBidAdapter.js @@ -79,6 +79,26 @@ export const spec = { return bidResponses; }, + getUserSyncs: function(syncOptions, serverResponses, gdprConsent, uspConsent) { + const syncs = [] + + var gdprParams = ''; + if (gdprConsent) { + if (typeof gdprConsent.gdprApplies === 'boolean') { + gdprParams = `?gdpr=${Number(gdprConsent.gdprApplies)}&gdpr_consent=${gdprConsent.consentString}`; + } else { + gdprParams = `?gdpr_consent=${gdprConsent.consentString}`; + } + } + + if (syncOptions.iframeEnabled) { + syncs.push({ + type: 'iframe', + url: 'http://ads.momagic.com/jsp/usersync.jsp' + gdprParams + }); + } + return syncs; + } }; diff --git a/modules/unifiedIdSystem.js b/modules/unifiedIdSystem.js index f916030d643..3db4003c424 100644 --- a/modules/unifiedIdSystem.js +++ b/modules/unifiedIdSystem.js @@ -30,10 +30,11 @@ export const unifiedIdSubmodule = { /** * performs action to obtain id and return a value in the callback's response argument * @function - * @param {SubmoduleParams} [configParams] + * @param {SubmoduleConfig} [config] * @returns {IdResponse|undefined} */ - getId(configParams) { + getId(config) { + const configParams = (config && config.params) || {}; if (!configParams || (typeof configParams.partner !== 'string' && typeof configParams.url !== 'string')) { utils.logError('User ID - unifiedId submodule requires either partner or url to be defined'); return; diff --git a/modules/userId/eids.js b/modules/userId/eids.js index eebd0146d50..d714d09962d 100644 --- a/modules/userId/eids.js +++ b/modules/userId/eids.js @@ -30,8 +30,16 @@ const USER_IDS_CONFIG = { // id5Id 'id5id': { + getValue: function(data) { + return data.uid + }, source: 'id5-sync.com', - atype: 1 + atype: 1, + getEidExt: function(data) { + if (data.ext) { + return data.ext; + } + } }, // parrableId @@ -142,6 +150,24 @@ const USER_IDS_CONFIG = { 'quantcastId': { source: 'quantcast.com', atype: 1 + }, + + // IDx + 'idx': { + source: 'idx.lat', + atype: 1 + }, + + // Verizon Media ConnectID + 'connectid': { + source: 'verizonmedia.com', + atype: 1 + }, + + // Neustar Fabrick + 'fabrickId': { + source: 'neustar.biz', + atype: 1 } }; @@ -182,9 +208,13 @@ export function createEidsArray(bidRequestUserId) { let eids = []; for (const subModuleKey in bidRequestUserId) { if (bidRequestUserId.hasOwnProperty(subModuleKey)) { - const eid = createEidObject(bidRequestUserId[subModuleKey], subModuleKey); - if (eid) { - eids.push(eid); + if (subModuleKey === 'pubProvidedId') { + eids = eids.concat(bidRequestUserId['pubProvidedId']); + } else { + const eid = createEidObject(bidRequestUserId[subModuleKey], subModuleKey); + if (eid) { + eids.push(eid); + } } } } diff --git a/modules/userId/eids.md b/modules/userId/eids.md index 03aec46cf48..fecf7e888bf 100644 --- a/modules/userId/eids.md +++ b/modules/userId/eids.md @@ -1,7 +1,8 @@ ## Example of eids array generated by UserId Module. + ``` userIdAsEids = [ - { + { source: 'pubcid.org', uids: [{ id: 'some-random-id-value', @@ -20,11 +21,23 @@ userIdAsEids = [ }] }, + { + source: 'neustar.biz', + uids: [{ + id: 'some-random-id-value', + atype: 1 + }] + }, + { source: 'id5-sync.com', uids: [{ id: 'some-random-id-value', atype: 1 + }, + ext: { + linkType: 2, + abTestingControlGroup: false }] }, @@ -86,16 +99,18 @@ userIdAsEids = [ atype: 1 }] }, + { source: 'sharedid.org', uids: [{ id: 'some-random-id-value', atype: 1, - ext: { + ext: { third: 'some-random-id-value' } }] }, + { source: 'zeotap.com', uids: [{ @@ -103,6 +118,7 @@ userIdAsEids = [ atype: 1 }] }, + { source: 'audigent.com', uids: [{ @@ -110,12 +126,21 @@ userIdAsEids = [ atype: 1 }] }, + { source: 'quantcast.com', uids: [{ id: 'some-random-id-value', atype: 1 }] + }, + + { + source: 'verizonmedia.com', + uids: [{ + id: 'some-random-id-value', + atype: 1 + }] } ] ``` diff --git a/modules/userId/index.js b/modules/userId/index.js index 14f7ad3599b..f063fbc973b 100644 --- a/modules/userId/index.js +++ b/modules/userId/index.js @@ -14,7 +14,7 @@ * If IdResponse#callback is defined, then it'll called at the end of auction. * It's permissible to return neither, one, or both fields. * @name Submodule#getId - * @param {SubmoduleParams} configParams + * @param {SubmoduleConfig} config * @param {ConsentData|undefined} consentData * @param {(Object|undefined)} cacheIdObj * @return {(IdResponse|undefined)} A response object that contains id and/or callback. @@ -27,7 +27,7 @@ * If IdResponse#callback is defined, then it'll called at the end of auction. * It's permissible to return neither, one, or both fields. * @name Submodule#extendId - * @param {SubmoduleParams} configParams + * @param {SubmoduleConfig} config * @param {Object} storedId - existing id, if any * @return {(IdResponse|function(callback:function))} A response object that contains id and/or callback. */ @@ -37,7 +37,7 @@ * @summary decode a stored value for passing to bid requests * @name Submodule#decode * @param {Object|string} value - * @param {SubmoduleParams|undefined} configParams + * @param {SubmoduleConfig|undefined} config * @return {(Object|undefined)} */ @@ -83,8 +83,9 @@ * @property {(string|undefined)} publisherId - the unique identifier of the publisher in question * @property {(string|undefined)} ajaxTimeout - the number of milliseconds a resolution request can take before automatically being terminated * @property {(array|undefined)} identifiersToResolve - the identifiers from either ls|cookie to be attached to the getId query - * @property {(string|undefined)} providedIdentifierName - defines the name of an identifier that can be found in local storage or in the cookie jar that can be sent along with the getId request. This parameter should be used whenever a customer is able to provide the most stable identifier possible * @property {(LiveIntentCollectConfig|undefined)} liCollectConfig - the config for LiveIntent's collect requests + * @property {(string|undefined)} pd - publisher provided data for reconciling ID5 IDs + * @property {(string|undefined)} emailHash - if provided, the hashed email address of a user */ /** @@ -108,15 +109,20 @@ * @property {(function|undefined)} callback - function that will return an id */ +/** + * @typedef {Object} RefreshUserIdsOptions + * @property {(string[]|undefined)} submoduleNames - submodules to refresh + */ + import find from 'core-js-pure/features/array/find.js'; -import {config} from '../../src/config.js'; +import { config } from '../../src/config.js'; import events from '../../src/events.js'; import * as utils from '../../src/utils.js'; -import {getGlobal} from '../../src/prebidGlobal.js'; -import {gdprDataHandler} from '../../src/adapterManager.js'; +import { getGlobal } from '../../src/prebidGlobal.js'; +import { gdprDataHandler } from '../../src/adapterManager.js'; import CONSTANTS from '../../src/constants.json'; -import {module, hook} from '../../src/hook.js'; -import {createEidsArray} from './eids.js'; +import { module, hook } from '../../src/hook.js'; +import { createEidsArray } from './eids.js'; import { getCoreStorageManager } from '../../src/storageManager.js'; const MODULE_NAME = 'User ID'; @@ -128,6 +134,7 @@ const CONSENT_DATA_COOKIE_STORAGE_CONFIG = { name: '_pbjs_userid_consent_data', expires: 30 // 30 days expiration, which should match how often consent is refreshed by CMPs }; +export const PBJS_USER_ID_OPTOUT_NAME = '_pbjs_id_optout'; export const coreStorage = getCoreStorageManager('userid'); /** @type {string[]} */ @@ -313,8 +320,14 @@ function hasGDPRConsent(consentData) { * @param {function} cb - callback for after processing is done. */ function processSubmoduleCallbacks(submodules, cb) { - const done = cb ? utils.delayExecution(cb, submodules.length) : function() { }; - submodules.forEach(function(submodule) { + let done = () => {}; + if (cb) { + done = utils.delayExecution(() => { + clearTimeout(timeoutID); + cb(); + }, submodules.length); + } + submodules.forEach(function (submodule) { submodule.callback(function callbackCompleted(idObj) { // if valid, id data should be saved to cookie/html storage if (idObj) { @@ -322,7 +335,7 @@ function processSubmoduleCallbacks(submodules, cb) { setStoredValue(submodule, idObj); } // cache decoded value (this is copied to every adUnit bid) - submodule.idObj = submodule.submodule.decode(idObj); + submodule.idObj = submodule.submodule.decode(idObj, submodule.config); } else { utils.logInfo(`${MODULE_NAME}: ${submodule.submodule.name} - request id responded with an empty value`); } @@ -332,7 +345,6 @@ function processSubmoduleCallbacks(submodules, cb) { // clear callback, this prop is used to test if all submodule callbacks are complete below submodule.callback = undefined; }); - clearTimeout(timeoutID); } /** @@ -365,11 +377,13 @@ function addIdDataToAdUnitBids(adUnits, submodules) { const combinedSubmoduleIdsAsEids = createEidsArray(combinedSubmoduleIds); if (Object.keys(combinedSubmoduleIds).length) { adUnits.forEach(adUnit => { - adUnit.bids.forEach(bid => { - // create a User ID object on the bid, - bid.userId = combinedSubmoduleIds; - bid.userIdAsEids = combinedSubmoduleIdsAsEids; - }); + if (adUnit.bids && utils.isArray(adUnit.bids)) { + adUnit.bids.forEach(bid => { + // create a User ID object on the bid, + bid.userId = combinedSubmoduleIds; + bid.userIdAsEids = combinedSubmoduleIdsAsEids; + }); + } }); } } @@ -392,7 +406,7 @@ function initializeSubmodulesAndExecuteCallbacks(continueAuction) { // delay auction until ids are available delayed = true; let continued = false; - const continueCallback = function() { + const continueCallback = function () { if (!continued) { continued = true; continueAuction(); @@ -409,7 +423,7 @@ function initializeSubmodulesAndExecuteCallbacks(continueAuction) { // when syncDelay is zero, process callbacks now, otherwise delay process with a setTimeout if (syncDelay > 0) { - setTimeout(function() { + setTimeout(function () { processSubmoduleCallbacks(submodulesWithCallbacks); }, syncDelay); } else { @@ -437,7 +451,7 @@ function initializeSubmodulesAndExecuteCallbacks(continueAuction) { */ export function requestBidsHook(fn, reqBidsConfigObj) { // initialize submodules only when undefined - initializeSubmodulesAndExecuteCallbacks(function() { + initializeSubmodulesAndExecuteCallbacks(function () { // pass available user id data to bid adapters addIdDataToAdUnitBids(reqBidsConfigObj.adUnits || getGlobal().adUnits, initializedSubmodules); // calling fn allows prebid to continue processing @@ -465,13 +479,112 @@ function getUserIdsAsEids() { return createEidsArray(getCombinedSubmoduleIds(initializedSubmodules)); } +/** +* This function will be exposed in the global-name-space so that userIds can be refreshed after initialization. +* @param {RefreshUserIdsOptions} options +*/ +function refreshUserIds(options, callback) { + let submoduleNames = options ? options.submoduleNames : null; + if (!submoduleNames) { + submoduleNames = []; + } + + initializeSubmodulesAndExecuteCallbacks(function() { + let consentData = gdprDataHandler.getConsentData() + + const storedConsentData = getStoredConsentData(); + setStoredConsentData(consentData); + + // gdpr consent with purpose one is required, otherwise exit immediately + let {userIdModules, hasValidated} = validateGdprEnforcement(submodules, consentData); + if (!hasValidated && !hasGDPRConsent(consentData)) { + utils.logWarn(`${MODULE_NAME} - gdpr permission not valid for local storage or cookies, exit module`); + return; + } + + let callbackSubmodules = []; + for (let submodule of userIdModules) { + if (submoduleNames.length > 0 && + submoduleNames.indexOf(submodule.submodule.name) === -1) { + continue; + } + + utils.logInfo(`${MODULE_NAME} - refreshing ${submodule.submodule.name}`); + populateSubmoduleId(submodule, consentData, storedConsentData, true); + + if (utils.isFn(submodule.callback)) { + callbackSubmodules.push(submodule); + } + } + + if (callbackSubmodules.length > 0) { + processSubmoduleCallbacks(callbackSubmodules); + } + + if (callback) { + callback(); + } + }); +} + /** * This hook returns updated list of submodules which are allowed to do get user id based on TCF 2 enforcement rules configured */ export const validateGdprEnforcement = hook('sync', function (submodules, consentData) { - return {userIdModules: submodules, hasValidated: consentData && consentData.hasValidated}; + return { userIdModules: submodules, hasValidated: consentData && consentData.hasValidated }; }, 'validateGdprEnforcement'); +function populateSubmoduleId(submodule, consentData, storedConsentData, forceRefresh) { + // There are two submodule configuration types to handle: storage or value + // 1. storage: retrieve user id data from cookie/html storage or with the submodule's getId method + // 2. value: pass directly to bids + if (submodule.config.storage) { + let storedId = getStoredValue(submodule.config.storage); + let response; + + let refreshNeeded = false; + if (typeof submodule.config.storage.refreshInSeconds === 'number') { + const storedDate = new Date(getStoredValue(submodule.config.storage, 'last')); + refreshNeeded = storedDate && (Date.now() - storedDate.getTime() > submodule.config.storage.refreshInSeconds * 1000); + } + + if (!storedId || refreshNeeded || forceRefresh || !storedConsentDataMatchesConsentData(storedConsentData, consentData)) { + // No id previously saved, or a refresh is needed, or consent has changed. Request a new id from the submodule. + response = submodule.submodule.getId(submodule.config, consentData, storedId); + } else if (typeof submodule.submodule.extendId === 'function') { + // If the id exists already, give submodule a chance to decide additional actions that need to be taken + response = submodule.submodule.extendId(submodule.config, storedId); + } + + if (utils.isPlainObject(response)) { + if (response.id) { + // A getId/extendId result assumed to be valid user id data, which should be saved to users local storage or cookies + setStoredValue(submodule, response.id); + storedId = response.id; + } + + if (typeof response.callback === 'function') { + // Save async callback to be invoked after auction + submodule.callback = response.callback; + } + } + + if (storedId) { + // cache decoded value (this is copied to every adUnit bid) + submodule.idObj = submodule.submodule.decode(storedId, submodule.config); + } + } else if (submodule.config.value) { + // cache decoded value (this is copied to every adUnit bid) + submodule.idObj = submodule.config.value; + } else { + const response = submodule.submodule.getId(submodule.config, consentData, undefined); + if (utils.isPlainObject(response)) { + if (typeof response.callback === 'function') { submodule.callback = response.callback; } + if (response.id) { submodule.idObj = submodule.submodule.decode(response.id, submodule.config); } + } + } +} + /** * @param {SubmoduleContainer[]} submodules * @param {ConsentData} consentData @@ -483,61 +596,14 @@ function initSubmodules(submodules, consentData) { setStoredConsentData(consentData); // gdpr consent with purpose one is required, otherwise exit immediately - let {userIdModules, hasValidated} = validateGdprEnforcement(submodules, consentData); + let { userIdModules, hasValidated } = validateGdprEnforcement(submodules, consentData); if (!hasValidated && !hasGDPRConsent(consentData)) { utils.logWarn(`${MODULE_NAME} - gdpr permission not valid for local storage or cookies, exit module`); return []; } return userIdModules.reduce((carry, submodule) => { - // There are two submodule configuration types to handle: storage or value - // 1. storage: retrieve user id data from cookie/html storage or with the submodule's getId method - // 2. value: pass directly to bids - if (submodule.config.storage) { - let storedId = getStoredValue(submodule.config.storage); - let response; - - let refreshNeeded = false; - if (typeof submodule.config.storage.refreshInSeconds === 'number') { - const storedDate = new Date(getStoredValue(submodule.config.storage, 'last')); - refreshNeeded = storedDate && (Date.now() - storedDate.getTime() > submodule.config.storage.refreshInSeconds * 1000); - } - - if (!storedId || refreshNeeded || !storedConsentDataMatchesConsentData(storedConsentData, consentData)) { - // No id previously saved, or a refresh is needed, or consent has changed. Request a new id from the submodule. - response = submodule.submodule.getId(submodule.config.params, consentData, storedId); - } else if (typeof submodule.submodule.extendId === 'function') { - // If the id exists already, give submodule a chance to decide additional actions that need to be taken - response = submodule.submodule.extendId(submodule.config.params, storedId); - } - - if (utils.isPlainObject(response)) { - if (response.id) { - // A getId/extendId result assumed to be valid user id data, which should be saved to users local storage or cookies - setStoredValue(submodule, response.id); - storedId = response.id; - } - - if (typeof response.callback === 'function') { - // Save async callback to be invoked after auction - submodule.callback = response.callback; - } - } - - if (storedId) { - // cache decoded value (this is copied to every adUnit bid) - submodule.idObj = submodule.submodule.decode(storedId, submodule.config.params); - } - } else if (submodule.config.value) { - // cache decoded value (this is copied to every adUnit bid) - submodule.idObj = submodule.config.value; - } else { - const response = submodule.submodule.getId(submodule.config.params, consentData, undefined); - if (utils.isPlainObject(response)) { - if (typeof response.callback === 'function') { submodule.callback = response.callback; } - if (response.id) { submodule.idObj = submodule.submodule.decode(response.id, submodule.config.params); } - } - } + populateSubmoduleId(submodule, consentData, storedConsentData, false); carry.push(submodule); return carry; }, []); @@ -602,7 +668,7 @@ function updateSubmodules() { if (!addedUserIdHook && submodules.length) { // priority value 40 will load after consentManagement with a priority of 50 getGlobal().requestBids.before(requestBidsHook, 40); - utils.logInfo(`${MODULE_NAME} - usersync config updated for ${submodules.length} submodules`); + utils.logInfo(`${MODULE_NAME} - usersync config updated for ${submodules.length} submodules: `, submodules.map(a => a.submodule.name)); addedUserIdHook = true; } } @@ -636,15 +702,15 @@ export function init(config) { ].filter(i => i !== null); // exit immediately if opt out cookie or local storage keys exists. - if (validStorageTypes.indexOf(COOKIE) !== -1 && (coreStorage.getCookie('_pbjs_id_optout') || coreStorage.getCookie('_pubcid_optout'))) { + if (validStorageTypes.indexOf(COOKIE) !== -1 && coreStorage.getCookie(PBJS_USER_ID_OPTOUT_NAME)) { utils.logInfo(`${MODULE_NAME} - opt-out cookie found, exit module`); return; } - // _pubcid_optout is checked for compatibility with pubCommonId - if (validStorageTypes.indexOf(LOCAL_STORAGE) !== -1 && (coreStorage.getDataFromLocalStorage('_pbjs_id_optout') || coreStorage.getDataFromLocalStorage('_pubcid_optout'))) { + if (validStorageTypes.indexOf(LOCAL_STORAGE) !== -1 && coreStorage.getDataFromLocalStorage(PBJS_USER_ID_OPTOUT_NAME)) { utils.logInfo(`${MODULE_NAME} - opt-out localStorage found, exit module`); return; } + // listen for config userSyncs to be set config.getConfig(conf => { // Note: support for 'usersync' was dropped as part of Prebid.js 4.0 @@ -660,6 +726,7 @@ export function init(config) { // exposing getUserIds function in global-name-space so that userIds stored in Prebid can be used by external codes. (getGlobal()).getUserIds = getUserIds; (getGlobal()).getUserIdsAsEids = getUserIdsAsEids; + (getGlobal()).refreshUserIds = refreshUserIds; } // init config update listener to start the application diff --git a/modules/userId/userId.md b/modules/userId/userId.md index a47ecd9f08c..267b3a60cea 100644 --- a/modules/userId/userId.md +++ b/modules/userId/userId.md @@ -26,12 +26,12 @@ pbjs.setConfig({ name: "id5Id", params: { partner: 173, // Set your real ID5 partner ID here for production, please ask for one at https://id5.io/universal-id - pd: "some-pd-string" // See https://wiki.id5.io/display/PD/Prebid.js+UserId+Module for details + pd: "some-pd-string" // See https://wiki.id5.io/x/BIAZ for details }, storage: { - type: "cookie", - name: "id5id.1st", - expires: 90, // Expiration of cookies in days + type: "html5", // ID5 requires html5 + name: "id5id", + expires: 90, // Expiration in days refreshInSeconds: 8*3600 // User Id cache lifetime in seconds, defaulting to 'expires' }, }, { @@ -43,7 +43,7 @@ pbjs.setConfig({ }, { name: 'identityLink', params: { - pid: '999' // Set your real identityLink placement ID here + pid: '999' // Set your real identityLink placement ID here }, storage: { type: 'cookie', @@ -53,7 +53,7 @@ pbjs.setConfig({ }, { name: 'liveIntentId', params: { - publisherId: '7798696' // Set an identifier of a publisher know to your systems + publisherId: '7798696' // Set an identifier of a publisher know to your systems }, storage: { type: 'cookie', @@ -70,6 +70,13 @@ pbjs.setConfig({ name: 'sharedid', expires: 28 } + }, { + name: 'criteo', + storage: { // It is best not to specify this parameter since the module needs to be called as many times as possible + type: 'cookie', + name: '_criteoId', + expires: 1 + } }], syncDelay: 5000, auctionDelay: 1000 @@ -102,7 +109,7 @@ pbjs.setConfig({ }, { name: 'identityLink', params: { - pid: '999' // Set your real identityLink placement ID here + pid: '999' // Set your real identityLink placement ID here }, storage: { type: 'html5', @@ -110,25 +117,44 @@ pbjs.setConfig({ expires: 30 } }, { - name: 'liveIntentId', - params: { - publisherId: '7798696' // Set an identifier of a publisher know to your systems - }, - storage: { - type: 'html5', - name: '_li_pbid', - expires: 60 - } + name: 'liveIntentId', + params: { + publisherId: '7798696' // Set an identifier of a publisher know to your systems + }, + storage: { + type: 'html5', + name: '_li_pbid', + expires: 60 + } }, { - name: 'sharedId', + name: 'sharedId', params: { syncTime: 60 // in seconds, default is 24 hours }, storage: { - type: 'cookie', + type: 'html5', name: 'sharedid', expires: 28 } + }, { + name: 'id5Id', + params: { + partner: 173, // Set your real ID5 partner ID here for production, please ask for one at https://id5.io/universal-id + pd: 'some-pd-string' // See https://wiki.id5.io/x/BIAZ for details + }, + storage: { + type: 'html5', + name: 'id5id', + expires: 90, // Expiration in days + refreshInSeconds: 8*3600 // User Id cache lifetime in seconds, defaulting to 'expires' + }, + }, { + name: 'criteo', + storage: { // It is best not to specify this parameter since the module needs to be called as many times as possible + type: 'html5', + name: '_criteoId', + expires: 1 + } }], syncDelay: 5000 } @@ -152,6 +178,10 @@ pbjs.setConfig({ { name: "netId", value: { "netId": "fH5A3n2O8_CZZyPoJVD-eabc6ECb7jhxCicsds7qSg" } + }, + { + name: "criteo", + value: { "criteoId": "wK-fkF8zaEIlMkZMbHl3eFo4NEtoNmZaeXJtYkFjZlVuWjBhcjJMaTRYd3pZNSUyQnlKRHNGRXlpdzdjd3pjVzhjcSUyQmY4eTFzN3VSZjV1ZyUyRlA0U2ZiR0UwN2I4bDZRJTNEJTNE" } }], syncDelay: 5000 } diff --git a/modules/userIdTargeting.js b/modules/userIdTargeting.js index 3ed8b2a14b5..e15c9ddaca2 100644 --- a/modules/userIdTargeting.js +++ b/modules/userIdTargeting.js @@ -20,14 +20,16 @@ export function userIdTargeting(userIds, config) { if (!SHARE_WITH_GAM) { logInfo(MODULE_NAME + ': Not enabled for ' + GAM); - } - - if (window.googletag && isFn(window.googletag.pubads) && hasOwn(window.googletag.pubads(), 'setTargeting') && isFn(window.googletag.pubads().setTargeting)) { + } else if (window.googletag && isFn(window.googletag.pubads) && hasOwn(window.googletag.pubads(), 'setTargeting') && isFn(window.googletag.pubads().setTargeting)) { GAM_API = window.googletag.pubads().setTargeting; } else { - SHARE_WITH_GAM = false; - logInfo(MODULE_NAME + ': Could not find googletag.pubads().setTargeting API. Not adding User Ids in targeting.') - return; + window.googletag = window.googletag || {}; + window.googletag.cmd = window.googletag.cmd || []; + GAM_API = function (key, value) { + window.googletag.cmd.push(function () { + window.googletag.pubads().setTargeting(key, value); + }); + }; } Object.keys(userIds).forEach(function(key) { diff --git a/modules/vdoaiBidAdapter.js b/modules/vdoaiBidAdapter.js index 395953fb737..d4fda36b1fc 100644 --- a/modules/vdoaiBidAdapter.js +++ b/modules/vdoaiBidAdapter.js @@ -1,14 +1,14 @@ import * as utils from '../src/utils.js'; import {config} from '../src/config.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; -import {BANNER} from '../src/mediaTypes.js'; +import {BANNER, VIDEO} from '../src/mediaTypes.js'; const BIDDER_CODE = 'vdo.ai'; const ENDPOINT_URL = 'https://prebid.vdo.ai/auction'; export const spec = { code: BIDDER_CODE, - supportedMediaTypes: [BANNER], + supportedMediaTypes: [BANNER, VIDEO], /** * Determines whether or not the given bid request is valid. * @@ -30,17 +30,16 @@ export const spec = { if (validBidRequests.length === 0) { return []; } + return validBidRequests.map(bidRequest => { - const sizes = utils.parseSizesInput(bidRequest.params.size || bidRequest.sizes)[0]; - const width = sizes.split('x')[0]; - const height = sizes.split('x')[1]; + const sizes = utils.getAdUnitSizes(bidRequest); const payload = { placementId: bidRequest.params.placementId, - width: width, - height: height, + sizes: sizes, bidId: bidRequest.bidId, referer: bidderRequest.refererInfo.referer, - id: bidRequest.auctionId + id: bidRequest.auctionId, + mediaType: bidRequest.mediaTypes.video ? 'video' : 'banner' }; bidRequest.params.bidFloor && (payload['bidFloor'] = bidRequest.params.bidFloor); return { @@ -63,9 +62,9 @@ export const spec = { const response = serverResponse.body; const creativeId = response.adid || 0; // const width = response.w || 0; - const width = bidRequest.data.width; + const width = response.width; // const height = response.h || 0; - const height = bidRequest.data.height; + const height = response.height; const cpm = response.price || 0; response.rWidth = width; @@ -90,8 +89,15 @@ export const spec = { ttl: config.getConfig('_bidderTimeout'), // referrer: referrer, // ad: response.adm - ad: adCreative + // ad: adCreative, + mediaType: response.mediaType }; + + if (response.mediaType == 'video') { + bidResponse.vastXml = adCreative; + } else { + bidResponse.ad = adCreative; + } bidResponses.push(bidResponse); } return bidResponses; diff --git a/modules/vdoaiBidAdapter.md b/modules/vdoaiBidAdapter.md index 81bd8e69c1d..712adc0ec76 100644 --- a/modules/vdoaiBidAdapter.md +++ b/modules/vdoaiBidAdapter.md @@ -31,4 +31,27 @@ Module that connects to VDO.AI's demand sources ] } ]; +``` + + +# Video Test Parameters +``` +var videoAdUnit = { + code: 'test-div', + sizes: [[640, 480]], + mediaTypes: { + video: { + playerSize: [[640, 480]], + context: 'instream' + }, + }, + bids: [ + { + bidder: "vdo.ai", + params: { + placement: 'newsdv77' + } + } + ] +}; ``` \ No newline at end of file diff --git a/modules/verizonMediaIdSystem.js b/modules/verizonMediaIdSystem.js new file mode 100644 index 00000000000..f39909f6666 --- /dev/null +++ b/modules/verizonMediaIdSystem.js @@ -0,0 +1,104 @@ +/** + * This module adds verizonMediaId to the User ID module + * The {@link module:modules/userId} module is required + * @module modules/verizonMediaIdSystem + * @requires module:modules/userId + */ + +import {ajax} from '../src/ajax.js'; +import {submodule} from '../src/hook.js'; +import * as utils from '../src/utils.js'; + +const MODULE_NAME = 'verizonMediaId'; +const VENDOR_ID = 25; +const PLACEHOLDER = '__PIXEL_ID__'; +const VMCID_ENDPOINT = `https://ups.analytics.yahoo.com/ups/${PLACEHOLDER}/fed`; + +function isEUConsentRequired(consentData) { + return !!(consentData && consentData.gdpr && consentData.gdpr.gdprApplies); +} + +/** @type {Submodule} */ +export const verizonMediaIdSubmodule = { + /** + * used to link submodule with config + * @type {string} + */ + name: MODULE_NAME, + /** + * Vendor id of Verizon Media EMEA Limited + * @type {Number} + */ + gvlid: VENDOR_ID, + /** + * decode the stored id value for passing to bid requests + * @function + * @returns {{connectid: string} | undefined} + */ + decode(value) { + return (typeof value === 'object' && (value.connectid || value.vmuid)) + ? {connectid: value.connectid || value.vmuid} : undefined; + }, + /** + * Gets the Verizon Media Connect ID + * @function + * @param {SubmoduleConfig} [config] + * @param {ConsentData} [consentData] + * @returns {IdResponse|undefined} + */ + getId(config, consentData) { + const params = config.params || {}; + if (!params || typeof params.he !== 'string' || + (typeof params.pixelId === 'undefined' && typeof params.endpoint === 'undefined')) { + utils.logError('The verizonMediaId submodule requires the \'he\' and \'pixelId\' parameters to be defined.'); + return; + } + + const data = { + '1p': [1, '1', true].includes(params['1p']) ? '1' : '0', + he: params.he, + gdpr: isEUConsentRequired(consentData) ? '1' : '0', + euconsent: isEUConsentRequired(consentData) ? consentData.gdpr.consentString : '', + us_privacy: consentData && consentData.uspConsent ? consentData.uspConsent : '' + }; + + if (params.pixelId) { + data.pixelId = params.pixelId + } + + const resp = function (callback) { + const callbacks = { + success: response => { + let responseObj; + if (response) { + try { + responseObj = JSON.parse(response); + } catch (error) { + utils.logError(error); + } + } + callback(responseObj); + }, + error: error => { + utils.logError(`${MODULE_NAME}: ID fetch encountered an error`, error); + callback(); + } + }; + const endpoint = VMCID_ENDPOINT.replace(PLACEHOLDER, params.pixelId); + let url = `${params.endpoint || endpoint}?${utils.formatQS(data)}`; + verizonMediaIdSubmodule.getAjaxFn()(url, callbacks, null, {method: 'GET', withCredentials: true}); + }; + return {callback: resp}; + }, + + /** + * Return the function used to perform XHR calls. + * Utilised for each of testing. + * @returns {Function} + */ + getAjaxFn() { + return ajax; + } +}; + +submodule('userId', verizonMediaIdSubmodule); diff --git a/modules/verizonMediaSystemId.md b/modules/verizonMediaSystemId.md new file mode 100644 index 00000000000..c0d315dc754 --- /dev/null +++ b/modules/verizonMediaSystemId.md @@ -0,0 +1,33 @@ +## Verizon Media User ID Submodule + +Verizon Media User ID Module. + +### Prebid Params + +``` +pbjs.setConfig({ + userSync: { + userIds: [{ + name: 'verizonMediaId', + storage: { + name: 'vmcid', + type: 'html5', + expires: 15 + }, + params: { + pixelId: 58776, + he: '0bef996248d63cea1529cb86de31e9547a712d9f380146e98bbd39beec70355a' + } + }] + } +}); +``` +## Parameter Descriptions for the `usersync` Configuration Section +The below parameters apply only to the Verizon Media User ID Module integration. + +| Param under usersync.userIds[] | Scope | Type | Description | Example | +| --- | --- | --- | --- | --- | +| name | Required | String | ID value for the Verizon Media module - `"verizonMediaId"` | `"verizonMediaId"` | +| params | Required | Object | Data for Verizon Media ID initialization. | | +| params.pixelId | Required | Number | The Verizon Media supplied publisher specific pixel Id | `8976` | +| params.he | Required | String | The SHA-256 hashed user email address | `"529cb86de31e9547a712d9f380146e98bbd39beec"` | diff --git a/modules/vidazooBidAdapter.js b/modules/vidazooBidAdapter.js index 0718f22d0d2..7fc6e3a5395 100644 --- a/modules/vidazooBidAdapter.js +++ b/modules/vidazooBidAdapter.js @@ -3,7 +3,7 @@ import { registerBidder } from '../src/adapters/bidderFactory.js'; import { BANNER } from '../src/mediaTypes.js'; import { getStorageManager } from '../src/storageManager.js'; -const GLVID = 744; +const GVLID = 744; const DEFAULT_SUB_DOMAIN = 'prebid'; const BIDDER_CODE = 'vidazoo'; const BIDDER_VERSION = '1.0.0'; @@ -24,7 +24,7 @@ export const SUPPORTED_ID_SYSTEMS = { 'pubcid': 1, 'tdid': 1, }; -const storage = getStorageManager(GLVID); +const storage = getStorageManager(GVLID); export function createDomain(subDomain = DEFAULT_SUB_DOMAIN) { return `https://${subDomain}.cootlogix.com`; @@ -117,6 +117,9 @@ function appendUserIdsToRequestPayload(payloadRef, userIds) { case 'parrableId': payloadRef[key] = userId.eid; break; + case 'id5id': + payloadRef[key] = userId.uid; + break; default: payloadRef[key] = userId; } @@ -263,6 +266,7 @@ export function tryParseJSON(value) { export const spec = { code: BIDDER_CODE, + gvlid: GVLID, version: BIDDER_VERSION, supportedMediaTypes: [BANNER], isBidRequestValid, diff --git a/modules/visxBidAdapter.js b/modules/visxBidAdapter.js index 511e658c947..725482d07c3 100644 --- a/modules/visxBidAdapter.js +++ b/modules/visxBidAdapter.js @@ -100,8 +100,8 @@ export const spec = { if (payloadUserId.tdid) { payload.tdid = payloadUserId.tdid; } - if (payloadUserId.id5id) { - payload.id5 = payloadUserId.id5id; + if (payloadUserId.id5id && payloadUserId.id5id.uid) { + payload.id5 = payloadUserId.id5id.uid; } if (payloadUserId.digitrustid && payloadUserId.digitrustid.data && payloadUserId.digitrustid.data.id) { payload.dtid = payloadUserId.digitrustid.data.id; diff --git a/modules/vuukleBidAdapter.js b/modules/vuukleBidAdapter.js new file mode 100644 index 00000000000..e9770b5e62e --- /dev/null +++ b/modules/vuukleBidAdapter.js @@ -0,0 +1,63 @@ +import * as utils from '../src/utils.js'; +import { registerBidder } from '../src/adapters/bidderFactory.js'; + +const BIDDER_CODE = 'vuukle'; +const URL = 'https://pb.vuukle.com/adapter'; +const TIME_TO_LIVE = 360; + +export const spec = { + code: BIDDER_CODE, + + isBidRequestValid: function(bid) { + return true + }, + + buildRequests: function(bidRequests) { + const requests = bidRequests.map(function (bid) { + const parseSized = utils.parseSizesInput(bid.sizes); + const arrSize = parseSized[0].split('x'); + const params = { + url: encodeURIComponent(window.location.href), + sizes: JSON.stringify(parseSized), + width: arrSize[0], + height: arrSize[1], + params: JSON.stringify(bid.params), + rnd: Math.random(), + bidId: bid.bidId, + source: 'pbjs', + version: '$prebid.version$', + v: 1, + }; + + return { + method: 'GET', + url: URL, + data: params, + options: {withCredentials: false} + } + }); + + return requests; + }, + + interpretResponse: function(serverResponse, bidRequest) { + if (!serverResponse || !serverResponse.body || !serverResponse.body.ad) { + return []; + } + + const res = serverResponse.body; + const bidResponse = { + requestId: bidRequest.data.bidId, + cpm: res.cpm, + width: res.width, + height: res.height, + creativeId: res.creative_id, + currency: res.currency || 'USD', + netRevenue: true, + ttl: TIME_TO_LIVE, + ad: res.ad + }; + return [bidResponse]; + }, +} +registerBidder(spec); diff --git a/modules/vuukleBidAdapter.md b/modules/vuukleBidAdapter.md new file mode 100644 index 00000000000..ee7b54c6262 --- /dev/null +++ b/modules/vuukleBidAdapter.md @@ -0,0 +1,26 @@ +# Overview +``` +Module Name: Vuukle Bid Adapter +Module Type: Bidder Adapter +Maintainer: support@vuukle.com +``` + +# Description +Module that connects to Vuukle's server for bids. +Currently module supports only banner mediaType. + +# Test Parameters +``` + var adUnits = [{ + code: '/test/div', + mediaTypes: { + banner: { + sizes: [[300, 250]] + } + }, + bids: [{ + bidder: 'vuukle', + params: {} + }] + }]; +``` diff --git a/modules/yieldlabBidAdapter.js b/modules/yieldlabBidAdapter.js index b252c0db2ee..5465a10a884 100644 --- a/modules/yieldlabBidAdapter.js +++ b/modules/yieldlabBidAdapter.js @@ -240,7 +240,7 @@ function createTargetingString (obj) { */ function createSchainString (schain) { const ver = schain.ver || '' - const complete = schain.complete || '' + const complete = (schain.complete === 1 || schain.complete === 0) ? schain.complete : '' const keys = ['asi', 'sid', 'hp', 'rid', 'name', 'domain', 'ext'] const nodesString = schain.nodes.reduce((acc, node) => { return acc += `!${keys.map(key => node[key] ? encodeURIComponentWithBangIncluded(node[key]) : '').join(',')}` diff --git a/modules/yieldmoBidAdapter.js b/modules/yieldmoBidAdapter.js index 08dc3189eda..05af0bf0d66 100644 --- a/modules/yieldmoBidAdapter.js +++ b/modules/yieldmoBidAdapter.js @@ -1,103 +1,133 @@ import * as utils from '../src/utils.js'; +import { BANNER, VIDEO } from '../src/mediaTypes.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; +import includes from 'core-js-pure/features/array/includes'; const BIDDER_CODE = 'yieldmo'; const CURRENCY = 'USD'; const TIME_TO_LIVE = 300; const NET_REVENUE = true; -const SERVER_ENDPOINT = 'https://ads.yieldmo.com/exchange/prebid'; +const BANNER_SERVER_ENDPOINT = 'https://ads.yieldmo.com/exchange/prebid'; +const VIDEO_SERVER_ENDPOINT = 'https://ads.yieldmo.com/exchange/prebidvideo'; +const OPENRTB_VIDEO_BIDPARAMS = ['placement', 'startdelay', 'skipafter', + 'protocols', 'api', 'playbackmethod', 'maxduration', 'minduration', 'pos']; +const OPENRTB_VIDEO_SITEPARAMS = ['name', 'domain', 'cat', 'keywords']; const localWindow = utils.getWindowTop(); export const spec = { code: BIDDER_CODE, - supportedMediaTypes: ['banner'], + supportedMediaTypes: [BANNER, VIDEO], + /** * Determines whether or not the given bid request is valid. * @param {object} bid, bid to validate * @return boolean, true if valid, otherwise false */ isBidRequestValid: function (bid) { - return !!(bid && bid.adUnitCode && bid.bidId); + return !!(bid && bid.adUnitCode && bid.bidId && (hasBannerMediaType(bid) || hasVideoMediaType(bid)) && + validateVideoParams(bid)); }, + /** * Make a server request from the list of BidRequests. * * @param {BidRequest[]} bidRequests A non-empty list of bid requests which should be sent to the Server. + * @param {BidderRequest} bidderRequest bidder request object. * @return ServerRequest Info describing the request to the server. */ buildRequests: function (bidRequests, bidderRequest) { - let serverRequest = { - pbav: '$prebid.version$', - p: [], - page_url: bidderRequest.refererInfo.referer, - bust: new Date().getTime().toString(), - pr: bidderRequest.refererInfo.referer, - scrd: localWindow.devicePixelRatio || 0, - dnt: getDNT(), - description: getPageDescription(), - title: localWindow.document.title || '', - w: localWindow.innerWidth, - h: localWindow.innerHeight, - userConsent: JSON.stringify({ - // case of undefined, stringify will remove param - gdprApplies: utils.deepAccess(bidderRequest, 'gdprConsent.gdprApplies') || '', - cmp: utils.deepAccess(bidderRequest, 'gdprConsent.consentString') || '' - }), - us_privacy: utils.deepAccess(bidderRequest, 'uspConsent') || '' - }; - - const mtp = window.navigator.maxTouchPoints; - if (mtp) { - serverRequest.mtp = mtp; - } + const bannerBidRequests = bidRequests.filter(request => hasBannerMediaType(request)); + const videoBidRequests = bidRequests.filter(request => hasVideoMediaType(request)); + + let serverRequests = []; + if (bannerBidRequests.length > 0) { + let serverRequest = { + pbav: '$prebid.version$', + p: [], + page_url: bidderRequest.refererInfo.referer, + bust: new Date().getTime().toString(), + pr: bidderRequest.refererInfo.referer, + scrd: localWindow.devicePixelRatio || 0, + dnt: getDNT(), + description: getPageDescription(), + title: localWindow.document.title || '', + w: localWindow.innerWidth, + h: localWindow.innerHeight, + userConsent: JSON.stringify({ + // case of undefined, stringify will remove param + gdprApplies: utils.deepAccess(bidderRequest, 'gdprConsent.gdprApplies') || '', + cmp: utils.deepAccess(bidderRequest, 'gdprConsent.consentString') || '' + }), + us_privacy: utils.deepAccess(bidderRequest, 'uspConsent') || '' + }; - bidRequests.forEach(request => { - serverRequest.p.push(addPlacement(request)); - const pubcid = getId(request, 'pubcid'); - if (pubcid) { - serverRequest.pubcid = pubcid; - } else if (request.crumbs) { - if (request.crumbs.pubcid) { + const mtp = window.navigator.maxTouchPoints; + if (mtp) { + serverRequest.mtp = mtp; + } + + bannerBidRequests.forEach(request => { + serverRequest.p.push(addPlacement(request)); + const pubcid = getId(request, 'pubcid'); + if (pubcid) { + serverRequest.pubcid = pubcid; + } else if (request.crumbs && request.crumbs.pubcid) { serverRequest.pubcid = request.crumbs.pubcid; } - } - const tdid = getId(request, 'tdid'); - if (tdid) { - serverRequest.tdid = tdid; - } - const criteoId = getId(request, 'criteoId'); - if (criteoId) { - serverRequest.cri_prebid = criteoId; - } - if (request.schain) { - serverRequest.schain = - JSON.stringify(request.schain); - } - }); - serverRequest.p = '[' + serverRequest.p.toString() + ']'; - return { - method: 'GET', - url: SERVER_ENDPOINT, - data: serverRequest - }; + const tdid = getId(request, 'tdid'); + if (tdid) { + serverRequest.tdid = tdid; + } + const criteoId = getId(request, 'criteoId'); + if (criteoId) { + serverRequest.cri_prebid = criteoId; + } + if (request.schain) { + serverRequest.schain = JSON.stringify(request.schain); + } + }); + serverRequest.p = '[' + serverRequest.p.toString() + ']'; + serverRequests.push({ + method: 'GET', + url: BANNER_SERVER_ENDPOINT, + data: serverRequest + }); + } + + if (videoBidRequests.length > 0) { + const serverRequest = openRtbRequest(videoBidRequests, bidderRequest); + serverRequests.push({ + method: 'POST', + url: VIDEO_SERVER_ENDPOINT, + data: serverRequest + }); + } + return serverRequests; }, + /** * Makes Yieldmo Ad Server response compatible to Prebid specs - * @param serverResponse successful response from Ad Server + * @param {ServerResponse} serverResponse successful response from Ad Server + * @param {ServerRequest} bidRequest * @return {Bid[]} an array of bids */ - interpretResponse: function (serverResponse) { + interpretResponse: function (serverResponse, bidRequest) { let bids = []; - let data = serverResponse.body; + const data = serverResponse.body; if (data.length > 0) { data.forEach(response => { - if (response.cpm && response.cpm > 0) { - bids.push(createNewBid(response)); + if (response.cpm > 0) { + bids.push(createNewBannerBid(response)); } }); } + if (data.seatbid) { + const seatbids = data.seatbid.reduce((acc, seatBid) => acc.concat(seatBid.bid), []); + seatbids.forEach(bid => bids.push(createNewVideoBid(bid, bidRequest))); + } return bids; }, + getUserSyncs: function () { return []; } @@ -108,6 +138,20 @@ registerBidder(spec); * Helper Functions ***************************************/ +/** + * @param {BidRequest} bidRequest bid request + */ +function hasBannerMediaType(bidRequest) { + return !!utils.deepAccess(bidRequest, 'mediaTypes.banner'); +} + +/** + * @param {BidRequest} bidRequest bid request + */ +function hasVideoMediaType(bidRequest) { + return !!utils.deepAccess(bidRequest, 'mediaTypes.video'); +} + /** * Adds placement information to array * @param request bid request @@ -130,10 +174,10 @@ function addPlacement(request) { } /** - * creates a new bid with response information + * creates a new banner bid with response information * @param response server response */ -function createNewBid(response) { +function createNewBannerBid(response) { return { requestId: response['callback_id'], cpm: response.cpm, @@ -147,6 +191,27 @@ function createNewBid(response) { }; } +/** + * creates a new video bid with response information + * @param response openRTB server response + * @param bidRequest server request + */ +function createNewVideoBid(response, bidRequest) { + const imp = (utils.deepAccess(bidRequest, 'data.imp') || []).find(imp => imp.id === response.impid); + return { + requestId: imp.id, + cpm: response.price, + width: imp.video.w, + height: imp.video.h, + creativeId: response.crid || response.adid, + currency: CURRENCY, + netRevenue: NET_REVENUE, + mediaType: VIDEO, + ttl: TIME_TO_LIVE, + vastXml: response.adm + }; +} + /** * Detects whether dnt is true * @returns true if user enabled dnt @@ -179,3 +244,215 @@ function getPageDescription() { function getId(request, idType) { return (typeof utils.deepAccess(request, 'userId') === 'object') ? request.userId[idType] : undefined; } + +/** + * @param {BidRequest[]} bidRequests bid request object + * @param {BidderRequest} bidderRequest bidder request object + * @return Object OpenRTB request object + */ +function openRtbRequest(bidRequests, bidderRequest) { + let openRtbRequest = { + id: bidRequests[0].bidderRequestId, + at: 1, + imp: bidRequests.map(bidRequest => openRtbImpression(bidRequest)), + site: openRtbSite(bidRequests[0], bidderRequest), + device: openRtbDevice(), + badv: bidRequests[0].params.badv || [], + bcat: bidRequests[0].params.bcat || [], + ext: { + prebid: '$prebid.version$', + } + }; + + populateOpenRtbGdpr(openRtbRequest, bidderRequest); + + return openRtbRequest; +} + +/** + * @param {BidRequest} bidRequest bidder request object. + * @return Object OpenRTB's 'imp' (impression) object + */ +function openRtbImpression(bidRequest) { + const videoReq = utils.deepAccess(bidRequest, 'mediaTypes.video'); + const size = extractPlayerSize(bidRequest); + const imp = { + id: bidRequest.bidId, + tagid: bidRequest.adUnitCode, + bidfloor: bidRequest.params.bidfloor || 0, + ext: { + placement_id: bidRequest.params.placementId + }, + video: { + w: size[0], + h: size[1], + mimes: videoReq.mimes, + linearity: 1 + } + }; + + const videoParams = utils.deepAccess(bidRequest, 'params.video'); + Object.keys(videoParams) + .filter(param => includes(OPENRTB_VIDEO_BIDPARAMS, param)) + .forEach(param => imp.video[param] = videoParams[param]); + + if (videoParams.skippable) { + imp.video.skip = 1; + } + + return imp; +} + +/** + * @param {BidRequest} bidRequest bidder request object. + * @return [number, number] || null Player's width and height, or undefined otherwise. + */ +function extractPlayerSize(bidRequest) { + const sizeArr = utils.deepAccess(bidRequest, 'mediaTypes.video.playerSize'); + if (utils.isArrayOfNums(sizeArr, 2)) { + return sizeArr; + } else if (utils.isArray(sizeArr) && utils.isArrayOfNums(sizeArr[0], 2)) { + return sizeArr[0]; + } + return null; +} + +/** + * @param {BidRequest} bidRequest bid request object + * @param {BidderRequest} bidderRequest bidder request object + * @return Object OpenRTB's 'site' object + */ +function openRtbSite(bidRequest, bidderRequest) { + let result = {}; + + const loc = utils.parseUrl(utils.deepAccess(bidderRequest, 'refererInfo.referer')); + if (!utils.isEmpty(loc)) { + result.page = `${loc.protocol}://${loc.hostname}${loc.pathname}`; + } + + if (self === top && document.referrer) { + result.ref = document.referrer; + } + + const keywords = document.getElementsByTagName('meta')['keywords']; + if (keywords && keywords.content) { + result.keywords = keywords.content; + } + + const siteParams = utils.deepAccess(bidRequest, 'params.site'); + if (siteParams) { + Object.keys(siteParams) + .filter(param => includes(OPENRTB_VIDEO_SITEPARAMS, param)) + .forEach(param => result[param] = siteParams[param]); + } + return result; +} + +/** + * @return Object OpenRTB's 'device' object + */ +function openRtbDevice() { + return { + ua: navigator.userAgent, + language: (navigator.language || navigator.browserLanguage || navigator.userLanguage || navigator.systemLanguage), + }; +} + +/** + * Updates openRtbRequest with GDPR info from bidderRequest, if present. + * @param {Object} openRtbRequest OpenRTB's request to update. + * @param {BidderRequest} bidderRequest bidder request object. + */ +function populateOpenRtbGdpr(openRtbRequest, bidderRequest) { + const gdpr = bidderRequest.gdprConsent; + if (gdpr && 'gdprApplies' in gdpr) { + utils.deepSetValue(openRtbRequest, 'regs.ext.gdpr', gdpr.gdprApplies ? 1 : 0); + utils.deepSetValue(openRtbRequest, 'user.ext.consent', gdpr.consentString); + } + const uspConsent = utils.deepAccess(bidderRequest, 'uspConsent'); + if (uspConsent) { + utils.deepSetValue(openRtbRequest, 'regs.ext.us_privacy', uspConsent); + } +} + +/** + * Determines whether or not the given video bid request is valid. If it's not a video bid, returns true. + * @param {object} bid, bid to validate + * @return boolean, true if valid, otherwise false + */ +function validateVideoParams(bid) { + if (!hasVideoMediaType(bid)) { + return true; + } + + const paramRequired = (paramStr, value, conditionStr) => { + let error = `"${paramStr}" is required`; + if (conditionStr) { + error += ' when ' + conditionStr; + } + throw new Error(error); + } + + const paramInvalid = (paramStr, value, expectedStr) => { + expectedStr = expectedStr ? ', expected: ' + expectedStr : ''; + value = JSON.stringify(value); + throw new Error(`"${paramStr}"=${value} is invalid${expectedStr}`); + } + + const isDefined = val => typeof val !== 'undefined'; + const validate = (fieldPath, validateCb, errorCb, errorCbParam) => { + const value = utils.deepAccess(bid, fieldPath); + if (!validateCb(value)) { + errorCb(fieldPath, value, errorCbParam); + } + return value; + } + + try { + validate('params.placementId', val => !utils.isEmpty(val), paramRequired); + + validate('mediaTypes.video.playerSize', val => utils.isArrayOfNums(val, 2) || + (utils.isArray(val) && val.every(v => utils.isArrayOfNums(v, 2))), + paramInvalid, 'array of 2 integers, ex: [640,480] or [[640,480]]'); + + validate('mediaTypes.video.mimes', val => isDefined(val), paramRequired); + validate('mediaTypes.video.mimes', val => utils.isArray(val) && val.every(v => utils.isStr(v)), paramInvalid, + 'array of strings, ex: ["video/mp4"]'); + + validate('params.video', val => !utils.isEmpty(val), paramRequired); + + const placement = validate('params.video.placement', val => isDefined(val), paramRequired); + validate('params.video.placement', val => val >= 1 && val <= 5, paramInvalid); + if (placement === 1) { + validate('params.video.startdelay', val => isDefined(val), + (field, v) => paramRequired(field, v, 'placement == 1')); + validate('params.video.startdelay', val => utils.isNumber(val), paramInvalid, 'number, ex: 5'); + } + + validate('params.video.protocols', val => isDefined(val), paramRequired); + validate('params.video.protocols', val => utils.isArrayOfNums(val) && val.every(v => (v >= 1 && v <= 6)), + paramInvalid, 'array of numbers, ex: [2,3]'); + + validate('params.video.api', val => isDefined(val), paramRequired); + validate('params.video.api', val => utils.isArrayOfNums(val) && val.every(v => (v >= 1 && v <= 6)), + paramInvalid, 'array of numbers, ex: [2,3]'); + + validate('params.video.playbackmethod', val => !isDefined(val) || utils.isArrayOfNums(val), paramInvalid, + 'array of integers, ex: [2,6]'); + + validate('params.video.maxduration', val => isDefined(val), paramRequired); + validate('params.video.maxduration', val => utils.isInteger(val), paramInvalid); + validate('params.video.minduration', val => !isDefined(val) || utils.isNumber(val), paramInvalid); + validate('params.video.skippable', val => !isDefined(val) || utils.isBoolean(val), paramInvalid); + validate('params.video.skipafter', val => !isDefined(val) || utils.isNumber(val), paramInvalid); + validate('params.video.pos', val => !isDefined(val) || utils.isNumber(val), paramInvalid); + validate('params.badv', val => !isDefined(val) || utils.isArray(val), paramInvalid, + 'array of strings, ex: ["ford.com","pepsi.com"]'); + validate('params.bcat', val => !isDefined(val) || utils.isArray(val), paramInvalid, + 'array of strings, ex: ["IAB1-5","IAB1-6"]'); + return true; + } catch (e) { + utils.logError(e.message); + return false; + } +} diff --git a/modules/yieldmoBidAdapter.md b/modules/yieldmoBidAdapter.md index 0f86d2507d1..1b8b7b1b741 100644 --- a/modules/yieldmoBidAdapter.md +++ b/modules/yieldmoBidAdapter.md @@ -11,26 +11,61 @@ Note: Our ads will only render in mobile Connects to Yieldmo Ad Server for bids. -Yieldmo bid adapter supports Banner. +Yieldmo bid adapter supports Banner and Video. # Test Parameters + +## Banner + +Sample banner ad unit config: +```javascript +var adUnits = [{ // Banner adUnit + code: 'div-gpt-ad-1460505748561-0', + mediaTypes: { + banner: { + sizes: [[300, 250], [300, 600]], + } + }, + bids: [{ + bidder: 'yieldmo', + params: { + placementId: '1779781193098233305', // string with at most 19 characters (may include numbers only) + bidFloor: .28 // optional param + } + }] +}]; +``` + +## Video + +Sample instream video ad unit config: +```javascript +var adUnits = [{ // Video adUnit + code: 'div-video-ad-1234567890', + mediaTypes: { + video: { + playerSize: [640, 480], // required + context: 'instream', + mimes: ['video/mp4'] // required, array of strings + } + }, + bids: [{ + bidder: 'yieldmo', + params: { + placementId: '1524592390382976659', // required + video: { + placement: 1, // required, integer + maxduration: 30, // required, integer + minduration: 15, // optional, integer + pos: 1, // optional, integer + startdelay: 10, // required if placement == 1 + protocols: [2, 3], // required, array of integers + api: [2, 3], // required, array of integers + playbackmethod: [2,6], // required, array of integers + skippable: true, // optional, boolean + skipafter: 10 // optional, integer + } + } + }] +}]; ``` -var adUnits = [ - // Banner adUnit - { - code: 'div-gpt-ad-1460505748561-0', - mediaTypes: { - banner: { - sizes: [[300, 250], [300,600]], - } - } - bids: [{ - bidder: 'yieldmo', - params: { - placementId: '1779781193098233305', // string with at most 19 characters (may include numbers only) - bidFloor: .28 // optional param - } - }] - } -]; -``` \ No newline at end of file diff --git a/modules/zemantaBidAdapter.js b/modules/zemantaBidAdapter.js new file mode 100644 index 00000000000..aa7a24985e0 --- /dev/null +++ b/modules/zemantaBidAdapter.js @@ -0,0 +1,256 @@ +// jshint esversion: 6, es3: false, node: true +'use strict'; + +import { + registerBidder +} from '../src/adapters/bidderFactory.js'; +import { NATIVE, BANNER } from '../src/mediaTypes.js'; +import * as utils from '../src/utils.js'; +import { ajax } from '../src/ajax.js'; +import { config } from '../src/config.js'; + +const BIDDER_CODE = 'zemanta'; +const CURRENCY = 'USD'; +const NATIVE_ASSET_IDS = { 0: 'title', 2: 'icon', 3: 'image', 5: 'sponsoredBy', 4: 'body', 1: 'cta' }; +const NATIVE_PARAMS = { + title: { id: 0, name: 'title' }, + icon: { id: 2, type: 1, name: 'img' }, + image: { id: 3, type: 3, name: 'img' }, + sponsoredBy: { id: 5, name: 'data', type: 1 }, + body: { id: 4, name: 'data', type: 2 }, + cta: { id: 1, type: 12, name: 'data' } +}; + +export const spec = { + code: BIDDER_CODE, + supportedMediaTypes: [ NATIVE, BANNER ], + isBidRequestValid: (bid) => { + return ( + !!config.getConfig('zemanta.bidderUrl') && + !!utils.deepAccess(bid, 'params.publisher.id') && + !!(bid.nativeParams || bid.sizes) + ); + }, + buildRequests: (validBidRequests, bidderRequest) => { + const page = bidderRequest.refererInfo.referer; + const ua = navigator.userAgent; + const test = setOnAny(validBidRequests, 'params.test'); + const publisher = setOnAny(validBidRequests, 'params.publisher'); + const cur = CURRENCY; + const endpointUrl = config.getConfig('zemanta.bidderUrl'); + const timeout = bidderRequest.timeout; + + const imps = validBidRequests.map((bid, id) => { + bid.netRevenue = 'net'; + const imp = { + id: id + 1 + '' + } + + if (bid.params.tagid) { + imp.tagid = bid.params.tagid + } + + if (bid.nativeParams) { + imp.native = { + request: JSON.stringify({ + assets: getNativeAssets(bid) + }) + } + } else { + imp.banner = { + format: transformSizes(bid.sizes) + } + } + + return imp; + }); + + const request = { + id: bidderRequest.auctionId, + site: { page, publisher }, + device: { ua }, + source: { fd: 1 }, + cur: [cur], + tmax: timeout, + imp: imps + }; + + if (test) { + request.is_debug = !!test; + request.test = 1; + } + + if (utils.deepAccess(bidderRequest, 'gdprConsent.gdprApplies')) { + utils.deepSetValue(request, 'user.ext.consent', bidderRequest.gdprConsent.consentString) + utils.deepSetValue(request, 'regs.ext.gdpr', bidderRequest.gdprConsent.gdprApplies & 1) + } + if (bidderRequest.uspConsent) { + utils.deepSetValue(request, 'regs.ext.us_privacy', bidderRequest.uspConsent) + } + if (config.getConfig('coppa') === true) { + utils.deepSetValue(request, 'regs.coppa', config.getConfig('coppa') & 1) + } + + return { + method: 'POST', + url: endpointUrl, + data: JSON.stringify(request), + bids: validBidRequests + }; + }, + interpretResponse: (serverResponse, { bids }) => { + if (!serverResponse.body) { + return []; + } + const { seatbid, cur } = serverResponse.body; + + const bidResponses = flatten(seatbid.map(seat => seat.bid)).reduce((result, bid) => { + result[bid.impid - 1] = bid; + return result; + }, []); + + return bids.map((bid, id) => { + const bidResponse = bidResponses[id]; + if (bidResponse) { + const type = bid.nativeParams ? NATIVE : BANNER; + const bidObject = { + requestId: bid.bidId, + cpm: bidResponse.price, + creativeId: bidResponse.crid, + ttl: 360, + netRevenue: bid.netRevenue === 'net', + currency: cur, + mediaType: type, + nurl: bidResponse.nurl, + }; + if (type === NATIVE) { + bidObject.native = parseNative(bidResponse); + } else { + bidObject.ad = bidResponse.adm; + bidObject.width = bidResponse.w; + bidObject.height = bidResponse.h; + } + return bidObject; + } + }).filter(Boolean); + }, + getUserSyncs: (syncOptions) => { + const syncs = []; + const syncUrl = config.getConfig('zemanta.usersyncUrl'); + if (syncOptions.pixelEnabled && syncUrl) { + syncs.push({ + type: 'image', + url: syncUrl + }); + } + return syncs; + }, + onBidWon: (bid) => { + ajax(utils.replaceAuctionPrice(bid.nurl, bid.originalCpm)) + } +}; + +registerBidder(spec); + +function parseNative(bid) { + const { assets, link, eventtrackers } = JSON.parse(bid.adm); + const result = { + clickUrl: link.url, + clickTrackers: link.clicktrackers || undefined + }; + assets.forEach(asset => { + const kind = NATIVE_ASSET_IDS[asset.id]; + const content = kind && asset[NATIVE_PARAMS[kind].name]; + if (content) { + result[kind] = content.text || content.value || { url: content.url, width: content.w, height: content.h }; + } + }); + if (eventtrackers) { + result.impressionTrackers = []; + eventtrackers.forEach(tracker => { + if (tracker.event !== 1) return; + switch (tracker.method) { + case 1: // img + result.impressionTrackers.push(tracker.url); + break; + case 2: // js + result.javascriptTrackers = ``; + break; + } + }); + } + return result; +} + +function setOnAny(collection, key) { + for (let i = 0, result; i < collection.length; i++) { + result = utils.deepAccess(collection[i], key); + if (result) { + return result; + } + } +} + +function flatten(arr) { + return [].concat(...arr); +} + +function getNativeAssets(bid) { + return utils._map(bid.nativeParams, (bidParams, key) => { + const props = NATIVE_PARAMS[key]; + const asset = { + required: bidParams.required & 1, + }; + if (props) { + asset.id = props.id; + let wmin, hmin, w, h; + let aRatios = bidParams.aspect_ratios; + + if (aRatios && aRatios[0]) { + aRatios = aRatios[0]; + wmin = aRatios.min_width || 0; + hmin = aRatios.ratio_height * wmin / aRatios.ratio_width | 0; + } + + if (bidParams.sizes) { + const sizes = flatten(bidParams.sizes); + w = sizes[0]; + h = sizes[1]; + } + + asset[props.name] = { + len: bidParams.len, + type: props.type, + wmin, + hmin, + w, + h + }; + + return asset; + } + }).filter(Boolean); +} + +/* Turn bid request sizes into ut-compatible format */ +function transformSizes(requestSizes) { + if (!utils.isArray(requestSizes)) { + return []; + } + + if (requestSizes.length === 2 && !utils.isArray(requestSizes[0])) { + return [{ + w: parseInt(requestSizes[0], 10), + h: parseInt(requestSizes[1], 10) + }]; + } else if (utils.isArray(requestSizes[0])) { + return requestSizes.map(item => + ({ + w: parseInt(item[0], 10), + h: parseInt(item[1], 10) + }) + ); + } + + return []; +} diff --git a/modules/zemantaBidAdapter.md b/modules/zemantaBidAdapter.md new file mode 100644 index 00000000000..d991b67d429 --- /dev/null +++ b/modules/zemantaBidAdapter.md @@ -0,0 +1,107 @@ +# Overview + +``` +Module Name: Zemanta Adapter +Module Type: Bidder Adapter +Maintainer: prog-ops-team@outbrain.com +``` + +# Description + +Module that connects to zemanta bidder to fetch bids. +Both native and display formats are supported but not at the same time. Using OpenRTB standard. + +# Configuration + +## Bidder and usersync URLs + +The Zemanta adapter does not work without setting the correct bidder and usersync URLs. +You will receive the URLs when contacting us. + +``` +pbjs.setConfig({ + zemanta: { + bidderUrl: 'https://bidder-url.com', + usersyncUrl: 'https://usersync-url.com' + } +}); +``` + + +# Test Native Parameters +``` + var adUnits = [ + code: '/19968336/prebid_native_example_1', + mediaTypes: { + native: { + image: { + required: false, + sizes: [100, 50] + }, + title: { + required: false, + len: 140 + }, + sponsoredBy: { + required: false + }, + clickUrl: { + required: false + }, + body: { + required: false + }, + icon: { + required: false, + sizes: [50, 50] + } + } + }, + bids: [{ + bidder: 'zemanta', + params: { + publisher: { + id: '2706', // required + name: 'Publishers Name', + domain: 'publisher.com' + }, + tagid: 'tag-id' + } + }] + ]; + + pbjs.setConfig({ + zemanta: { + bidderUrl: 'https://prebidtest.zemanta.com/api/bidder/prebidtest/bid/' + } + }); +``` + +# Test Display Parameters +``` + var adUnits = [ + code: '/19968336/prebid_display_example_1', + mediaTypes: { + banner: { + sizes: [[300, 250]] + } + }, + bids: [{ + bidder: 'zemanta', + params: { + publisher: { + id: '2706', // required + name: 'Publishers Name', + domain: 'publisher.com' + }, + tagid: 'tag-id' + }, + }] + ]; + + pbjs.setConfig({ + zemanta: { + bidderUrl: 'https://prebidtest.zemanta.com/api/bidder/prebidtest/bid/' + } + }); +``` diff --git a/modules/zeotapIdPlusIdSystem.js b/modules/zeotapIdPlusIdSystem.js index ea1173cd61e..d800286b00e 100644 --- a/modules/zeotapIdPlusIdSystem.js +++ b/modules/zeotapIdPlusIdSystem.js @@ -41,7 +41,7 @@ export const zeotapIdPlusSubmodule = { /** * performs action to obtain id and return a value in the callback's response argument * @function - * @param {SubmoduleParams} configParams + * @param {SubmoduleConfig} config * @return {{id: string | undefined} | undefined} */ getId() { diff --git a/modules/zetaBidAdapter.js b/modules/zetaBidAdapter.js new file mode 100644 index 00000000000..f60e8946799 --- /dev/null +++ b/modules/zetaBidAdapter.js @@ -0,0 +1,161 @@ +import * as utils from '../src/utils.js'; +import { registerBidder } from '../src/adapters/bidderFactory.js'; +import {BANNER} from '../src/mediaTypes.js'; +const BIDDER_CODE = 'zeta_global'; +const ENDPOINT_URL = 'https://prebid.rfihub.com/prebid'; +const USER_SYNC_URL = 'https://p.rfihub.com/cm?pub=42770&in=1'; +const DEFAULT_CUR = 'USD'; +const TTL = 200; +const NET_REV = true; + +export const spec = { + code: BIDDER_CODE, + supportedMediaTypes: [BANNER], + + /** + * Determines whether or not the given bid request is valid. + * + * @param {BidRequest} bid The bid params to validate. + * @return boolean True if this is a valid bid, and false otherwise. + */ + isBidRequestValid: function(bid) { + // check for all required bid fields + let isValid = !!( + bid && + bid.bidId && + bid.params && + bid.params.ip && + bid.params.user && + bid.params.user.buyeruid && + bid.params.definerId + ); + if (!isValid) { + utils.logWarn('Invalid bid request'); + } + return isValid; + }, + + /** + * Make a server request from the list of BidRequests. + * + * @param {Bids[]} validBidRequests - an array of bidRequest objects + * @param {BidderRequest} bidderRequest - master bidRequest object + * @return ServerRequest Info describing the request to the server. + */ + buildRequests: function(validBidRequests, bidderRequest) { + const secure = 1; // treat all requests as secure + const request = validBidRequests[0]; + const params = request.params; + let impData = { + id: request.bidId, + secure: secure, + banner: buildBanner(request) + }; + let isMobile = /(ios|ipod|ipad|iphone|android)/i.test(navigator.userAgent) ? 1 : 0; + let payload = { + id: bidderRequest.auctionId, + cur: [DEFAULT_CUR], + imp: [impData], + site: { + mobile: isMobile, + page: bidderRequest.refererInfo.referer + }, + device: { + ua: navigator.userAgent, + ip: params.ip + }, + user: { + buyeruid: params.user.buyeruid, + uid: params.user.uid + }, + ext: { + definerId: params.definerId + } + }; + if (params.test) { + payload.test = params.test; + } + if (request.gdprConsent) { + payload.regs = { + ext: { + gdpr: request.gdprConsent.gdprApplies === true ? 1 : 0 + } + }; + } + if (request.gdprConsent && request.gdprConsent.gdprApplies) { + payload.user = { + ext: { + consent: request.gdprConsent.consentString + } + }; + } + return { + method: 'POST', + url: ENDPOINT_URL, + data: JSON.stringify(payload), + }; + }, + + /** + * Unpack the response from the server into a list of bids. + * + * @param {ServerResponse} serverResponse A successful response from the server. + * @param bidRequest The payload from the server's response. + * @return {Bid[]} An array of bids which were nested inside the server. + */ + interpretResponse: function(serverResponse, bidRequest) { + let bidResponse = []; + if (Object.keys(serverResponse.body).length !== 0) { + let zetaResponse = serverResponse.body; + let zetaBid = zetaResponse.seatbid[0].bid[0]; + let bid = { + requestId: zetaBid.impid, + cpm: zetaBid.price, + currency: zetaResponse.cur, + width: zetaBid.w, + height: zetaBid.h, + ad: zetaBid.adm, + ttl: TTL, + creativeId: zetaBid.crid, + netRevenue: NET_REV + }; + bidResponse.push(bid); + } + return bidResponse; + }, + + /** + * Register the user sync pixels which should be dropped after the auction. + * + * @param {SyncOptions} syncOptions Which user syncs are allowed? + * @param {ServerResponse[]} serverResponses List of server's responses. + * @param gdprConsent The GDPR consent parameters + * @param uspConsent The USP consent parameters + * @return {UserSync[]} The user syncs which should be dropped. + */ + getUserSyncs: function(syncOptions, serverResponses, gdprConsent, uspConsent) { + const syncs = []; + if (syncOptions.iframeEnabled) { + syncs.push({ + type: 'iframe', + url: USER_SYNC_URL + }); + } + return syncs; + } +} + +function buildBanner(request) { + let sizes = request.sizes; + if (request.mediaTypes && + request.mediaTypes.banner && + request.mediaTypes.banner.sizes) { + sizes = request.mediaTypes.banner.sizes; + } + return { + w: sizes[0][0], + h: sizes[0][1] + }; +} + +registerBidder(spec); diff --git a/modules/zetaBidAdapter.md b/modules/zetaBidAdapter.md new file mode 100644 index 00000000000..ce19b831d4d --- /dev/null +++ b/modules/zetaBidAdapter.md @@ -0,0 +1,40 @@ +# Overview + +``` +Module Name: Zeta Bidder Adapter +Module Type: Bidder Adapter +Maintainer: DL-ZetaDSP-Supply-Engineering@zetaglobal.com +``` + +# Description + +Module that connects to Zeta's demand sources + +# Test Parameters +``` + var adUnits = [ + { + mediaTypes: { + banner: { + sizes: [[300, 250]], // a display size + } + }, + bids: [ + { + bidder: 'zeta_global', + bidId: 12345, + params: { + placement: 12345, + user: { + uid: 12345, + buyeruid: 12345 + }, + ip: '111.222.33.44', + definerId: 1, + test: 1 + } + } + ] + } + ]; +``` diff --git a/package-lock.json b/package-lock.json index 1784b885be9..bf95487ed9d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "prebid.js", - "version": "4.8.0-pre", + "version": "4.14.0-pre", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -7796,13 +7796,24 @@ "function-bind": "^1.1.1", "has": "^1.0.3", "has-symbols": "^1.0.1", - "is-callable": "^1.2.0", - "is-regex": "^1.1.0", + "is-callable": "^1.1.5", + "is-regex": "^1.0.5", "object-inspect": "^1.7.0", "object-keys": "^1.1.1", "object.assign": "^4.1.0", - "string.prototype.trimend": "^1.0.1", - "string.prototype.trimstart": "^1.0.1" + "string.prototype.trimleft": "^2.1.1", + "string.prototype.trimright": "^2.1.1" + }, + "dependencies": { + "is-regex": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.1.tgz", + "integrity": "sha512-1+QkEcxiLlB7VEyFtyBg94e08OAsvq7FUBgApTq/w2ymCLyKJgDPsybBENVtA7XCQEgEXxKPonG+mvYRxh/LIg==", + "dev": true, + "requires": { + "has-symbols": "^1.0.1" + } + } } }, "es-array-method-boxes-properly": { @@ -7838,6 +7849,7 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", + "dev": true, "requires": { "is-callable": "^1.1.4", "is-date-object": "^1.0.1", @@ -12019,6 +12031,12 @@ "integrity": "sha1-aRC8pdqMleeEtXUbl2z1oQ/uNtI=", "dev": true }, + "is-negative-zero": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.0.tgz", + "integrity": "sha1-lVOxIbD6wohp2p7UWeIMdUN4hGE=", + "dev": true + }, "is-number": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", @@ -12076,7 +12094,7 @@ "integrity": "sha512-vlKW17SNq44owv5AQR3Cq0bQPEb8+kF3UKZ2fiZNOWtztYE5i0CzCZxFDwO58qAOWtxdBRVO/V5Qin1wjCqFYQ==", "dev": true, "requires": { - "has-symbols": "^1.0.1" + "has": "^1.0.3" } }, "is-relative": { @@ -15709,9 +15727,9 @@ "dev": true }, "live-connect-js": { - "version": "1.1.10", - "resolved": "https://registry.npmjs.org/live-connect-js/-/live-connect-js-1.1.10.tgz", - "integrity": "sha512-G/LJKN3b21DZILCQRyataC/znLvJRyogtu7mAkKlkhP9B9UJ8bcOL7ihW/clD2PsT4hVUkeabHhUGsPCmhsjFw==", + "version": "1.1.23", + "resolved": "https://registry.npmjs.org/live-connect-js/-/live-connect-js-1.1.23.tgz", + "integrity": "sha512-alOXlYyDdMXt8zzCIs3+iCrdi6r/69c7YRN3sMETa3b2cCOxep3i9j2O0iepk2hxT5JxiR1MvqlqdWAL9d2Hcg==", "requires": { "@kiosked/ulid": "^3.0.0", "abab": "^2.0.3", @@ -17484,7 +17502,8 @@ "object-inspect": { "version": "1.8.0", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.8.0.tgz", - "integrity": "sha512-jLdtEOB112fORuypAyl/50VRVIBIdVQOSUUGQHzJ4xBSbit81zRarz7GThkEFZy1RceYrWYcPcBFPQwHyAc1gA==" + "integrity": "sha512-jLdtEOB112fORuypAyl/50VRVIBIdVQOSUUGQHzJ4xBSbit81zRarz7GThkEFZy1RceYrWYcPcBFPQwHyAc1gA==", + "dev": true }, "object-is": { "version": "1.1.2", @@ -20697,23 +20716,143 @@ } }, "string.prototype.trimend": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.1.tgz", - "integrity": "sha512-LRPxFUaTtpqYsTeNKaFOw3R4bxIzWOnbQ837QfBylo8jIxtcbK/A/sMV7Q+OAV/vWo+7s25pOE10KYSjaSO06g==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.2.tgz", + "integrity": "sha512-8oAG/hi14Z4nOVP0z6mdiVZ/wqjDtWSLygMigTzAb+7aPEDTleeFf+WrF+alzecxIRkckkJVn+dTlwzJXORATw==", "dev": true, "requires": { "define-properties": "^1.1.3", - "es-abstract": "^1.17.5" + "es-abstract": "^1.18.0-next.1" + }, + "dependencies": { + "es-abstract": { + "version": "1.18.0-next.1", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.18.0-next.1.tgz", + "integrity": "sha512-I4UGspA0wpZXWENrdA0uHbnhte683t3qT/1VFH9aX2dA5PPSf6QW5HHXf5HImaqPmjXaVeVk4RGWnaylmV7uAA==", + "dev": true, + "requires": { + "es-to-primitive": "^1.2.1", + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.1", + "is-callable": "^1.2.2", + "is-negative-zero": "^2.0.0", + "is-regex": "^1.1.1", + "object-inspect": "^1.8.0", + "object-keys": "^1.1.1", + "object.assign": "^4.1.1", + "string.prototype.trimend": "^1.0.1", + "string.prototype.trimstart": "^1.0.1" + } + }, + "is-callable": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.2.tgz", + "integrity": "sha512-dnMqspv5nU3LoewK2N/y7KLtxtakvTuaCsU9FU50/QDmdbHNy/4/JuRtMHqRU22o3q+W89YQndQEeCVwK+3qrA==", + "dev": true + }, + "is-regex": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.1.tgz", + "integrity": "sha512-1+QkEcxiLlB7VEyFtyBg94e08OAsvq7FUBgApTq/w2ymCLyKJgDPsybBENVtA7XCQEgEXxKPonG+mvYRxh/LIg==", + "dev": true, + "requires": { + "has-symbols": "^1.0.1" + } + }, + "object.assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.1.tgz", + "integrity": "sha512-VT/cxmx5yaoHSOTSyrCygIDFco+RsibY2NM0a4RdEeY/4KgqezwFtK1yr3U67xYhqJSlASm2pKhLVzPj2lr4bA==", + "dev": true, + "requires": { + "define-properties": "^1.1.3", + "es-abstract": "^1.18.0-next.0", + "has-symbols": "^1.0.1", + "object-keys": "^1.1.1" + } + } + } + }, + "string.prototype.trimleft": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/string.prototype.trimleft/-/string.prototype.trimleft-2.1.2.tgz", + "integrity": "sha512-gCA0tza1JBvqr3bfAIFJGqfdRTyPae82+KTnm3coDXkZN9wnuW3HjGgN386D7hfv5CHQYCI022/rJPVlqXyHSw==", + "dev": true, + "requires": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.5", + "string.prototype.trimstart": "^1.0.0" + } + }, + "string.prototype.trimright": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/string.prototype.trimright/-/string.prototype.trimright-2.1.2.tgz", + "integrity": "sha512-ZNRQ7sY3KroTaYjRS6EbNiiHrOkjihL9aQE/8gfQ4DtAC/aEBRHFJa44OmoWxGGqXuJlfKkZW4WcXErGr+9ZFg==", + "dev": true, + "requires": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.5", + "string.prototype.trimend": "^1.0.0" } }, "string.prototype.trimstart": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.1.tgz", - "integrity": "sha512-XxZn+QpvrBI1FOcg6dIpxUPgWCPuNXvMD72aaRaUQv1eD4e/Qy8i/hFTe0BUmD60p/QA6bh1avmuPTfNjqVWRw==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.2.tgz", + "integrity": "sha512-7F6CdBTl5zyu30BJFdzSTlSlLPwODC23Od+iLoVH8X6+3fvDPPuBVVj9iaB1GOsSTSIgVfsfm27R2FGrAPznWg==", "dev": true, "requires": { "define-properties": "^1.1.3", - "es-abstract": "^1.17.5" + "es-abstract": "^1.18.0-next.1" + }, + "dependencies": { + "es-abstract": { + "version": "1.18.0-next.1", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.18.0-next.1.tgz", + "integrity": "sha512-I4UGspA0wpZXWENrdA0uHbnhte683t3qT/1VFH9aX2dA5PPSf6QW5HHXf5HImaqPmjXaVeVk4RGWnaylmV7uAA==", + "dev": true, + "requires": { + "es-to-primitive": "^1.2.1", + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.1", + "is-callable": "^1.2.2", + "is-negative-zero": "^2.0.0", + "is-regex": "^1.1.1", + "object-inspect": "^1.8.0", + "object-keys": "^1.1.1", + "object.assign": "^4.1.1", + "string.prototype.trimend": "^1.0.1", + "string.prototype.trimstart": "^1.0.1" + } + }, + "is-callable": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.2.tgz", + "integrity": "sha512-dnMqspv5nU3LoewK2N/y7KLtxtakvTuaCsU9FU50/QDmdbHNy/4/JuRtMHqRU22o3q+W89YQndQEeCVwK+3qrA==", + "dev": true + }, + "is-regex": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.1.tgz", + "integrity": "sha512-1+QkEcxiLlB7VEyFtyBg94e08OAsvq7FUBgApTq/w2ymCLyKJgDPsybBENVtA7XCQEgEXxKPonG+mvYRxh/LIg==", + "dev": true, + "requires": { + "has-symbols": "^1.0.1" + } + }, + "object.assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.1.tgz", + "integrity": "sha512-VT/cxmx5yaoHSOTSyrCygIDFco+RsibY2NM0a4RdEeY/4KgqezwFtK1yr3U67xYhqJSlASm2pKhLVzPj2lr4bA==", + "dev": true, + "requires": { + "define-properties": "^1.1.3", + "es-abstract": "^1.18.0-next.0", + "has-symbols": "^1.0.1", + "object-keys": "^1.1.1" + } + } } }, "string_decoder": { diff --git a/package.json b/package.json index 7141cc55b69..3b0d654b08e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "prebid.js", - "version": "4.9.0", + "version": "4.20.0", "description": "Header Bidding Management Library", "main": "src/prebid.js", "scripts": { @@ -112,6 +112,6 @@ "fun-hooks": "^0.9.9", "jsencrypt": "^3.0.0-rc.1", "just-clone": "^1.0.2", - "live-connect-js": "1.1.10" + "live-connect-js": "^1.1.23" } } diff --git a/src/Renderer.js b/src/Renderer.js index f073d97d052..7cedf278537 100644 --- a/src/Renderer.js +++ b/src/Renderer.js @@ -115,5 +115,21 @@ function isRendererPreferredFromAdUnit(adUnitCode) { const adUnit = find(adUnits, adUnit => { return adUnit.code === adUnitCode; }); - return !!(adUnit && adUnit.renderer && adUnit.renderer.url && adUnit.renderer.render && !(utils.isBoolean(adUnit.renderer.backupOnly) && adUnit.renderer.backupOnly)); + + if (!adUnit) { + return false + } + + // renderer defined at adUnit level + const adUnitRenderer = utils.deepAccess(adUnit, 'renderer'); + const hasValidAdUnitRenderer = !!(adUnitRenderer && adUnitRenderer.url && adUnitRenderer.render); + + // renderer defined at adUnit.mediaTypes level + const mediaTypeRenderer = utils.deepAccess(adUnit, 'mediaTypes.video.renderer'); + const hasValidMediaTypeRenderer = !!(mediaTypeRenderer && mediaTypeRenderer.url && mediaTypeRenderer.render) + + return !!( + (hasValidAdUnitRenderer && !(adUnitRenderer.backupOnly === true)) || + (hasValidMediaTypeRenderer && !(mediaTypeRenderer.backupOnly === true)) + ); } diff --git a/src/auction.js b/src/auction.js index 5858c3edf78..c94e3adc9a7 100644 --- a/src/auction.js +++ b/src/auction.js @@ -57,7 +57,7 @@ * @property {function(): void} callBids - sends requests to all adapters for bids */ -import {flatten, timestamp, adUnitsFilter, deepAccess, getBidRequest, getValue, parseUrl, isBoolean} from './utils.js'; +import {flatten, timestamp, adUnitsFilter, deepAccess, getBidRequest, getValue, parseUrl} from './utils.js'; import { getPriceBucketString } from './cpmBucketManager.js'; import { getNativeTargeting } from './native.js'; import { getCacheUrl, store } from './videoCache.js'; @@ -66,6 +66,7 @@ import { config } from './config.js'; import { userSync } from './userSync.js'; import { hook } from './hook.js'; import find from 'core-js-pure/features/array/find.js'; +import includes from 'core-js-pure/features/array/includes.js'; import { OUTSTREAM } from './video.js'; import { VIDEO } from './mediaTypes.js'; @@ -397,10 +398,19 @@ export function auctionCallbacks(auctionDone, auctionInstance) { function adapterDone() { let bidderRequest = this; + let bidderRequests = auctionInstance.getBidRequests(); + const auctionOptionsConfig = config.getConfig('auctionOptions'); bidderRequestsDone.add(bidderRequest); - allAdapterCalledDone = auctionInstance.getBidRequests() - .every(bidderRequest => bidderRequestsDone.has(bidderRequest)); + + if (auctionOptionsConfig && !utils.isEmpty(auctionOptionsConfig)) { + const secondaryBidders = auctionOptionsConfig.secondaryBidders; + if (secondaryBidders && !bidderRequests.every(bidder => includes(secondaryBidders, bidder.bidderCode))) { + bidderRequests = bidderRequests.filter(request => !includes(secondaryBidders, request.bidderCode)); + } + } + + allAdapterCalledDone = bidderRequests.every(bidderRequest => bidderRequestsDone.has(bidderRequest)); bidderRequest.bids.forEach(bid => { if (!bidResponseMap[bid.bidId]) { @@ -512,9 +522,26 @@ function getPreparedBidForAuction({adUnitCode, bid, bidderRequest, auctionId}) { const bidReq = bidderRequest.bids && find(bidderRequest.bids, bid => bid.adUnitCode == adUnitCode); const adUnitRenderer = bidReq && bidReq.renderer; - if (adUnitRenderer && adUnitRenderer.url && !(adUnitRenderer.backupOnly && isBoolean(adUnitRenderer.backupOnly) && bid.renderer)) { - bidObject.renderer = Renderer.install({ url: adUnitRenderer.url }); - bidObject.renderer.setRender(adUnitRenderer.render); + // a publisher can also define a renderer for a mediaType + const bidObjectMediaType = bidObject.mediaType; + const bidMediaType = bidReq && + bidReq.mediaTypes && + bidReq.mediaTypes[bidObjectMediaType]; + + var mediaTypeRenderer = bidMediaType && bidMediaType.renderer; + + var renderer = null; + + // the renderer for the mediaType takes precendence + if (mediaTypeRenderer && mediaTypeRenderer.url && !(mediaTypeRenderer.backupOnly === true && mediaTypeRenderer.render)) { + renderer = mediaTypeRenderer; + } else if (adUnitRenderer && adUnitRenderer.url && !(adUnitRenderer.backupOnly === true && bid.renderer)) { + renderer = adUnitRenderer; + } + + if (renderer) { + bidObject.renderer = Renderer.install({ url: renderer.url }); + bidObject.renderer.setRender(renderer.render); } // Use the config value 'mediaTypeGranularity' if it has been defined for mediaType, else use 'customPriceBucket' diff --git a/src/config.js b/src/config.js index 3284be52296..daaf739bbbd 100644 --- a/src/config.js +++ b/src/config.js @@ -199,6 +199,16 @@ export function newConfig() { set disableAjaxTimeout(val) { this._disableAjaxTimeout = val; }, + + _auctionOptions: {}, + get auctionOptions() { + return this._auctionOptions; + }, + set auctionOptions(val) { + if (validateauctionOptions(val)) { + this._auctionOptions = val; + } + }, }; if (config) { @@ -237,6 +247,30 @@ export function newConfig() { } return true; } + + function validateauctionOptions(val) { + if (!utils.isPlainObject(val)) { + utils.logWarn('Auction Options must be an object') + return false + } + + for (let k of Object.keys(val)) { + if (k !== 'secondaryBidders') { + utils.logWarn(`Auction Options given an incorrect param: ${k}`) + return false + } + if (k === 'secondaryBidders') { + if (!utils.isArray(val[k])) { + utils.logWarn(`Auction Options ${k} must be of type Array`); + return false + } else if (!val[k].every(utils.isStr)) { + utils.logWarn(`Auction Options ${k} must be only string`); + return false + } + } + } + return true; + } } /** diff --git a/src/constants.json b/src/constants.json index 7c0af445cdb..7e82946b65c 100644 --- a/src/constants.json +++ b/src/constants.json @@ -41,7 +41,7 @@ "AUCTION_DEBUG": "auctionDebug" }, "AD_RENDER_FAILED_REASON" : { - "PREVENT_WRITING_ON_MAIN_DOCUMENT": "preventWritingOnMainDocuemnt", + "PREVENT_WRITING_ON_MAIN_DOCUMENT": "preventWritingOnMainDocument", "NO_AD": "noAd", "EXCEPTION": "exception", "CANNOT_FIND_AD": "cannotFindAd", diff --git a/src/events.js b/src/events.js index e7a11635476..8749ddf206b 100644 --- a/src/events.js +++ b/src/events.js @@ -44,7 +44,8 @@ module.exports = (function () { eventsFired.push({ eventType: eventString, args: eventPayload, - id: key + id: key, + elapsedTime: utils.getPerformanceNow(), }); /** Push each specific callback to the `callbacks` array. diff --git a/src/prebid.js b/src/prebid.js index 31e0140cfe2..0f72ca878e5 100644 --- a/src/prebid.js +++ b/src/prebid.js @@ -149,7 +149,14 @@ export const checkAdUnitSetup = hook('sync', function (adUnits) { adUnits.forEach(adUnit => { const mediaTypes = adUnit.mediaTypes; + const bids = adUnit.bids; let validatedBanner, validatedVideo, validatedNative; + + if (!bids || !utils.isArray(bids)) { + utils.logError(`Detected adUnit.code '${adUnit.code}' did not have 'adUnit.bids' defined or 'adUnit.bids' is not an array. Removing adUnit from auction.`); + return; + } + if (!mediaTypes || Object.keys(mediaTypes).length === 0) { utils.logError(`Detected adUnit.code '${adUnit.code}' did not have a 'mediaTypes' object defined. This is a required field for the auction, so this adUnit has been removed.`); return; @@ -251,6 +258,18 @@ $$PREBID_GLOBAL$$.getNoBids = function () { return getBids('getNoBids'); }; +/** + * This function returns the bids requests involved in an auction but not bid on or the specified adUnitCode + * @param {string} adUnitCode adUnitCode + * @alias module:pbjs.getNoBidsForAdUnitCode + * @return {Object} bidResponse object + */ + +$$PREBID_GLOBAL$$.getNoBidsForAdUnitCode = function (adUnitCode) { + const bids = auctionManager.getNoBids().filter(bid => bid.adUnitCode === adUnitCode); + return { bids }; +}; + /** * This function returns the bid responses at the given moment. * @alias module:pbjs.getBidResponses @@ -342,7 +361,7 @@ function emitAdRenderFail({ reason, message, bid, id }) { * @param {string} id bid id to locate the ad * @alias module:pbjs.renderAd */ -$$PREBID_GLOBAL$$.renderAd = function (doc, id) { +$$PREBID_GLOBAL$$.renderAd = function (doc, id, options) { utils.logInfo('Invoking $$PREBID_GLOBAL$$.renderAd', arguments); utils.logMessage('Calling renderAd with adId :' + id); @@ -354,6 +373,14 @@ $$PREBID_GLOBAL$$.renderAd = function (doc, id) { // replace macros according to openRTB with price paid = bid.cpm bid.ad = utils.replaceAuctionPrice(bid.ad, bid.cpm); bid.adUrl = utils.replaceAuctionPrice(bid.adUrl, bid.cpm); + + // replacing clickthrough if submitted + if (options && options.clickThrough) { + const { clickThrough } = options; + bid.ad = utils.replaceClickThrough(bid.ad, clickThrough); + bid.adUrl = utils.replaceClickThrough(bid.adUrl, clickThrough); + } + // save winning bids auctionManager.addWinningBid(bid); diff --git a/src/refererDetection.js b/src/refererDetection.js index 60198678666..da68313736b 100644 --- a/src/refererDetection.js +++ b/src/refererDetection.js @@ -10,153 +10,48 @@ import { logWarn } from './utils.js'; +/** + * @param {Window} win Window + * @returns {Function} + */ export function detectReferer(win) { - /** - * Returns number of frames to reach top from current frame where prebid.js sits - * @returns {Array} levels - */ - function getLevels() { - let levels = walkUpWindows(); - let ancestors = getAncestorOrigins(); - - if (ancestors) { - for (let i = 0, l = ancestors.length; i < l; i++) { - levels[i].ancestor = ancestors[i]; - } - } - return levels; - } - /** * This function would return a read-only array of hostnames for all the parent frames. * win.location.ancestorOrigins is only supported in webkit browsers. For non-webkit browsers it will return undefined. + * + * @param {Window} win Window object * @returns {(undefined|Array)} Ancestor origins or undefined */ - function getAncestorOrigins() { + function getAncestorOrigins(win) { try { if (!win.location.ancestorOrigins) { return; } + return win.location.ancestorOrigins; } catch (e) { // Ignore error } } - /** - * This function would try to get referer and urls for all parent frames in case of win.location.ancestorOrigins undefined. - * @param {Array} levels - * @returns {Object} urls for all parent frames and top most detected referer url - */ - function getPubUrlStack(levels) { - let stack = []; - let defUrl = null; - let frameLocation = null; - let prevFrame = null; - let prevRef = null; - let ancestor = null; - let detectedRefererUrl = null; - - let i; - for (i = levels.length - 1; i >= 0; i--) { - try { - frameLocation = levels[i].location; - } catch (e) { - // Ignore error - } - - if (frameLocation) { - stack.push(frameLocation); - if (!detectedRefererUrl) { - detectedRefererUrl = frameLocation; - } - } else if (i !== 0) { - prevFrame = levels[i - 1]; - try { - prevRef = prevFrame.referrer; - ancestor = prevFrame.ancestor; - } catch (e) { - // Ignore error - } - - if (prevRef) { - stack.push(prevRef); - if (!detectedRefererUrl) { - detectedRefererUrl = prevRef; - } - } else if (ancestor) { - stack.push(ancestor); - if (!detectedRefererUrl) { - detectedRefererUrl = ancestor; - } - } else { - stack.push(defUrl); - } - } else { - stack.push(defUrl); - } - } - return { - stack, - detectedRefererUrl - }; - } - /** * This function returns canonical URL which refers to an HTML link element, with the attribute of rel="canonical", found in the element of your webpage + * * @param {Object} doc document + * @returns {string|null} */ function getCanonicalUrl(doc) { try { - let element = doc.querySelector("link[rel='canonical']"); + const element = doc.querySelector("link[rel='canonical']"); + if (element !== null) { return element.href; } } catch (e) { + // Ignore error } - return null; - } - /** - * Walk up to the top of the window to detect origin, number of iframes, ancestor origins and canonical url - */ - function walkUpWindows() { - let acc = []; - let currentWindow; - do { - try { - currentWindow = currentWindow ? currentWindow.parent : win; - try { - let isTop = (currentWindow == win.top); - let refData = { - referrer: currentWindow.document.referrer || null, - location: currentWindow.location.href || null, - isTop - } - if (isTop) { - refData = Object.assign(refData, { - canonicalUrl: getCanonicalUrl(currentWindow.document) - }) - } - acc.push(refData); - } catch (e) { - acc.push({ - referrer: null, - location: null, - isTop: (currentWindow == win.top) - }); - logWarn('Trying to access cross domain iframe. Continuing without referrer and location'); - } - } catch (e) { - acc.push({ - referrer: null, - location: null, - isTop: false - }); - return acc; - } - } while (currentWindow != win.top); - return acc; + return null; } /** @@ -170,31 +65,114 @@ export function detectReferer(win) { */ /** - * Get referer info + * Walk up the windows to get the origin stack and best available referrer, canonical URL, etc. + * * @returns {refererInfo} */ function refererInfo() { - try { - let levels = getLevels(); - let numIframes = levels.length - 1; - let reachedTop = (levels[numIframes].location !== null || - (numIframes > 0 && levels[numIframes - 1].referrer !== null)); - let stackInfo = getPubUrlStack(levels); - let canonicalUrl; - if (levels[levels.length - 1].canonicalUrl) { - canonicalUrl = levels[levels.length - 1].canonicalUrl; + const stack = []; + const ancestors = getAncestorOrigins(win); + let currentWindow; + let bestReferrer; + let bestCanonicalUrl; + let reachedTop = false; + let level = 0; + let valuesFromAmp = false; + let inAmpFrame = false; + + do { + const previousWindow = currentWindow; + const wasInAmpFrame = inAmpFrame; + let currentLocation; + let crossOrigin = false; + let foundReferrer = null; + + inAmpFrame = false; + currentWindow = currentWindow ? currentWindow.parent : win; + + try { + currentLocation = currentWindow.location.href || null; + } catch (e) { + crossOrigin = true; } - return { - referer: stackInfo.detectedRefererUrl, - reachedTop, - numIframes, - stack: stackInfo.stack, - canonicalUrl - }; - } catch (e) { - // Ignore error - } + if (crossOrigin) { + if (wasInAmpFrame) { + const context = previousWindow.context; + + try { + foundReferrer = context.sourceUrl; + bestReferrer = foundReferrer; + + valuesFromAmp = true; + + if (currentWindow === win.top) { + reachedTop = true; + } + + if (context.canonicalUrl) { + bestCanonicalUrl = context.canonicalUrl; + } + } catch (e) { /* Do nothing */ } + } else { + logWarn('Trying to access cross domain iframe. Continuing without referrer and location'); + + try { + const referrer = previousWindow.document.referrer; + + if (referrer) { + foundReferrer = referrer; + + if (currentWindow === win.top) { + reachedTop = true; + } + } + } catch (e) { /* Do nothing */ } + + if (!foundReferrer && ancestors && ancestors[level - 1]) { + foundReferrer = ancestors[level - 1]; + } + + if (foundReferrer && !valuesFromAmp) { + bestReferrer = foundReferrer; + } + } + } else { + if (currentLocation) { + foundReferrer = currentLocation; + bestReferrer = foundReferrer; + valuesFromAmp = false; + + if (currentWindow === win.top) { + reachedTop = true; + + const canonicalUrl = getCanonicalUrl(currentWindow.document); + + if (canonicalUrl) { + bestCanonicalUrl = canonicalUrl; + } + } + } + + if (currentWindow.context && currentWindow.context.sourceUrl) { + inAmpFrame = true; + } + } + + stack.push(foundReferrer); + level++; + } while (currentWindow !== win.top); + + stack.reverse(); + + return { + referer: bestReferrer || null, + reachedTop, + isAmp: valuesFromAmp, + numIframes: level - 1, + stack, + canonicalUrl: bestCanonicalUrl || null + }; } return refererInfo; diff --git a/src/secureCreatives.js b/src/secureCreatives.js index 34de2be275c..cb192dd773e 100644 --- a/src/secureCreatives.js +++ b/src/secureCreatives.js @@ -5,14 +5,14 @@ import events from './events.js'; import { fireNativeTrackers, getAssetMessage } from './native.js'; -import { EVENTS } from './constants.json'; +import constants from './constants.json'; import { logWarn, replaceAuctionPrice } from './utils.js'; import { auctionManager } from './auctionManager.js'; import find from 'core-js-pure/features/array/find.js'; import { isRendererRequired, executeRenderer } from './Renderer.js'; import includes from 'core-js-pure/features/array/includes.js'; -const BID_WON = EVENTS.BID_WON; +const BID_WON = constants.EVENTS.BID_WON; export function listenMessagesFromCreative() { window.addEventListener('message', receiveMessage, false); diff --git a/src/targeting.js b/src/targeting.js index 1b1e14fd4a6..b6a38bdbb61 100644 --- a/src/targeting.js +++ b/src/targeting.js @@ -181,6 +181,48 @@ export function newTargeting(auctionManager) { return []; }; + /** + * Returns filtered ad server targeting for custom and allowed keys. + * @param {targetingArray} targeting + * @param {string[]} allowedKeys + * @return {targetingArray} filtered targeting + */ + function getAllowedTargetingKeyValues(targeting, allowedKeys) { + const defaultKeyring = Object.assign({}, CONSTANTS.TARGETING_KEYS, CONSTANTS.NATIVE_KEYS); + const defaultKeys = Object.keys(defaultKeyring); + const keyDispositions = {}; + logInfo(`allowTargetingKeys - allowed keys [ ${allowedKeys.map(k => defaultKeyring[k]).join(', ')} ]`); + targeting.map(adUnit => { + const adUnitCode = Object.keys(adUnit)[0]; + const keyring = adUnit[adUnitCode]; + const keys = keyring.filter(kvPair => { + const key = Object.keys(kvPair)[0]; + // check if key is in default keys, if not, it's custom, we won't remove it. + const isCustom = defaultKeys.filter(defaultKey => key.indexOf(defaultKeyring[defaultKey]) === 0).length === 0; + // check if key explicitly allowed, if not, we'll remove it. + const found = isCustom || allowedKeys.find(allowedKey => { + const allowedKeyName = defaultKeyring[allowedKey]; + // we're looking to see if the key exactly starts with one of our default keys. + // (which hopefully means it's not custom) + const found = key.indexOf(allowedKeyName) === 0; + return found; + }); + keyDispositions[key] = !found; + return found; + }); + adUnit[adUnitCode] = keys; + }); + const removedKeys = Object.keys(keyDispositions).filter(d => keyDispositions[d]); + logInfo(`allowTargetingKeys - removed keys [ ${removedKeys.join(', ')} ]`); + // remove any empty targeting objects, as they're unnecessary. + const filteredTargeting = targeting.filter(adUnit => { + const adUnitCode = Object.keys(adUnit)[0]; + const keyring = adUnit[adUnitCode]; + return keyring.length > 0; + }); + return filteredTargeting + } + /** * Returns all ad server targeting for all ad units. * @param {string=} adUnitCode @@ -193,7 +235,8 @@ export function newTargeting(auctionManager) { // `alwaysUseBid=true`. If sending all bids is enabled, add targeting for losing bids. var targeting = getWinningBidTargeting(adUnitCodes, bidsReceived) .concat(getCustomBidTargeting(adUnitCodes, bidsReceived)) - .concat(config.getConfig('enableSendAllBids') ? getBidLandscapeTargeting(adUnitCodes, bidsReceived) : getDealBids(adUnitCodes, bidsReceived)); + .concat(config.getConfig('enableSendAllBids') ? getBidLandscapeTargeting(adUnitCodes, bidsReceived) : getDealBids(adUnitCodes, bidsReceived)) + .concat(getAdUnitTargeting(adUnitCodes)); // store a reference of the targeting keys targeting.map(adUnitCode => { @@ -206,6 +249,11 @@ export function newTargeting(auctionManager) { }); }); + const allowedKeys = config.getConfig('targetingControls.allowTargetingKeys'); + if (Array.isArray(allowedKeys) && allowedKeys.length > 0) { + targeting = getAllowedTargetingKeyValues(targeting, allowedKeys); + } + targeting = flattenTargeting(targeting); const auctionKeysThreshold = config.getConfig('targetingControls.auctionKeyMaxChars'); @@ -562,6 +610,27 @@ export function newTargeting(auctionManager) { }); } + function getAdUnitTargeting(adUnitCodes) { + function getTargetingObj(adUnit) { + return deepAccess(adUnit, CONSTANTS.JSON_MAPPING.ADSERVER_TARGETING); + } + + function getTargetingValues(adUnit) { + const aut = getTargetingObj(adUnit); + + return Object.keys(aut) + .map(function(key) { + return {[key]: utils.isArray(aut[key]) ? aut[key] : aut[key].split(',')}; + }); + } + + return auctionManager.getAdUnits() + .filter(adUnit => includes(adUnitCodes, adUnit.code) && getTargetingObj(adUnit)) + .map(adUnit => { + return {[adUnit.code]: getTargetingValues(adUnit)} + }); + } + targeting.isApntagDefined = function() { if (window.apntag && utils.isFn(window.apntag.setKeywords)) { return true; diff --git a/src/utils.js b/src/utils.js index 9426308daf4..acdf0f101ad 100644 --- a/src/utils.js +++ b/src/utils.js @@ -718,10 +718,23 @@ export function replaceAuctionPrice(str, cpm) { return str.replace(/\$\{AUCTION_PRICE\}/g, cpm); } +export function replaceClickThrough(str, clicktag) { + if (!str || !clicktag || typeof clicktag !== 'string') return; + return str.replace(/\${CLICKTHROUGH}/g, clicktag); +} + export function timestamp() { return new Date().getTime(); } +/** + * The returned value represents the time elapsed since the time origin. @see https://developer.mozilla.org/en-US/docs/Web/API/Performance/now + * @returns {number} + */ +export function getPerformanceNow() { + return (window.performance && window.performance.now && window.performance.now()) || 0; +} + /** * When the deviceAccess flag config option is false, no cookies should be read or set * @returns {boolean} diff --git a/src/video.js b/src/video.js index befeb2ded39..20df7a92442 100644 --- a/src/video.js +++ b/src/video.js @@ -59,7 +59,7 @@ export const checkVideoBidSetup = hook('sync', function(bid, bidRequest, videoMe // outstream bids require a renderer on the bid or pub-defined on adunit if (context === OUTSTREAM) { - return !!(bid.renderer || bidRequest.renderer); + return !!(bid.renderer || bidRequest.renderer || videoMediaType.renderer); } return true; diff --git a/test/mocks/fabrickId.json b/test/mocks/fabrickId.json new file mode 100644 index 00000000000..a8723ec88ec --- /dev/null +++ b/test/mocks/fabrickId.json @@ -0,0 +1,3 @@ +{ + "fabrickId": 1980 +} diff --git a/test/spec/api_spec.js b/test/spec/api_spec.js index 6f21eba7aaf..6d67565056f 100755 --- a/test/spec/api_spec.js +++ b/test/spec/api_spec.js @@ -43,10 +43,14 @@ describe('Publisher API', function () { assert.isFunction($$PREBID_GLOBAL$$.getBidResponses); }); - it('should have function $$PREBID_GLOBAL$$.getBidResponses', function () { + it('should have function $$PREBID_GLOBAL$$.getNoBids', function () { assert.isFunction($$PREBID_GLOBAL$$.getNoBids); }); + it('should have function $$PREBID_GLOBAL$$.getNoBidsForAdUnitCode', function () { + assert.isFunction($$PREBID_GLOBAL$$.getNoBidsForAdUnitCode); + }); + it('should have function $$PREBID_GLOBAL$$.getBidResponsesForAdUnitCode', function () { assert.isFunction($$PREBID_GLOBAL$$.getBidResponsesForAdUnitCode); }); diff --git a/test/spec/auctionmanager_spec.js b/test/spec/auctionmanager_spec.js index e35b1406fbf..a50eba5e585 100644 --- a/test/spec/auctionmanager_spec.js +++ b/test/spec/auctionmanager_spec.js @@ -763,6 +763,68 @@ describe('auctionmanager.js', function () { assert.equal(addedBid.renderer.url, 'renderer.js'); }); + it('installs publisher-defined renderers for a media type', function () { + const renderer = { + url: 'videoRenderer.js', + render: (bid) => bid + }; + let myBid = mockBid(); + let bidRequest = mockBidRequest(myBid); + + bidRequest.bids[0] = { + ...bidRequest.bids[0], + mediaTypes: { + banner: { + sizes: [[300, 250], [300, 600]] + }, + video: { + context: 'outstream', + renderer + } + } + }; + makeRequestsStub.returns([bidRequest]); + + myBid.mediaType = 'video'; + spec.interpretResponse.returns(myBid); + auction.callBids(); + + const addedBid = auction.getBidsReceived().pop(); + assert.equal(addedBid.renderer.url, renderer.url); + }); + + it('installs bidder-defined renderer when onlyBackup is true in mediaTypes.video options ', function () { + const renderer = { + url: 'videoRenderer.js', + backupOnly: true, + render: (bid) => bid + }; + let myBid = mockBid(); + let bidRequest = mockBidRequest(myBid); + + bidRequest.bids[0] = { + ...bidRequest.bids[0], + mediaTypes: { + video: { + context: 'outstream', + renderer + } + } + }; + makeRequestsStub.returns([bidRequest]); + + myBid.mediaType = 'video'; + myBid.renderer = { + url: 'renderer.js', + render: sinon.spy() + }; + spec.interpretResponse.returns(myBid); + auction.callBids(); + + const addedBid = auction.getBidsReceived().pop(); + assert.strictEqual(addedBid.renderer.url, myBid.renderer.url); + }); + it('bid for a regular unit and a video unit', function() { let renderer = { url: 'renderer.js', @@ -1252,4 +1314,119 @@ describe('auctionmanager.js', function () { assert.equal(doneSpy.callCount, 1); }) }); + + describe('auctionOptions', function() { + let bidRequests; + let doneSpy; + let clock; + let auction = { + getBidRequests: () => bidRequests, + getAuctionId: () => '1', + addBidReceived: () => true, + getTimeout: () => 1000 + } + let requiredBidder = BIDDER_CODE; + let requiredBidder1 = BIDDER_CODE1; + let secondaryBidder = 'doNotWaitForMe'; + + beforeEach(() => { + clock = sinon.useFakeTimers(); + doneSpy = sinon.spy(); + config.setConfig({ + 'auctionOptions': { + secondaryBidders: [ secondaryBidder ] + } + }) + }); + + afterEach(() => { + doneSpy.resetHistory(); + config.resetConfig(); + clock.restore(); + }); + + it('should not wait to call auction done for secondary bidders', function () { + let bids1 = [mockBid({ bidderCode: requiredBidder })]; + let bids2 = [mockBid({ bidderCode: requiredBidder1 })]; + let bids3 = [mockBid({ bidderCode: secondaryBidder })]; + bidRequests = [ + mockBidRequest(bids1[0], { adUnitCode: ADUNIT_CODE1 }), + mockBidRequest(bids2[0], { adUnitCode: ADUNIT_CODE1 }), + mockBidRequest(bids3[0], { adUnitCode: ADUNIT_CODE1 }), + ]; + let cbs = auctionCallbacks(doneSpy, auction); + // required bidder responds immeaditely to auction + cbs.addBidResponse.call(bidRequests[0], ADUNIT_CODE1, bids1[0]); + cbs.adapterDone.call(bidRequests[0]); + assert.equal(doneSpy.callCount, 0); + + // auction waits for second required bidder to respond + clock.tick(100); + cbs.addBidResponse.call(bidRequests[1], ADUNIT_CODE1, bids2[0]); + cbs.adapterDone.call(bidRequests[1]); + + // auction done is reported and does not wait for secondaryBidder request + assert.equal(doneSpy.callCount, 1); + + cbs.addBidResponse.call(bidRequests[2], ADUNIT_CODE1, bids3[0]); + cbs.adapterDone.call(bidRequests[2]); + }); + + it('should wait for all bidders if they are all secondary', function () { + config.setConfig({ + 'auctionOptions': { + secondaryBidders: [requiredBidder, requiredBidder1, secondaryBidder] + } + }) + let bids1 = [mockBid({ bidderCode: requiredBidder })]; + let bids2 = [mockBid({ bidderCode: requiredBidder1 })]; + let bids3 = [mockBid({ bidderCode: secondaryBidder })]; + bidRequests = [ + mockBidRequest(bids1[0], { adUnitCode: ADUNIT_CODE1 }), + mockBidRequest(bids2[0], { adUnitCode: ADUNIT_CODE1 }), + mockBidRequest(bids3[0], { adUnitCode: ADUNIT_CODE1 }), + ]; + let cbs = auctionCallbacks(doneSpy, auction); + cbs.addBidResponse.call(bidRequests[0], ADUNIT_CODE1, bids1[0]); + cbs.adapterDone.call(bidRequests[0]); + clock.tick(100); + assert.equal(doneSpy.callCount, 0) + + cbs.addBidResponse.call(bidRequests[1], ADUNIT_CODE1, bids2[0]); + cbs.adapterDone.call(bidRequests[1]); + clock.tick(100); + assert.equal(doneSpy.callCount, 0); + + cbs.addBidResponse.call(bidRequests[2], ADUNIT_CODE1, bids3[0]); + cbs.adapterDone.call(bidRequests[2]); + assert.equal(doneSpy.callCount, 1); + }); + + it('should allow secondaryBidders to respond in auction before is is done', function () { + let bids1 = [mockBid({ bidderCode: requiredBidder })]; + let bids2 = [mockBid({ bidderCode: requiredBidder1 })]; + let bids3 = [mockBid({ bidderCode: secondaryBidder })]; + bidRequests = [ + mockBidRequest(bids1[0], { adUnitCode: ADUNIT_CODE1 }), + mockBidRequest(bids2[0], { adUnitCode: ADUNIT_CODE1 }), + mockBidRequest(bids3[0], { adUnitCode: ADUNIT_CODE1 }), + ]; + let cbs = auctionCallbacks(doneSpy, auction); + // secondaryBidder is first to respond + cbs.addBidResponse.call(bidRequests[2], ADUNIT_CODE1, bids3[0]); + cbs.adapterDone.call(bidRequests[2]); + clock.tick(100); + assert.equal(doneSpy.callCount, 0); + + cbs.addBidResponse.call(bidRequests[1], ADUNIT_CODE1, bids2[0]); + cbs.adapterDone.call(bidRequests[1]); + clock.tick(100); + assert.equal(doneSpy.callCount, 0); + + // first required bidder takes longest to respond, auction isn't marked as done until this occurs + cbs.addBidResponse.call(bidRequests[0], ADUNIT_CODE1, bids1[0]); + cbs.adapterDone.call(bidRequests[0]); + assert.equal(doneSpy.callCount, 1); + }); + }); }); diff --git a/test/spec/config_spec.js b/test/spec/config_spec.js index be5b4bbb78b..81ce966efb2 100644 --- a/test/spec/config_spec.js +++ b/test/spec/config_spec.js @@ -211,4 +211,37 @@ describe('config API', function () { setConfig({ bidderSequence: 'random' }); expect(logWarnSpy.called).to.equal(false); }); + + it('sets auctionOptions', function () { + const auctionOptionsConfig = { + 'secondaryBidders': ['rubicon', 'appnexus'] + } + setConfig({ auctionOptions: auctionOptionsConfig }); + expect(getConfig('auctionOptions')).to.eql(auctionOptionsConfig); + }); + + it('should log warning for the wrong value passed to auctionOptions', function () { + setConfig({ auctionOptions: '' }); + expect(logWarnSpy.calledOnce).to.equal(true); + const warning = 'Auction Options must be an object'; + assert.ok(logWarnSpy.calledWith(warning), 'expected warning was logged'); + }); + + it('should log warning for invalid auctionOptions bidder values', function () { + setConfig({ auctionOptions: { + 'secondaryBidders': 'appnexus, rubicon', + }}); + expect(logWarnSpy.calledOnce).to.equal(true); + const warning = 'Auction Options secondaryBidders must be of type Array'; + assert.ok(logWarnSpy.calledWith(warning), 'expected warning was logged'); + }); + + it('should log warning for invalid properties to auctionOptions', function () { + setConfig({ auctionOptions: { + 'testing': true + }}); + expect(logWarnSpy.calledOnce).to.equal(true); + const warning = 'Auction Options given an incorrect param: testing'; + assert.ok(logWarnSpy.calledWith(warning), 'expected warning was logged'); + }); }); diff --git a/test/spec/modules/33acrossBidAdapter_spec.js b/test/spec/modules/33acrossBidAdapter_spec.js index d30659791ea..edc7b7a2767 100644 --- a/test/spec/modules/33acrossBidAdapter_spec.js +++ b/test/spec/modules/33acrossBidAdapter_spec.js @@ -7,8 +7,8 @@ import { spec } from 'modules/33acrossBidAdapter.js'; describe('33acrossBidAdapter:', function () { const BIDDER_CODE = '33across'; - const SITE_ID = 'pub1234'; - const PRODUCT_ID = 'product1'; + const SITE_ID = 'sample33xGUID123456789'; + const PRODUCT_ID = 'siab'; const END_POINT = 'https://ssc.33across.com/api/v1/hb'; let element, win; @@ -17,39 +17,13 @@ describe('33acrossBidAdapter:', function () { function TtxRequestBuilder() { const ttxRequest = { - imp: [{ - banner: { - format: [ - { - w: 300, - h: 250 - }, - { - w: 728, - h: 90 - } - ], - ext: { - ttx: { - viewability: { - amount: 100 - } - } - } - }, - ext: { - ttx: { - prod: PRODUCT_ID - } - } - }], + imp: [{}], site: { id: SITE_ID }, id: 'b1', user: { ext: { - consent: undefined } }, regs: { @@ -69,13 +43,52 @@ describe('33acrossBidAdapter:', function () { } }; - this.withSizes = sizes => { + this.withBanner = () => { + Object.assign(ttxRequest.imp[0], { + banner: { + format: [ + { + w: 300, + h: 250 + }, + { + w: 728, + h: 90 + } + ], + ext: { + ttx: { + viewability: { + amount: 100 + } + } + } + } + }); + + return this; + }; + + this.withBannerSizes = this.withSizes = sizes => { Object.assign(ttxRequest.imp[0].banner, { format: sizes }); return this; }; - this.withViewability = viewability => { - Object.assign(ttxRequest.imp[0].banner, { + this.withVideo = (params = {}) => { + Object.assign(ttxRequest.imp[0], { + video: { + w: 300, + h: 250, + placement: 2, + ...params + } + }); + + return this; + }; + + this.withViewability = (viewability, format = 'banner') => { + Object.assign(ttxRequest.imp[0][format], { ext: { ttx: { viewability } } @@ -83,6 +96,18 @@ describe('33acrossBidAdapter:', function () { return this; }; + this.withProduct = (prod = PRODUCT_ID) => { + Object.assign(ttxRequest.imp[0], { + ext: { + ttx: { + prod + } + } + }); + + return this; + }; + this.withGdprConsent = (consent, gdpr) => { Object.assign(ttxRequest, { user: { @@ -140,18 +165,31 @@ describe('33acrossBidAdapter:', function () { return this; }; - this.withFormatFloors = floors => { - const format = ttxRequest.imp[0].banner.format.map((fm, i) => { - return Object.assign(fm, { - ext: { - ttx: { - bidfloors: [ floors[i] ] + this.withFloors = this.withFormatFloors = (mediaType, floors) => { + switch (mediaType) { + case 'banner': + const format = ttxRequest.imp[0].banner.format.map((fm, i) => { + return Object.assign(fm, { + ext: { + ttx: { + bidfloors: [ floors[i] ] + } + } + }) + }); + + ttxRequest.imp[0].banner.format = format; + break; + case 'video': + Object.assign(ttxRequest.imp[0].video, { + ext: { + ttx: { + bidfloors: floors + } } - } - }) - }); - - ttxRequest.imp[0].banner.format = format; + }); + break; + } return this; }; @@ -188,6 +226,53 @@ describe('33acrossBidAdapter:', function () { this.build = () => serverRequest; } + function BidRequestsBuilder() { + const bidRequests = [ + { + bidId: 'b1', + bidder: '33across', + bidderRequestId: 'b1a', + params: { + siteId: SITE_ID, + productId: PRODUCT_ID + }, + adUnitCode: 'div-id', + auctionId: 'r1', + mediaTypes: {}, + transactionId: 't1' + } + ]; + + this.withBanner = () => { + bidRequests[0].mediaTypes.banner = { + sizes: [ + [300, 250], + [728, 90] + ] + }; + + return this; + }; + + this.withProduct = (prod) => { + bidRequests[0].params.productId = prod; + + return this; + }; + + this.withVideo = (params) => { + bidRequests[0].mediaTypes.video = { + playerSize: [[300, 250]], + context: 'outstream', + ...params + }; + + return this; + } + + this.build = () => bidRequests; + } + beforeEach(function() { element = { x: 0, @@ -217,24 +302,11 @@ describe('33acrossBidAdapter:', function () { innerHeight: 600 }; - bidRequests = [ - { - bidId: 'b1', - bidder: '33across', - bidderRequestId: 'b1a', - params: { - siteId: SITE_ID, - productId: PRODUCT_ID - }, - adUnitCode: 'div-id', - auctionId: 'r1', - sizes: [ - [300, 250], - [728, 90] - ], - transactionId: 't1' - } - ]; + bidRequests = ( + new BidRequestsBuilder() + .withBanner() + .build() + ); sandbox = sinon.sandbox.create(); sandbox.stub(Date, 'now').returns(1); @@ -248,78 +320,246 @@ describe('33acrossBidAdapter:', function () { }); describe('isBidRequestValid:', function() { - it('returns true when valid bid request is sent', function() { - const validBid = { - bidder: BIDDER_CODE, - params: { - siteId: SITE_ID, - productId: PRODUCT_ID - } - }; + context('basic validation', function() { + it('returns true for valid guid values', function() { + // NOTE: We ignore whitespace at the start and end since + // in our experience these are common typos + const validGUIDs = [ + `${SITE_ID}`, + `${SITE_ID} `, + ` ${SITE_ID}`, + ` ${SITE_ID} ` + ]; - expect(spec.isBidRequestValid(validBid)).to.be.true; - }); + validGUIDs.forEach((siteId) => { + const bid = { + bidder: '33across', + params: { + siteId + } + }; - it('returns true when valid test bid request is sent', function() { - const validBid = { - bidder: BIDDER_CODE, - params: { - siteId: SITE_ID, - productId: PRODUCT_ID, - test: 1 - } - }; + expect(spec.isBidRequestValid(bid)).to.be.true; + }); + }); - expect(spec.isBidRequestValid(validBid)).to.be.true; - }); + it('returns false for invalid guid values', function() { + const invalidGUIDs = [ + undefined, + 'siab' + ]; - it('returns false when bidder not set to "33across"', function() { - const invalidBid = { - bidder: 'foo', - params: { - siteId: SITE_ID, - productId: PRODUCT_ID - } - }; + invalidGUIDs.forEach((siteId) => { + const bid = { + bidder: '33across', + params: { + siteId + } + }; - expect(spec.isBidRequestValid(invalidBid)).to.be.false; + expect(spec.isBidRequestValid(bid)).to.be.false; + }); + }); }); - it('returns false when params not set', function() { - const invalidBid = { - bidder: 'foo' - }; + context('banner validation', function() { + it('returns true when banner mediaType does not exist', function() { + const bid = { + bidder: '33across', + params: { + siteId: 'cxBE0qjUir6iopaKkGJozW' + } + }; - expect(spec.isBidRequestValid(invalidBid)).to.be.false; - }); + expect(spec.isBidRequestValid(bid)).to.be.true; + }); - it('returns false when site ID is not set in params', function() { - const invalidBid = { - bidder: 'foo', - params: { - productId: PRODUCT_ID - } - }; + it('returns true when banner sizes are defined', function() { + const bid = { + bidder: '33across', + mediaTypes: { + banner: { + sizes: [[250, 300]] + } + }, + params: { + siteId: 'cxBE0qjUir6iopaKkGJozW' + } + }; - expect(spec.isBidRequestValid(invalidBid)).to.be.false; - }); + expect(spec.isBidRequestValid(bid)).to.be.true; + }); - it('returns false when product ID not set in params', function() { - const invalidBid = { - bidder: 'foo', - params: { - siteId: SITE_ID - } - }; + it('returns false when banner sizes are invalid', function() { + const invalidSizes = [ + undefined, + '16:9', + 300, + 'foo' + ]; + + invalidSizes.forEach((sizes) => { + const bid = { + bidder: '33across', + mediaTypes: { + banner: { + sizes + } + }, + params: { + siteId: 'cxBE0qjUir6iopaKkGJozW' + } + }; - expect(spec.isBidRequestValid(invalidBid)).to.be.false; + expect(spec.isBidRequestValid(bid)).to.be.false; + }); + }); }); + + context('video validation', function() { + beforeEach(function() { + // Basic Valid BidRequest + this.bid = { + bidder: '33across', + mediaTypes: { + video: { + playerSize: [[300, 50]], + context: 'outstream', + mimes: ['foo', 'bar'], + protocols: [1, 2] + } + }, + params: { + siteId: `${SITE_ID}` + } + }; + }); + + it('returns true when video mediaType does not exist', function() { + const bid = { + bidder: '33across', + params: { + siteId: `${SITE_ID}` + } + }; + + expect(spec.isBidRequestValid(bid)).to.be.true; + }); + + it('returns true when valid video mediaType is defined', function() { + expect(spec.isBidRequestValid(this.bid)).to.be.true; + }); + + it('returns false when video context is not defined', function() { + delete this.bid.mediaTypes.video.context; + + expect(spec.isBidRequestValid(this.bid)).to.be.false; + }); + + it('returns false when video playserSize is invalid', function() { + const invalidSizes = [ + undefined, + '16:9', + 300, + 'foo' + ]; + + invalidSizes.forEach((playerSize) => { + this.bid.mediaTypes.video.playerSize = playerSize; + expect(spec.isBidRequestValid(this.bid)).to.be.false; + }); + }); + + it('returns false when video mimes is invalid', function() { + const invalidMimes = [ + undefined, + 'foo', + 1, + [] + ] + + invalidMimes.forEach((mimes) => { + this.bid.mediaTypes.video.mimes = mimes; + expect(spec.isBidRequestValid(this.bid)).to.be.false; + }) + }); + + it('returns false when video protocols is invalid', function() { + const invalidMimes = [ + undefined, + 'foo', + 1, + [] + ] + + invalidMimes.forEach((protocols) => { + this.bid.mediaTypes.video.protocols = protocols; + expect(spec.isBidRequestValid(this.bid)).to.be.false; + }) + }); + + it('returns false when video placement is invalid', function() { + const invalidPlacement = [ + [], + '1', + {}, + 'foo' + ]; + + invalidPlacement.forEach((placement) => { + this.bid.mediaTypes.video.placement = placement; + expect(spec.isBidRequestValid(this.bid)).to.be.false; + }); + }); + + it('returns false when video startdelay is invalid for instream context', function() { + const bidRequests = ( + new BidRequestsBuilder() + .withVideo({context: 'instream', protocols: [1, 2], mimes: ['foo', 'bar']}) + .build() + ); + + const invalidStartdelay = [ + [], + '1', + {}, + 'foo' + ]; + + invalidStartdelay.forEach((startdelay) => { + bidRequests[0].mediaTypes.video.startdelay = startdelay; + expect(spec.isBidRequestValid(bidRequests[0])).to.be.false; + }); + }); + + it('returns true when video startdelay is invalid for outstream context', function() { + const bidRequests = ( + new BidRequestsBuilder() + .withVideo({context: 'outstream', protocols: [1, 2], mimes: ['foo', 'bar']}) + .build() + ); + + const invalidStartdelay = [ + [], + '1', + {}, + 'foo' + ]; + + invalidStartdelay.forEach((startdelay) => { + bidRequests[0].mediaTypes.video.startdelay = startdelay; + expect(spec.isBidRequestValid(bidRequests[0])).to.be.true; + }); + }); + }) }); describe('buildRequests:', function() { context('when element is fully in view', function() { it('returns 100', function() { const ttxRequest = new TtxRequestBuilder() + .withBanner() + .withProduct() .withViewability({amount: 100}) .build(); const serverRequest = new ServerRequestBuilder() @@ -335,6 +575,8 @@ describe('33acrossBidAdapter:', function () { context('when element is out of view', function() { it('returns 0', function() { const ttxRequest = new TtxRequestBuilder() + .withBanner() + .withProduct() .withViewability({amount: 0}) .build(); const serverRequest = new ServerRequestBuilder() @@ -350,6 +592,8 @@ describe('33acrossBidAdapter:', function () { context('when element is partially in view', function() { it('returns percentage', function() { const ttxRequest = new TtxRequestBuilder() + .withBanner() + .withProduct() .withViewability({amount: 75}) .build(); const serverRequest = new ServerRequestBuilder() @@ -365,6 +609,8 @@ describe('33acrossBidAdapter:', function () { context('when width or height of the element is zero', function() { it('try to use alternative values', function() { const ttxRequest = new TtxRequestBuilder() + .withBanner() + .withProduct() .withSizes([{ w: 800, h: 2400 }]) .withViewability({amount: 25}) .build(); @@ -373,7 +619,7 @@ describe('33acrossBidAdapter:', function () { .build(); Object.assign(element, { width: 0, height: 0 }); - bidRequests[0].sizes = [[800, 2400]]; + bidRequests[0].mediaTypes.banner.sizes = [[800, 2400]]; expect(spec.buildRequests(bidRequests)).to.deep.equal([ serverRequest ]); }); @@ -382,6 +628,8 @@ describe('33acrossBidAdapter:', function () { context('when nested iframes', function() { it('returns \'nm\'', function() { const ttxRequest = new TtxRequestBuilder() + .withBanner() + .withProduct() .withViewability({amount: spec.NON_MEASURABLE}) .build(); const serverRequest = new ServerRequestBuilder() @@ -402,6 +650,8 @@ describe('33acrossBidAdapter:', function () { context('when tab is inactive', function() { it('returns 0', function() { const ttxRequest = new TtxRequestBuilder() + .withBanner() + .withProduct() .withViewability({amount: 0}) .build(); const serverRequest = new ServerRequestBuilder() @@ -432,6 +682,8 @@ describe('33acrossBidAdapter:', function () { it('returns corresponding server requests with gdpr consent data', function() { const ttxRequest = new TtxRequestBuilder() + .withBanner() + .withProduct() .withGdprConsent('foobarMyPreference', 1) .build(); const serverRequest = new ServerRequestBuilder() @@ -450,6 +702,8 @@ describe('33acrossBidAdapter:', function () { }); const ttxRequest = new TtxRequestBuilder() + .withBanner() + .withProduct() .withGdprConsent('foobarMyPreference', 1) .build(); const serverRequest = new ServerRequestBuilder() @@ -471,6 +725,8 @@ describe('33acrossBidAdapter:', function () { it('returns corresponding server requests with default gdpr consent data', function() { const ttxRequest = new TtxRequestBuilder() + .withBanner() + .withProduct() .build(); const serverRequest = new ServerRequestBuilder() .withData(ttxRequest) @@ -488,6 +744,8 @@ describe('33acrossBidAdapter:', function () { }); const ttxRequest = new TtxRequestBuilder() + .withBanner() + .withProduct() .build(); const serverRequest = new ServerRequestBuilder() .withData(ttxRequest) @@ -510,6 +768,8 @@ describe('33acrossBidAdapter:', function () { it('returns corresponding server requests with us_privacy consent data', function() { const ttxRequest = new TtxRequestBuilder() + .withBanner() + .withProduct() .withUspConsent('foo') .build(); const serverRequest = new ServerRequestBuilder() @@ -528,6 +788,8 @@ describe('33acrossBidAdapter:', function () { }); const ttxRequest = new TtxRequestBuilder() + .withBanner() + .withProduct() .withUspConsent('foo') .build(); const serverRequest = new ServerRequestBuilder() @@ -549,6 +811,8 @@ describe('33acrossBidAdapter:', function () { it('returns corresponding server requests with default us_privacy data', function() { const ttxRequest = new TtxRequestBuilder() + .withBanner() + .withProduct() .build(); const serverRequest = new ServerRequestBuilder() .withData(ttxRequest) @@ -566,6 +830,8 @@ describe('33acrossBidAdapter:', function () { }); const ttxRequest = new TtxRequestBuilder() + .withBanner() + .withProduct() .build(); const serverRequest = new ServerRequestBuilder() .withData(ttxRequest) @@ -586,6 +852,8 @@ describe('33acrossBidAdapter:', function () { }; const ttxRequest = new TtxRequestBuilder() + .withBanner() + .withProduct() .withPageUrl('http://foo.com/bar') .build(); const serverRequest = new ServerRequestBuilder() @@ -605,6 +873,8 @@ describe('33acrossBidAdapter:', function () { }; const ttxRequest = new TtxRequestBuilder() + .withBanner() + .withProduct() .build(); const serverRequest = new ServerRequestBuilder() .withData(ttxRequest) @@ -656,6 +926,8 @@ describe('33acrossBidAdapter:', function () { bidRequests[0].schain = schain; const ttxRequest = new TtxRequestBuilder() + .withBanner() + .withProduct() .withSchain(schain) .build(); const serverRequest = new ServerRequestBuilder() @@ -672,6 +944,8 @@ describe('33acrossBidAdapter:', function () { context('when there no schain object is passed', function() { it('does not set source field', function() { const ttxRequest = new TtxRequestBuilder() + .withBanner() + .withProduct() .build(); const serverRequest = new ServerRequestBuilder() @@ -684,9 +958,11 @@ describe('33acrossBidAdapter:', function () { }); }); - context('when price floor module is not enabled in bidRequest', function() { + context('when price floor module is not enabled for banner in bidRequest', function() { it('does not set any bidfloors in ttxRequest', function() { const ttxRequest = new TtxRequestBuilder() + .withBanner() + .withProduct() .build(); const serverRequest = new ServerRequestBuilder() .withData(ttxRequest) @@ -697,11 +973,13 @@ describe('33acrossBidAdapter:', function () { }); }); - context('when price floor module is enabled in bidRequest', function() { + context('when price floor module is enabled for banner in bidRequest', function() { it('does not set any bidfloors in ttxRequest if there is no floor', function() { bidRequests[0].getFloor = () => ({}); const ttxRequest = new TtxRequestBuilder() + .withBanner() + .withProduct() .build(); const serverRequest = new ServerRequestBuilder() .withData(ttxRequest) @@ -723,9 +1001,188 @@ describe('33acrossBidAdapter:', function () { }; const ttxRequest = new TtxRequestBuilder() - .withFormatFloors([ 1.0, 0.10 ]) + .withBanner() + .withProduct() + .withFormatFloors('banner', [ 1.0, 0.10 ]) + .build(); + + const serverRequest = new ServerRequestBuilder() + .withData(ttxRequest) + .build(); + const builtServerRequests = spec.buildRequests(bidRequests, {}); + + expect(builtServerRequests).to.deep.equal([serverRequest]); + }); + }); + + context('when mediaType has video only and context is instream', function() { + it('builds instream request with default params', function() { + const bidRequests = ( + new BidRequestsBuilder() + .withVideo({context: 'instream'}) + .build() + ); + + const ttxRequest = new TtxRequestBuilder() + .withVideo() + .withProduct('instream') + .build(); + + ttxRequest.imp[0].video.placement = 1; + ttxRequest.imp[0].video.startdelay = 0; + + const serverRequest = new ServerRequestBuilder() + .withData(ttxRequest) + .build(); + const builtServerRequests = spec.buildRequests(bidRequests, {}); + + expect(builtServerRequests).to.deep.equal([serverRequest]); + }); + + it('builds instream request with params passed', function() { + const bidRequests = ( + new BidRequestsBuilder() + .withVideo({context: 'instream', startdelay: -2}) + .build() + ); + + const ttxRequest = new TtxRequestBuilder() + .withVideo({startdelay: -2, placement: 1}) + .withProduct('instream') + .build(); + + const builtServerRequests = spec.buildRequests(bidRequests, {}); + + expect(JSON.parse(builtServerRequests[0].data)).to.deep.equal(ttxRequest); + }); + }); + + context('when mediaType has video only and context is outstream', function() { + it('builds siab request with video only with default params', function() { + const bidRequests = ( + new BidRequestsBuilder() + .withVideo({context: 'outstream'}) + .build() + ); + + const ttxRequest = new TtxRequestBuilder() + .withVideo() + .withProduct('siab') + .build(); + + ttxRequest.imp[0].video.placement = 2; + + const serverRequest = new ServerRequestBuilder() + .withData(ttxRequest) + .build(); + const builtServerRequests = spec.buildRequests(bidRequests, {}); + + expect(builtServerRequests).to.deep.equal([serverRequest]); + }); + + it('builds siab request with video params passed', function() { + const bidRequests = ( + new BidRequestsBuilder() + .withVideo({context: 'outstream', placement: 3, playbackmethod: [2]}) + .build() + ); + + const ttxRequest = new TtxRequestBuilder() + .withVideo({placement: 3, playbackmethod: [2]}) + .withProduct('siab') + .build(); + + const serverRequest = new ServerRequestBuilder() + .withData(ttxRequest) + .build(); + const builtServerRequests = spec.buildRequests(bidRequests, {}); + + expect(builtServerRequests).to.deep.equal([serverRequest]); + }); + }); + + context('when mediaType has banner only', function() { + it('builds default siab request', function() { + const bidRequests = ( + new BidRequestsBuilder() + .withBanner() + .build() + ); + + const ttxRequest = new TtxRequestBuilder() + .withBanner() + .withProduct('siab') + .build(); + + const serverRequest = new ServerRequestBuilder() + .withData(ttxRequest) + .build(); + const builtServerRequests = spec.buildRequests(bidRequests, {}); + + expect(builtServerRequests).to.deep.equal([serverRequest]); + }); + + it('builds default inview request when product is set as such', function() { + const bidRequests = ( + new BidRequestsBuilder() + .withBanner() + .withProduct('inview') + .build() + ); + + const ttxRequest = new TtxRequestBuilder() + .withBanner() + .withProduct('inview') + .build(); + + const serverRequest = new ServerRequestBuilder() + .withData(ttxRequest) + .build(); + const builtServerRequests = spec.buildRequests(bidRequests, {}); + + expect(builtServerRequests).to.deep.equal([serverRequest]); + }); + }); + + context('when mediaType has banner and video', function() { + it('builds siab request with banner and outstream video', function() { + const bidRequests = ( + new BidRequestsBuilder() + .withBanner() + .withVideo({context: 'outstream'}) + .build() + ); + + const ttxRequest = new TtxRequestBuilder() + .withBanner() + .withVideo() + .withProduct('siab') + .build(); + + const serverRequest = new ServerRequestBuilder() + .withData(ttxRequest) + .build(); + const builtServerRequests = spec.buildRequests(bidRequests, {}); + + expect(builtServerRequests).to.deep.equal([serverRequest]); + }); + + it('builds siab request with banner and outstream video even when context is instream', function() { + const bidRequests = ( + new BidRequestsBuilder() + .withBanner() + .withVideo({context: 'instream'}) + .build() + ); + + const ttxRequest = new TtxRequestBuilder() + .withBanner() + .withVideo() + .withProduct('siab') .build(); + ttxRequest.imp[0].video.placement = 2; + const serverRequest = new ServerRequestBuilder() .withData(ttxRequest) .build(); @@ -734,6 +1191,55 @@ describe('33acrossBidAdapter:', function () { expect(builtServerRequests).to.deep.equal([serverRequest]); }); }); + + context('when price floor module is enabled for video in bidRequest', function() { + it('does not set any bidfloors in video if there is no floor', function() { + const bidRequests = ( + new BidRequestsBuilder() + .withVideo({context: 'outstream'}) + .build() + ); + + bidRequests[0].getFloor = () => ({}); + + const ttxRequest = new TtxRequestBuilder() + .withVideo() + .withProduct() + .build(); + + const builtServerRequests = spec.buildRequests(bidRequests, {}); + + expect(JSON.parse(builtServerRequests[0].data)).to.deep.equal(ttxRequest); + }); + + it('sets bidfloors in video if there is a floor', function() { + const bidRequests = ( + new BidRequestsBuilder() + .withVideo({context: 'outstream'}) + .build() + ); + + bidRequests[0].getFloor = ({size, currency, mediaType}) => { + const floor = (mediaType === 'video') ? 1.0 : 0.10 + return ( + { + floor, + currency: 'USD' + } + ); + }; + + const ttxRequest = new TtxRequestBuilder() + .withVideo() + .withProduct() + .withFloors('video', [ 1.0 ]) + .build(); + + const builtServerRequests = spec.buildRequests(bidRequests, {}); + + expect(JSON.parse(builtServerRequests[0].data)).to.deep.equal(ttxRequest); + }); + }); }); describe('interpretResponse', function() { @@ -741,6 +1247,8 @@ describe('33acrossBidAdapter:', function () { beforeEach(function() { ttxRequest = new TtxRequestBuilder() + .withBanner() + .withProduct() .withSite({ id: SITE_ID, page: 'https://test-url.com' @@ -757,7 +1265,7 @@ describe('33acrossBidAdapter:', function () { }); context('when exactly one bid is returned', function() { - it('interprets and returns the single bid response', function() { + it('interprets and returns the single banner bid response', function() { const serverResponse = { cur: 'USD', ext: {}, @@ -784,12 +1292,56 @@ describe('33acrossBidAdapter:', function () { ad: '

I am an ad

', ttl: 60, creativeId: 1, + mediaType: 'banner', currency: 'USD', netRevenue: true }; expect(spec.interpretResponse({ body: serverResponse }, serverRequest)).to.deep.equal([bidResponse]); }); + + it('interprets and returns the single video bid response', function() { + const videoBid = ''; + const serverResponse = { + cur: 'USD', + ext: {}, + id: 'b1', + seatbid: [ + { + bid: [{ + id: '1', + adm: videoBid, + ext: { + ttx: { + mediaType: 'video', + vastType: 'xml' + } + }, + crid: 1, + h: 250, + w: 300, + price: 0.0938 + }] + } + ] + }; + const bidResponse = { + requestId: 'b1', + bidderCode: BIDDER_CODE, + cpm: 0.0938, + width: 300, + height: 250, + ad: videoBid, + ttl: 60, + creativeId: 1, + mediaType: 'video', + currency: 'USD', + netRevenue: true, + vastXml: videoBid + }; + + expect(spec.interpretResponse({ body: serverResponse }, serverRequest)).to.deep.equal([bidResponse]); + }); }); context('when no bids are returned', function() { @@ -852,6 +1404,7 @@ describe('33acrossBidAdapter:', function () { ad: '

I am an ad

', ttl: 60, creativeId: 1, + mediaType: 'banner', currency: 'USD', netRevenue: true }; @@ -886,9 +1439,13 @@ describe('33acrossBidAdapter:', function () { }, adUnitCode: 'div-id', auctionId: 'r1', - sizes: [ - [300, 250] - ], + mediaTypes: { + banner: { + sizes: [ + [300, 250] + ] + } + }, transactionId: 't1' }, { @@ -901,9 +1458,13 @@ describe('33acrossBidAdapter:', function () { }, adUnitCode: 'div-id', auctionId: 'r1', - sizes: [ - [300, 250] - ], + mediaTypes: { + banner: { + sizes: [ + [300, 250] + ] + } + }, transactionId: 't2' } ]; diff --git a/test/spec/modules/ablidaBidAdapter_spec.js b/test/spec/modules/ablidaBidAdapter_spec.js index 0743bf0a896..73109d8cf16 100644 --- a/test/spec/modules/ablidaBidAdapter_spec.js +++ b/test/spec/modules/ablidaBidAdapter_spec.js @@ -75,7 +75,7 @@ describe('ablidaBidAdapter', function () { method: 'POST', url: ENDPOINT_URL, data: { - adapterVersion: 4, + adapterVersion: 5, bidId: '2b8c4de0116e54', categories: undefined, device: 'desktop', diff --git a/test/spec/modules/adWMGBidAdapter_spec.js b/test/spec/modules/adWMGBidAdapter_spec.js new file mode 100644 index 00000000000..5c2364d454c --- /dev/null +++ b/test/spec/modules/adWMGBidAdapter_spec.js @@ -0,0 +1,292 @@ +import { expect } from 'chai'; +import { spec } from 'modules/adWMGBidAdapter.js'; +import { config } from 'src/config.js'; + +describe('adWMGBidAdapter', function () { + describe('isBidRequestValid', function () { + let bid; + beforeEach(function() { + bid = { + bidder: 'adWMG', + params: { + publisherId: '5cebea3c9eea646c7b623d5e' + }, + mediaTypes: { + banner: { + size: [[300, 250]] + } + } + }; + }); + + it('should return true when valid bid request is set', function() { + expect(spec.isBidRequestValid(bid)).to.equal(true); + }); + + it('should return false when bidder is not set to "adWMG"', function() { + bid.bidder = 'bidder'; + expect(spec.isBidRequestValid(bid)).to.equal(false); + }); + + it('should return false when \'publisherId\' param are not set', function() { + delete bid.params.publisherId; + expect(spec.isBidRequestValid(bid)).to.equal(false); + }); + }); + + describe('parseUserAgent', function() { + let ua_desktop, ua_mobile, ua_tv, ua_tablet; + beforeEach(function() { + ua_desktop = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.66 Safari/537.36'; + ua_tv = 'Mozilla/5.0 (Linux; NetCast; U) AppleWebKit/537.31 (KHTML, like Gecko) Chrome/38.0.2125.122 Safari/537.31 SmartTV/7.0'; + ua_mobile = 'Mozilla/5.0 (Linux; Android 7.0; SAMSUNG SM-G610M Build/NRD90M) AppleWebKit/537.36 (KHTML, like Gecko) SamsungBrowser/7.2 Chrome/59.0.3071.125 Mobile Safari/537.36'; + ua_tablet = 'Mozilla/5.0 (iPad; CPU OS 7_1_2 like Mac OS X) AppleWebKit/537.51.2 (KHTML, like Gecko) Version/7.0 Mobile/11D257 Safari/9537.53'; + }); + + it('should return correct device type: desktop', function() { + let userDeviceInfo = spec.parseUserAgent(ua_desktop); + expect(userDeviceInfo.devicetype).to.equal(2); + }); + + it('should return correct device type: TV', function() { + let userDeviceInfo = spec.parseUserAgent(ua_tv); + expect(userDeviceInfo.devicetype).to.equal(3); + }); + + it('should return correct device type: mobile', function() { + let userDeviceInfo = spec.parseUserAgent(ua_mobile); + expect(userDeviceInfo.devicetype).to.equal(4); + }); + + it('should return correct device type: tablet', function() { + let userDeviceInfo = spec.parseUserAgent(ua_tablet); + expect(userDeviceInfo.devicetype).to.equal(5); + }); + + it('should return correct OS name', function() { + let userDeviceInfo = spec.parseUserAgent(ua_desktop); + expect(userDeviceInfo.os).to.equal('Windows'); + }); + + it('should return correct OS version', function() { + let userDeviceInfo = spec.parseUserAgent(ua_desktop); + expect(userDeviceInfo.osv).to.equal('10.0'); + }); + }); + + describe('buildRequests', function () { + let bidRequests; + beforeEach(function() { + bidRequests = [ + { + bidder: 'adWMG', + adUnitCode: 'adwmg-test-ad', + auctionId: 'test-auction-id', + bidId: 'test-bid-id', + bidRequestsCount: 1, + bidderRequestId: 'bidderrequestid123', + transactionId: 'transaction-id-123', + sizes: [[300, 250]], + requestId: 'requestid123', + params: { + floorPrice: 100, + currency: 'USD' + }, + mediaTypes: { + banner: { + size: [[300, 250]] + } + }, + userId: { + pubcid: 'pubc-id-123' + } + }, { + bidder: 'adWMG', + adUnitCode: 'adwmg-test-ad-2', + auctionId: 'test-auction-id-2', + bidId: 'test-bid-id-2', + bidRequestsCount: 1, + bidderRequestId: 'bidderrequestid456', + transactionId: 'transaction-id-456', + sizes: [[320, 50]], + requestId: 'requestid456', + params: { + floorPrice: 100, + currency: 'USD' + }, + mediaTypes: { + banner: { + size: [[320, 50]] + } + }, + userId: { + pubcid: 'pubc-id-456' + } + } + ]; + }); + + let bidderRequest = { + refererInfo: { + referer: 'https://test.com' + }, + gdprConsent: { + consentString: 'CO9rhBTO9rhBTAcABBENBCCsAP_AAH_AACiQHItf_X_fb3_j-_59_9t0eY1f9_7_v20zjgeds-8Nyd_X_L8X42M7vB36pq4KuR4Eu3LBIQdlHOHcTUmw6IkVqTPsbk2Mr7NKJ7PEinMbe2dYGH9_n9XTuZKY79_s___z__-__v__7_f_r-3_3_vp9V---3YHIgEmGpfARZiWOBJNGlUKIEIVxIdACACihGFomsICVwU7K4CP0EDABAagIwIgQYgoxZBAAAAAElEQEgB4IBEARAIAAQAqQEIACNAEFgBIGAQACgGhYARQBCBIQZHBUcpgQESLRQTyVgCUXexhhCGUUANAg4AA.YAAAAAAAAAAA', + vendorData: {}, + gdprApplies: true, + apiVersion: 2 + } + }; + + it('should not contain a sizes when sizes is not set', function() { + delete bidRequests[0].sizes; + delete bidRequests[1].sizes; + let requests = spec.buildRequests(bidRequests, bidderRequest); + expect(JSON.parse(requests[0].data).sizes).to.be.an('undefined'); + expect(JSON.parse(requests[1].data).sizes).to.be.an('undefined'); + }); + + it('should not contain a userId when userId is not set', function() { + delete bidRequests[0].userId; + delete bidRequests[1].userId; + let requests = spec.buildRequests(bidRequests, bidderRequest); + expect(JSON.parse(requests[0].data).userId).to.be.an('undefined'); + expect(JSON.parse(requests[1].data).userId).to.be.an('undefined'); + }); + + it('should have a post method', function() { + let requests = spec.buildRequests(bidRequests, bidderRequest); + expect(requests[0].method).to.equal('POST'); + expect(requests[1].method).to.equal('POST'); + }); + + it('should contain a request id equals to the bid id', function() { + let requests = spec.buildRequests(bidRequests, bidderRequest); + expect(JSON.parse(requests[0].data).requestId).to.equal(bidRequests[0].bidId); + expect(JSON.parse(requests[1].data).requestId).to.equal(bidRequests[1].bidId); + }); + + it('should have an url that match the default endpoint', function() { + let requests = spec.buildRequests(bidRequests, bidderRequest); + expect(requests[0].url).to.equal('https://rtb.adwmg.com/prebid'); + expect(requests[1].url).to.equal('https://rtb.adwmg.com/prebid'); + }); + + it('should contain GDPR consent data if GDPR set', function() { + let requests = spec.buildRequests(bidRequests, bidderRequest); + expect(JSON.parse(requests[0].data).gdpr.applies).to.be.true; + expect(JSON.parse(requests[0].data).gdpr.consentString).to.equal(bidderRequest.gdprConsent.consentString); + expect(JSON.parse(requests[1].data).gdpr.applies).to.be.true; + expect(JSON.parse(requests[1].data).gdpr.consentString).to.equal(bidderRequest.gdprConsent.consentString); + }) + + it('should not contain GDPR consent data if GDPR not set', function() { + delete bidderRequest.gdprConsent; + let requests = spec.buildRequests(bidRequests, bidderRequest); + expect(JSON.parse(requests[0].data).gdpr).to.be.an('undefined'); + expect(JSON.parse(requests[1].data).gdpr).to.be.an('undefined'); + }) + + it('should set debug mode in requests if enabled', function() { + sinon.stub(config, 'getConfig').withArgs('debug').returns(true); + let requests = spec.buildRequests(bidRequests, bidderRequest); + expect(JSON.parse(requests[0].data).debug).to.be.true; + expect(JSON.parse(requests[1].data).debug).to.be.true; + config.getConfig.restore(); + }) + }); + + describe('interpretResponse', function () { + let serverResponse; + beforeEach(function() { + serverResponse = { + body: { + 'requestId': 'request-id', + 'cpm': 100, + 'width': 300, + 'height': 250, + 'ad': '
ad
', + 'ttl': 300, + 'creativeId': 'creative-id', + 'netRevenue': true, + 'currency': 'USD' + } + }; + }); + + it('should return a valid response', () => { + var responses = spec.interpretResponse(serverResponse); + expect(responses).to.be.an('array').that.is.not.empty; + + let response = responses[0]; + expect(response).to.have.all.keys('requestId', 'cpm', 'width', 'height', 'ad', 'ttl', 'creativeId', + 'netRevenue', 'currency'); + expect(response.requestId).to.equal('request-id'); + expect(response.cpm).to.equal(100); + expect(response.width).to.equal(300); + expect(response.height).to.equal(250); + expect(response.ad).to.equal('
ad
'); + expect(response.ttl).to.equal(300); + expect(response.creativeId).to.equal('creative-id'); + expect(response.netRevenue).to.be.true; + expect(response.currency).to.equal('USD'); + }); + + it('should return an empty array when serverResponse is empty', () => { + serverResponse = {}; + var responses = spec.interpretResponse(serverResponse); + expect(responses).to.deep.equal([]); + }); + }); + + describe('getUserSyncs', function () { + it('should return nothing when sync is disabled', function () { + const syncOptions = { + 'iframeEnabled': false, + 'pixelEnabled': false + }; + + let syncs = spec.getUserSyncs(syncOptions); + expect(syncs).to.deep.equal([]); + }); + + it('should register iframe sync when only iframe is enabled', function () { + const syncOptions = { + 'iframeEnabled': true, + 'pixelEnabled': false + }; + + let syncs = spec.getUserSyncs(syncOptions); + expect(syncs[0].type).to.equal('iframe'); + expect(syncs[0].url).includes('https://rtb.adwmg.com/cphb.html?'); + }); + + it('should register iframe sync when iframe and image are enabled', function () { + const syncOptions = { + 'iframeEnabled': true, + 'pixelEnabled': true + }; + + let syncs = spec.getUserSyncs(syncOptions); + expect(syncs[0].type).to.equal('iframe'); + expect(syncs[0].url).includes('https://rtb.adwmg.com/cphb.html?'); + }); + + it('should send GDPR consent if enabled', function() { + const syncOptions = { + 'iframeEnabled': true, + 'pixelEnabled': true + }; + const gdprConsent = { + consentString: 'CO9rhBTO9rhBTAcABBENBCCsAP_AAH_AACiQHItf_X_fb3_j-_59_9t0eY1f9_7_v20zjgeds-8Nyd_X_L8X42M7vB36pq4KuR4Eu3LBIQdlHOHcTUmw6IkVqTPsbk2Mr7NKJ7PEinMbe2dYGH9_n9XTuZKY79_s___z__-__v__7_f_r-3_3_vp9V---3YHIgEmGpfARZiWOBJNGlUKIEIVxIdACACihGFomsICVwU7K4CP0EDABAagIwIgQYgoxZBAAAAAElEQEgB4IBEARAIAAQAqQEIACNAEFgBIGAQACgGhYARQBCBIQZHBUcpgQESLRQTyVgCUXexhhCGUUANAg4AA.YAAAAAAAAAAA', + vendorData: {}, + gdprApplies: true, + apiVersion: 2 + }; + const serverResponse = {}; + let syncs = spec.getUserSyncs(syncOptions, serverResponse, gdprConsent); + expect(syncs[0].url).includes('gdpr=1'); + expect(syncs[0].url).includes(`gdpr_consent=${gdprConsent.consentString}`); + }); + }); +}); diff --git a/test/spec/modules/adagioBidAdapter_spec.js b/test/spec/modules/adagioBidAdapter_spec.js index 52b99f274d5..0a585caaa1a 100644 --- a/test/spec/modules/adagioBidAdapter_spec.js +++ b/test/spec/modules/adagioBidAdapter_spec.js @@ -1,8 +1,19 @@ import find from 'core-js-pure/features/array/find.js'; import { expect } from 'chai'; -import { _features, internal as adagio, adagioScriptFromLocalStorageCb, getAdagioScript, storage, spec, ENDPOINT, VERSION } from '../../../modules/adagioBidAdapter.js'; +import { + _features, + internal as adagio, + adagioScriptFromLocalStorageCb, + getAdagioScript, + storage, + spec, + ENDPOINT, + VERSION, + RENDERER_URL +} from '../../../modules/adagioBidAdapter.js'; import { loadExternalScript } from '../../../src/adloader.js'; import * as utils from '../../../src/utils.js'; +import { config } from 'src/config.js'; const BidRequestBuilder = function BidRequestBuilder(options) { const defaults = { @@ -106,7 +117,7 @@ describe('Adagio bid adapter', () => { adagioMock = sinon.mock(adagio); utilsMock = sinon.mock(utils); - sandbox = sinon.sandbox.create(); + sandbox = sinon.createSandbox(); }); afterEach(() => { @@ -133,6 +144,19 @@ describe('Adagio bid adapter', () => { sinon.assert.callCount(utils.logWarn, 1); }); + it('should use adUnit code for adUnitElementId and placement params', function() { + const bid01 = new BidRequestBuilder({ params: { + organizationId: '1000', + site: 'site-name', + useAdUnitCodeAsPlacement: true, + useAdUnitCodeAsAdUnitElementId: true + }}).build(); + + expect(spec.isBidRequestValid(bid01)).to.equal(true); + expect(bid01.params.adUnitElementId).to.equal('adunit-code'); + expect(bid01.params.placement).to.equal('adunit-code'); + }) + it('should return false when a required param is missing', function() { const bid01 = new BidRequestBuilder({ params: { organizationId: '1000', @@ -218,7 +242,8 @@ describe('Adagio bid adapter', () => { placement: 'PAVE_ATF', site: 'SITE-NAME', adUnitElementId: 'gpt-adunit-code', - environment: 'desktop' + environment: 'desktop', + supportIObs: true } }], auctionId: '4fd1ca2d-846c-4211-b9e5-321dfe1709c9', @@ -238,7 +263,8 @@ describe('Adagio bid adapter', () => { placement: 'PAVE_ATF', site: 'SITE-NAME', adUnitElementId: 'gpt-adunit-code', - environment: 'desktop' + environment: 'desktop', + supportIObs: true } }], auctionId: '4fd1ca2d-846c-4211-b9e5-321dfe1709c9', @@ -249,11 +275,12 @@ describe('Adagio bid adapter', () => { it('should store bids config once by bid in window.top if it accessible', function() { sandbox.stub(adagio, 'getCurrentWindow').returns(window.top); + sandbox.stub(adagio, 'supportIObs').returns(true); // replace by the values defined in beforeEach window.top.ADAGIO = { ...window.ADAGIO - } + }; spec.isBidRequestValid(bid01); spec.isBidRequestValid(bid02); @@ -263,8 +290,22 @@ describe('Adagio bid adapter', () => { expect(find(window.top.ADAGIO.pbjsAdUnits, aU => aU.code === 'adunit-code-02')).to.deep.eql(expected[1]); }); + it('should detect IntersectionObserver support', function() { + sandbox.stub(adagio, 'getCurrentWindow').returns(window.top); + sandbox.stub(adagio, 'supportIObs').returns(false); + + window.top.ADAGIO = { + ...window.ADAGIO + }; + + spec.isBidRequestValid(bid01); + const validBidReq = find(window.top.ADAGIO.pbjsAdUnits, aU => aU.code === 'adunit-code-01'); + expect(validBidReq.bids[0].params.supportIObs).to.equal(false); + }); + it('should store bids config once by bid in current window', function() { sandbox.stub(adagio, 'getCurrentWindow').returns(window.self); + sandbox.stub(adagio, 'supportIObs').returns(true); spec.isBidRequestValid(bid01); spec.isBidRequestValid(bid02); @@ -285,7 +326,8 @@ describe('Adagio bid adapter', () => { 'site', 'pageviewId', 'adUnits', - 'gdpr', + 'regs', + 'user', 'schain', 'prebidVersion', 'adapterVersion', @@ -382,6 +424,81 @@ describe('Adagio bid adapter', () => { expect(requests[0].data.adUnits[0].features.url).to.not.exist; }); + describe('With video mediatype', function() { + context('Outstream video', function() { + it('should logWarn if user does not set renderer.backupOnly: true', function() { + sandbox.spy(utils, 'logWarn'); + const bid01 = new BidRequestBuilder({ + adUnitCode: 'adunit-code-01', + mediaTypes: { + banner: { sizes: [[300, 250]] }, + video: { + context: 'outstream', + playerSize: [[300, 250]], + renderer: { + url: 'https://url.tld', + render: () => true + } + } + }, + }).withParams().build(); + const bidderRequest = new BidderRequestBuilder().build(); + const request = spec.buildRequests([bid01], bidderRequest)[0]; + + expect(request.data.adUnits[0].mediaTypes.video.playerName).to.equal('other'); + sinon.assert.calledWith(utils.logWarn, 'Adagio: renderer.backupOnly has not been set. Adagio recommends to use its own player to get expected behavior.'); + }); + }); + + it('Update mediaTypes.video with OpenRTB options. Validate and sanitize whitelisted OpenRTB', function() { + sandbox.spy(utils, 'logWarn'); + const bid01 = new BidRequestBuilder({ + adUnitCode: 'adunit-code-01', + mediaTypes: { + banner: { sizes: [[300, 250]] }, + video: { + context: 'outstream', + playerSize: [[300, 250]], + mimes: ['video/mp4'], + api: 5, // will be removed because invalid + playbackmethod: [7], // will be removed because invalid + } + }, + }).withParams({ + // options in video, will overide + video: { + skip: 1, + skipafter: 4, + minduration: 10, + maxduration: 30, + placement: [3], + protocols: [8] + } + }).build(); + + const bidderRequest = new BidderRequestBuilder().build(); + const expected = { + context: 'outstream', + playerSize: [[300, 250]], + playerName: 'adagio', + mimes: ['video/mp4'], + skip: 1, + skipafter: 4, + minduration: 10, + maxduration: 30, + placement: [3], + protocols: [8], + w: 300, + h: 250 + }; + + const requests = spec.buildRequests([bid01], bidderRequest); + expect(requests).to.have.lengthOf(1); + expect(requests[0].data.adUnits[0].mediaTypes.video).to.deep.equal(expected); + sinon.assert.calledTwice(utils.logWarn); + }); + }); + describe('with sChain', function() { const schain = { ver: '1.0', @@ -450,7 +567,7 @@ describe('Adagio bid adapter', () => { const requests = spec.buildRequests([bid01], bidderRequest); - expect(requests[0].data.gdpr).to.deep.equal(expected); + expect(requests[0].data.regs.gdpr).to.deep.equal(expected); }); it('send data.gdpr object to the server from TCF v.2 cmp', function() { @@ -466,7 +583,7 @@ describe('Adagio bid adapter', () => { const requests = spec.buildRequests([bid01], bidderRequest); - expect(requests[0].data.gdpr).to.deep.equal(expected); + expect(requests[0].data.regs.gdpr).to.deep.equal(expected); }); }); @@ -485,7 +602,7 @@ describe('Adagio bid adapter', () => { const requests = spec.buildRequests([bid01], bidderRequest); - expect(requests[0].data.gdpr).to.deep.equal(expected); + expect(requests[0].data.regs.gdpr).to.deep.equal(expected); }); it('send data.gdpr object to the server from TCF v.2 cmp', function() { @@ -501,7 +618,7 @@ describe('Adagio bid adapter', () => { const requests = spec.buildRequests([bid01], bidderRequest); - expect(requests[0].data.gdpr).to.deep.equal(expected); + expect(requests[0].data.regs.gdpr).to.deep.equal(expected); }); }); @@ -510,10 +627,115 @@ describe('Adagio bid adapter', () => { const bidderRequest = new BidderRequestBuilder().build(); const requests = spec.buildRequests([bid01], bidderRequest); - expect(requests[0].data.gdpr).to.be.empty; + expect(requests[0].data.regs.gdpr).to.be.empty; }); }); }); + + describe('with COPPA', function() { + const bid01 = new BidRequestBuilder().withParams().build(); + + it('should send the Coppa "required" flag set to "1" in the request', function () { + const bidderRequest = new BidderRequestBuilder().build(); + + sinon.stub(config, 'getConfig') + .withArgs('coppa') + .returns(true); + + const requests = spec.buildRequests([bid01], bidderRequest); + + expect(requests[0].data.regs.coppa.required).to.equal(1); + + config.getConfig.restore(); + }); + }); + + describe('without COPPA', function() { + const bid01 = new BidRequestBuilder().withParams().build(); + + it('should send the Coppa "required" flag set to "0" in the request', function () { + const bidderRequest = new BidderRequestBuilder().build(); + + const requests = spec.buildRequests([bid01], bidderRequest); + + expect(requests[0].data.regs.coppa.required).to.equal(0); + }); + }); + + describe('with USPrivacy', function() { + const bid01 = new BidRequestBuilder().withParams().build(); + + const consent = 'Y11N'; + + it('should send the USPrivacy "ccpa.uspConsent" in the request', function () { + const bidderRequest = new BidderRequestBuilder({ + uspConsent: consent + }).build(); + + const requests = spec.buildRequests([bid01], bidderRequest); + + expect(requests[0].data.regs.ccpa.uspConsent).to.equal(consent); + }); + }); + + describe('without USPrivacy', function() { + const bid01 = new BidRequestBuilder().withParams().build(); + + it('should have an empty "ccpa" field in the request', function () { + const bidderRequest = new BidderRequestBuilder().build(); + + const requests = spec.buildRequests([bid01], bidderRequest); + + expect(requests[0].data.regs.ccpa).to.be.empty; + }); + }); + + describe('with userID modules', function() { + const userId = { + sharedid: {id: '01EAJWWNEPN3CYMM5N8M5VXY22', third: '01EAJWWNEPN3CYMM5N8M5VXY22'}, + unsuported: '666' + }; + + it('should send "user.eids" in the request for Prebid.js supported modules only', function() { + const bid01 = new BidRequestBuilder({ + userId + }).withParams().build(); + + const bidderRequest = new BidderRequestBuilder().build(); + + const requests = spec.buildRequests([bid01], bidderRequest); + + const expected = [{ + source: 'sharedid.org', + uids: [ + { + atype: 1, + ext: { + third: '01EAJWWNEPN3CYMM5N8M5VXY22' + }, + id: '01EAJWWNEPN3CYMM5N8M5VXY22' + } + ] + }]; + + expect(requests[0].data.user.eids).to.have.lengthOf(1); + expect(requests[0].data.user.eids).to.deep.equal(expected); + }); + + it('should send an empty "user.eids" array in the request if userId module is unsupported', function() { + const bid01 = new BidRequestBuilder({ + userId: { + unsuported: '666' + } + }).withParams().build(); + + const bidderRequest = new BidderRequestBuilder().build(); + + const requests = spec.buildRequests([bid01], bidderRequest); + + expect(requests[0].data.user.eids).to.be.empty; + }); + }); }); describe('interpretResponse()', function() { @@ -548,7 +770,8 @@ describe('Adagio bid adapter', () => { pagetype: 'ARTICLE', category: 'NEWS', subcategory: 'SPORT', - environment: 'desktop' + environment: 'desktop', + supportIObs: true }, adUnitCode: 'adunit-code', mediaTypes: { @@ -625,6 +848,37 @@ describe('Adagio bid adapter', () => { utilsMock.verify(); }); + + describe('Response with video outstream', () => { + const bidRequestWithOutstream = utils.deepClone(bidRequest); + bidRequestWithOutstream.data.adUnits[0].mediaTypes.video = { + context: 'outstream', + playerSize: [[300, 250]], + mimes: ['video/mp4'], + skip: true + }; + + const serverResponseWithOutstream = utils.deepClone(serverResponse); + serverResponseWithOutstream.body.bids[0].vastXml = ''; + serverResponseWithOutstream.body.bids[0].mediaType = 'video'; + serverResponseWithOutstream.body.bids[0].outstream = { + bvwUrl: 'https://foo.baz', + impUrl: 'https://foo.bar' + }; + + it('should set a renderer in video outstream context', function() { + const bidResponse = spec.interpretResponse(serverResponseWithOutstream, bidRequestWithOutstream)[0]; + expect(bidResponse).to.have.any.keys('outstream', 'renderer', 'mediaType'); + expect(bidResponse.renderer).to.be.a('object'); + expect(bidResponse.renderer.url).to.equal(RENDERER_URL); + expect(bidResponse.renderer.config.bvwUrl).to.be.ok; + expect(bidResponse.renderer.config.impUrl).to.be.ok; + expect(bidResponse.renderer.loaded).to.not.be.ok; + expect(bidResponse.width).to.equal(300); + expect(bidResponse.height).to.equal(250); + expect(bidResponse.vastUrl).to.match(/^data:text\/xml;/) + }); + }); }); describe('getUserSyncs()', function() { @@ -1088,7 +1342,7 @@ describe('Adagio bid adapter', () => { expect(loadExternalScript.called).to.be.false; expect(localStorage.getItem(ADAGIO_LOCALSTORAGE_KEY)).to.be.null; - }) + }); }); it('should verify valid hash with valid script', function () { diff --git a/test/spec/modules/adformBidAdapter_spec.js b/test/spec/modules/adformBidAdapter_spec.js index 360979659de..23db7a8dc97 100644 --- a/test/spec/modules/adformBidAdapter_spec.js +++ b/test/spec/modules/adformBidAdapter_spec.js @@ -149,6 +149,14 @@ describe('Adform adapter', function () { }); }); + it('should allow to pass custom extended ids', function () { + bids[0].params.eids = 'some_id_value'; + let request = spec.buildRequests(bids); + let eids = parseUrl(request.url).query.eids; + + assert.equal(eids, 'some_id_value'); + }); + describe('user privacy', function () { it('should send GDPR Consent data to adform if gdprApplies', function () { let request = spec.buildRequests([bids[0]], {gdprConsent: {gdprApplies: true, consentString: 'concentDataString'}}); diff --git a/test/spec/modules/adheseBidAdapter_spec.js b/test/spec/modules/adheseBidAdapter_spec.js index aa4872641b4..4d888db269d 100644 --- a/test/spec/modules/adheseBidAdapter_spec.js +++ b/test/spec/modules/adheseBidAdapter_spec.js @@ -116,7 +116,7 @@ describe('AdheseAdapter', function () { }); it('should include id5 id as /x5 param', function () { - let req = spec.buildRequests([ bidWithParams({}, { 'id5id': 'ID5-1234567890' }) ], bidderRequest); + let req = spec.buildRequests([ bidWithParams({}, { 'id5id': { 'uid': 'ID5-1234567890' } }) ], bidderRequest); expect(JSON.parse(req.data).parameters).to.deep.include({ 'x5': [ 'ID5-1234567890' ] }); }); diff --git a/test/spec/modules/adkernelBidAdapter_spec.js b/test/spec/modules/adkernelBidAdapter_spec.js index 87504aa46af..4d3dca7f344 100644 --- a/test/spec/modules/adkernelBidAdapter_spec.js +++ b/test/spec/modules/adkernelBidAdapter_spec.js @@ -175,7 +175,6 @@ describe('Adkernel adapter', function () { dealid: 'deal' }] }], - cur: 'USD', ext: { adk_usersync: [{type: 1, url: 'https://adk.sync.com/sync'}] } @@ -192,7 +191,6 @@ describe('Adkernel adapter', function () { cid: '16855' }] }], - cur: 'USD' }, usersyncOnlyResponse = { id: 'nobid1', ext: { @@ -222,12 +220,18 @@ describe('Adkernel adapter', function () { } }), adomain: ['displayurl.com'], + cat: ['IAB1-4', 'IAB8-16', 'IAB25-5'], cid: '1', - crid: '4' + crid: '4', + ext: { + 'advertiser_id': 777, + 'advertiser_name': 'advertiser', + 'agency_name': 'agency' + } }] }], bidid: 'pTuOlf5KHUo', - cur: 'USD' + cur: 'EUR' }; var sandbox; @@ -552,8 +556,7 @@ describe('Adkernel adapter', function () { describe('adapter configuration', () => { it('should have aliases', () => { - expect(spec.aliases).to.have.lengthOf(6); - expect(spec.aliases).to.include.members(['headbidding', 'adsolut', 'oftmediahb', 'audiencemedia', 'waardex_ak', 'roqoon']); + expect(spec.aliases).to.have.lengthOf(9); }); }); @@ -587,7 +590,13 @@ describe('Adkernel adapter', function () { let resp = spec.interpretResponse({body: nativeResponse}, pbRequests[0])[0]; expect(resp).to.have.property('requestId', 'Bid_01'); expect(resp).to.have.property('cpm', 2.25); - expect(resp).to.have.property('currency', 'USD'); + expect(resp).to.have.property('currency', 'EUR'); + expect(resp).to.have.property('meta'); + expect(resp.meta.advertiserId).to.be.eql(777); + expect(resp.meta.advertiserName).to.be.eql('advertiser'); + expect(resp.meta.agencyName).to.be.eql('agency'); + expect(resp.meta.advertiserDomains).to.be.eql(['displayurl.com']); + expect(resp.meta.secondaryCatIds).to.be.eql(['IAB1-4', 'IAB8-16', 'IAB25-5']); expect(resp).to.have.property('mediaType', NATIVE); expect(resp).to.have.property('native'); expect(resp.native).to.have.property('clickUrl', 'http://rtb.com/click?i=pTuOlf5KHUo_0'); diff --git a/test/spec/modules/admixerBidAdapter_spec.js b/test/spec/modules/admixerBidAdapter_spec.js index 6d2e3059dc8..dfadf1f95d5 100644 --- a/test/spec/modules/admixerBidAdapter_spec.js +++ b/test/spec/modules/admixerBidAdapter_spec.js @@ -3,7 +3,7 @@ import {spec} from 'modules/admixerBidAdapter.js'; import {newBidder} from 'src/adapters/bidderFactory.js'; const BIDDER_CODE = 'admixer'; -const ENDPOINT_URL = 'https://inv-nets.admixer.net/prebid.1.0.aspx'; +const ENDPOINT_URL = 'https://inv-nets.admixer.net/prebid.1.1.aspx'; const ZONE_ID = '2eb6bd58-865c-47ce-af7f-a918108c3fd2'; describe('AdmixerAdapter', function () { @@ -78,33 +78,37 @@ describe('AdmixerAdapter', function () { describe('interpretResponse', function () { let response = { - body: [{ - 'currency': 'USD', - 'cpm': 6.210000, - 'ad': '
ad
', - 'width': 300, - 'height': 600, - 'creativeId': 'ccca3e5e-0c54-4761-9667-771322fbdffc', - 'ttl': 360, - 'netRevenue': false, - 'bidId': '5e4e763b6bc60b' - }] + body: { + ads: [{ + 'currency': 'USD', + 'cpm': 6.210000, + 'ad': '
ad
', + 'width': 300, + 'height': 600, + 'creativeId': 'ccca3e5e-0c54-4761-9667-771322fbdffc', + 'ttl': 360, + 'netRevenue': false, + 'bidId': '5e4e763b6bc60b', + 'dealId': 'asd123', + }] + } }; it('should get correct bid response', function () { - const body = response.body; + const ads = response.body.ads; let expectedResponse = [ { - 'requestId': body[0].bidId, - 'cpm': body[0].cpm, - 'creativeId': body[0].creativeId, - 'width': body[0].width, - 'height': body[0].height, - 'ad': body[0].ad, + 'requestId': ads[0].bidId, + 'cpm': ads[0].cpm, + 'creativeId': ads[0].creativeId, + 'width': ads[0].width, + 'height': ads[0].height, + 'ad': ads[0].ad, 'vastUrl': undefined, - 'currency': body[0].currency, - 'netRevenue': body[0].netRevenue, - 'ttl': body[0].ttl, + 'currency': ads[0].currency, + 'netRevenue': ads[0].netRevenue, + 'ttl': ads[0].ttl, + 'dealId': ads[0].dealId, } ]; @@ -119,4 +123,34 @@ describe('AdmixerAdapter', function () { expect(result.length).to.equal(0); }); }); + + describe('getUserSyncs', function () { + let imgUrl = 'https://example.com/img1'; + let frmUrl = 'https://example.com/frm2'; + let responses = [{ + body: { + cm: { + pixels: [ + imgUrl + ], + iframes: [ + frmUrl + ], + } + } + }]; + + it('Returns valid values', function () { + let userSyncAll = spec.getUserSyncs({pixelEnabled: true, iframeEnabled: true}, responses); + let userSyncImg = spec.getUserSyncs({pixelEnabled: true, iframeEnabled: false}, responses); + let userSyncFrm = spec.getUserSyncs({pixelEnabled: false, iframeEnabled: true}, responses); + expect(userSyncAll).to.be.an('array').with.lengthOf(2); + expect(userSyncImg).to.be.an('array').with.lengthOf(1); + expect(userSyncImg[0].url).to.be.equal(imgUrl); + expect(userSyncImg[0].type).to.be.equal('image'); + expect(userSyncFrm).to.be.an('array').with.lengthOf(1); + expect(userSyncFrm[0].url).to.be.equal(frmUrl); + expect(userSyncFrm[0].type).to.be.equal('iframe'); + }); + }); }); diff --git a/test/spec/modules/adnowBidAdapter_spec.js b/test/spec/modules/adnowBidAdapter_spec.js new file mode 100644 index 00000000000..a8013e3fa04 --- /dev/null +++ b/test/spec/modules/adnowBidAdapter_spec.js @@ -0,0 +1,163 @@ +import { expect } from 'chai'; +import { spec } from 'modules/adnowBidAdapter.js'; + +describe('adnowBidAdapter', function () { + describe('isBidRequestValid', function () { + it('Should return true', function() { + expect(spec.isBidRequestValid({ + bidder: 'adnow', + params: { + codeId: 12345 + } + })).to.equal(true); + }); + + it('Should return false when required params is not passed', function() { + expect(spec.isBidRequestValid({ + bidder: 'adnow', + params: {} + })).to.equal(false); + }); + }); + + describe('buildRequests', function() { + it('Common settings', function() { + const bidRequestData = [{ + bidId: 'bid12345', + params: { + codeId: 12345 + } + }]; + + const req = spec.buildRequests(bidRequestData); + const reqData = req[0].data; + + expect(reqData) + .to.match(/Id=12345/) + .to.match(/mediaType=native/) + .to.match(/out=prebid/) + .to.match(/requestid=bid12345/) + .to.match(/d_user_agent=.+/); + }); + + it('Banner sizes', function () { + const bidRequestData = [{ + bidId: 'bid12345', + params: { + codeId: 12345 + }, + mediaTypes: { + banner: { + sizes: [[300, 250]] + } + } + }]; + + const req = spec.buildRequests(bidRequestData); + const reqData = req[0].data; + + expect(reqData).to.match(/sizes=300x250/); + }); + + it('Native sizes', function () { + const bidRequestData = [{ + bidId: 'bid12345', + params: { + codeId: 12345 + }, + mediaTypes: { + native: { + image: { + sizes: [100, 100] + } + } + } + }]; + + const req = spec.buildRequests(bidRequestData); + const reqData = req[0].data; + + expect(reqData) + .to.match(/width=100/) + .to.match(/height=100/); + }); + }); + + describe('interpretResponse', function() { + const request = { + bidRequest: { + bidId: 'bid12345' + } + }; + + it('Response with native bid', function() { + const response = { + currency: 'USD', + cpm: 0.5, + native: { + title: 'Title', + body: 'Body', + sponsoredBy: 'AdNow', + clickUrl: '//click.url', + image: { + url: '//img.url', + height: 200, + width: 200 + } + }, + meta: { + mediaType: 'native' + } + }; + + const bids = spec.interpretResponse({ body: response }, request); + expect(bids).to.be.an('array').that.is.not.empty; + + const bid = bids[0]; + expect(bid).to.have.keys('requestId', 'cpm', 'currency', 'native', 'creativeId', 'netRevenue', 'meta', 'ttl'); + + const nativePart = bid.native; + + expect(nativePart.title).to.be.equal('Title'); + expect(nativePart.body).to.be.equal('Body'); + expect(nativePart.clickUrl).to.be.equal('//click.url'); + expect(nativePart.image.url).to.be.equal('//img.url'); + expect(nativePart.image.height).to.be.equal(200); + expect(nativePart.image.width).to.be.equal(200); + }); + + it('Response with banner bid', function() { + const response = { + currency: 'USD', + cpm: 0.5, + ad: '
Banner
', + meta: { + mediaType: 'banner' + } + }; + + const bids = spec.interpretResponse({ body: response }, request); + expect(bids).to.be.an('array').that.is.not.empty; + + const bid = bids[0]; + expect(bid).to.have.keys( + 'requestId', 'cpm', 'currency', 'ad', 'creativeId', 'netRevenue', 'meta', 'ttl', 'width', 'height' + ); + + expect(bid.ad).to.be.equal('
Banner
'); + }); + + it('Response with no bid should return an empty array', function() { + const noBidResponses = [ + false, + {}, + {body: false}, + {body: {}} + ]; + + noBidResponses.forEach(response => { + return expect(spec.interpretResponse(response, request)).to.be.an('array').that.is.empty; + }); + }); + }); +}); diff --git a/test/spec/modules/adotBidAdapter_spec.js b/test/spec/modules/adotBidAdapter_spec.js index 594fc4ac7b7..d580cd763a4 100644 --- a/test/spec/modules/adotBidAdapter_spec.js +++ b/test/spec/modules/adotBidAdapter_spec.js @@ -2062,10 +2062,11 @@ describe('Adot Adapter', function () { serverResponse.body.cur = 'USD'; const ads = spec.interpretResponse(serverResponse, serverRequest); + const admWithAuctionPriceReplaced = utils.replaceAuctionPrice(serverResponse.body.seatbid[0].bid[0].adm, serverResponse.body.seatbid[0].bid[0].price); expect(ads).to.be.an('array').and.to.have.length(1); expect(ads[0].requestId).to.exist.and.to.be.a('string').and.to.equal(serverRequest._adot_internal.impressions[0].bidId); - expect(ads[0].ad).to.exist.and.to.be.a('string').and.to.have.string(serverResponse.body.seatbid[0].bid[0].adm); + expect(ads[0].ad).to.exist.and.to.be.a('string').and.to.have.string(admWithAuctionPriceReplaced); expect(ads[0].adUrl).to.equal(null); expect(ads[0].vastXml).to.equal(null); expect(ads[0].vastUrl).to.equal(null); @@ -2087,10 +2088,13 @@ describe('Adot Adapter', function () { const ads = spec.interpretResponse(serverResponse, serverRequest); + const admWithAuctionPriceReplaced = utils.replaceAuctionPrice(serverResponse.body.seatbid[0].bid[0].adm, serverResponse.body.seatbid[0].bid[0].price); + const adm2WithAuctionPriceReplaced = utils.replaceAuctionPrice(serverResponse.body.seatbid[0].bid[1].adm, serverResponse.body.seatbid[0].bid[1].price); + expect(ads).to.be.an('array').and.to.have.length(2); expect(ads[0].requestId).to.exist.and.to.be.a('string').and.to.equal(serverRequest._adot_internal.impressions[0].bidId); - expect(ads[0].ad).to.exist.and.to.be.a('string').and.to.have.string(serverResponse.body.seatbid[0].bid[0].adm); + expect(ads[0].ad).to.exist.and.to.be.a('string').and.to.have.string(admWithAuctionPriceReplaced); expect(ads[0].adUrl).to.equal(null); expect(ads[0].vastXml).to.equal(null); expect(ads[0].vastUrl).to.equal(null); @@ -2104,7 +2108,7 @@ describe('Adot Adapter', function () { expect(ads[0].mediaType).to.exist.and.to.be.a('string').and.to.equal('banner'); expect(ads[0].renderer).to.equal(null); expect(ads[1].requestId).to.exist.and.to.be.a('string').and.to.equal(serverRequest._adot_internal.impressions[1].bidId); - expect(ads[1].ad).to.exist.and.to.be.a('string').and.to.have.string(serverResponse.body.seatbid[0].bid[1].adm); + expect(ads[1].ad).to.exist.and.to.be.a('string').and.to.have.string(adm2WithAuctionPriceReplaced); expect(ads[1].adUrl).to.equal(null); expect(ads[1].creativeId).to.exist.and.to.be.a('string').and.to.equal(serverResponse.body.seatbid[0].bid[1].crid); expect(ads[1].cpm).to.exist.and.to.be.a('number').and.to.equal(serverResponse.body.seatbid[0].bid[1].price); @@ -2592,10 +2596,11 @@ describe('Adot Adapter', function () { const serverResponse = examples.serverResponse_banner; const ads = spec.interpretResponse(serverResponse, serverRequest); + const admWithAuctionPriceReplaced = utils.replaceAuctionPrice(serverResponse.body.seatbid[0].bid[0].adm, serverResponse.body.seatbid[0].bid[0].price); expect(ads).to.be.an('array').and.to.have.length(1); expect(ads[0].requestId).to.exist.and.to.be.a('string').and.to.equal(serverRequest._adot_internal.impressions[0].bidId); - expect(ads[0].ad).to.exist.and.to.be.a('string').and.to.have.string(serverResponse.body.seatbid[0].bid[0].adm); + expect(ads[0].ad).to.exist.and.to.be.a('string').and.to.have.string(admWithAuctionPriceReplaced); expect(ads[0].adUrl).to.equal(null); expect(ads[0].vastXml).to.equal(null); expect(ads[0].vastUrl).to.equal(null); @@ -2617,10 +2622,11 @@ describe('Adot Adapter', function () { serverResponse.body.seatbid[0].bid[0].nurl = undefined; const ads = spec.interpretResponse(serverResponse, serverRequest); + const admWithAuctionPriceReplaced = utils.replaceAuctionPrice(serverResponse.body.seatbid[0].bid[0].adm, serverResponse.body.seatbid[0].bid[0].price); expect(ads).to.be.an('array').and.to.have.length(1); expect(ads[0].requestId).to.exist.and.to.be.a('string').and.to.equal(serverRequest._adot_internal.impressions[0].bidId); - expect(ads[0].ad).to.exist.and.to.be.a('string').and.to.equal(serverResponse.body.seatbid[0].bid[0].adm); + expect(ads[0].ad).to.exist.and.to.be.a('string').and.to.equal(admWithAuctionPriceReplaced); expect(ads[0].adUrl).to.equal(null); expect(ads[0].vastXml).to.equal(null); expect(ads[0].vastUrl).to.equal(null); @@ -2642,11 +2648,12 @@ describe('Adot Adapter', function () { serverResponse.body.seatbid[0].bid[0].adm = undefined; const ads = spec.interpretResponse(serverResponse, serverRequest); + const nurlWithAuctionPriceReplaced = utils.replaceAuctionPrice(serverResponse.body.seatbid[0].bid[0].nurl, serverResponse.body.seatbid[0].bid[0].price); expect(ads).to.be.an('array').and.to.have.length(1); expect(ads[0].requestId).to.exist.and.to.be.a('string').and.to.equal(serverRequest._adot_internal.impressions[0].bidId); expect(ads[0].ad).to.equal(null); - expect(ads[0].adUrl).to.exist.and.to.be.a('string').and.to.equal(serverResponse.body.seatbid[0].bid[0].nurl); + expect(ads[0].adUrl).to.exist.and.to.be.a('string').and.to.equal(nurlWithAuctionPriceReplaced); expect(ads[0].creativeId).to.exist.and.to.be.a('string').and.to.equal(serverResponse.body.seatbid[0].bid[0].crid); expect(ads[0].cpm).to.exist.and.to.be.a('number').and.to.equal(serverResponse.body.seatbid[0].bid[0].price); expect(ads[0].currency).to.exist.and.to.be.a('string').and.to.equal(serverResponse.body.cur); @@ -2721,12 +2728,13 @@ describe('Adot Adapter', function () { const serverResponse = examples.serverResponse_video_instream; const ads = spec.interpretResponse(serverResponse, serverRequest); + const admWithAuctionPriceReplaced = utils.replaceAuctionPrice(serverResponse.body.seatbid[0].bid[0].adm, serverResponse.body.seatbid[0].bid[0].price); expect(ads).to.be.an('array').and.to.have.length(1); expect(ads[0].requestId).to.exist.and.to.be.a('string').and.to.equal(serverRequest._adot_internal.impressions[0].bidId); - expect(ads[0].ad).to.exist.and.to.be.a('string').and.to.have.string(serverResponse.body.seatbid[0].bid[0].adm); + expect(ads[0].ad).to.exist.and.to.be.a('string').and.to.have.string(admWithAuctionPriceReplaced); expect(ads[0].adUrl).to.equal(null); - expect(ads[0].vastXml).to.equal(serverResponse.body.seatbid[0].bid[0].adm); + expect(ads[0].vastXml).to.equal(admWithAuctionPriceReplaced); expect(ads[0].vastUrl).to.equal(null); expect(ads[0].creativeId).to.exist.and.to.be.a('string').and.to.equal(serverResponse.body.seatbid[0].bid[0].crid); expect(ads[0].cpm).to.exist.and.to.be.a('number').and.to.equal(serverResponse.body.seatbid[0].bid[0].price); @@ -2745,12 +2753,13 @@ describe('Adot Adapter', function () { const serverResponse = examples.serverResponse_video_outstream; const ads = spec.interpretResponse(serverResponse, serverRequest); + const admWithAuctionPriceReplaced = utils.replaceAuctionPrice(serverResponse.body.seatbid[0].bid[0].adm, serverResponse.body.seatbid[0].bid[0].price); expect(ads).to.be.an('array').and.to.have.length(1); expect(ads[0].requestId).to.exist.and.to.be.a('string').and.to.equal(serverRequest._adot_internal.impressions[0].bidId); - expect(ads[0].ad).to.exist.and.to.be.a('string').and.to.have.string(serverResponse.body.seatbid[0].bid[0].adm); + expect(ads[0].ad).to.exist.and.to.be.a('string').and.to.have.string(admWithAuctionPriceReplaced); expect(ads[0].adUrl).to.equal(null); - expect(ads[0].vastXml).to.equal(serverResponse.body.seatbid[0].bid[0].adm); + expect(ads[0].vastXml).to.equal(admWithAuctionPriceReplaced); expect(ads[0].vastUrl).to.equal(null); expect(ads[0].creativeId).to.exist.and.to.be.a('string').and.to.equal(serverResponse.body.seatbid[0].bid[0].crid); expect(ads[0].cpm).to.exist.and.to.be.a('number').and.to.equal(serverResponse.body.seatbid[0].bid[0].price); @@ -2769,12 +2778,14 @@ describe('Adot Adapter', function () { const serverResponse = examples.serverResponse_video_instream_outstream; const ads = spec.interpretResponse(serverResponse, serverRequest); + const admWithAuctionPriceReplaced = utils.replaceAuctionPrice(serverResponse.body.seatbid[0].bid[0].adm, serverResponse.body.seatbid[0].bid[0].price); + const adm2WithAuctionPriceReplaced = utils.replaceAuctionPrice(serverResponse.body.seatbid[0].bid[1].adm, serverResponse.body.seatbid[0].bid[1].price); expect(ads).to.be.an('array').and.to.have.length(2); expect(ads[0].requestId).to.exist.and.to.be.a('string').and.to.equal(serverRequest._adot_internal.impressions[0].bidId); - expect(ads[0].ad).to.exist.and.to.be.a('string').and.to.have.string(serverResponse.body.seatbid[0].bid[0].adm); + expect(ads[0].ad).to.exist.and.to.be.a('string').and.to.have.string(admWithAuctionPriceReplaced); expect(ads[0].adUrl).to.equal(null); - expect(ads[0].vastXml).to.equal(serverResponse.body.seatbid[0].bid[0].adm); + expect(ads[0].vastXml).to.equal(admWithAuctionPriceReplaced); expect(ads[0].vastUrl).to.equal(null); expect(ads[0].creativeId).to.exist.and.to.be.a('string').and.to.equal(serverResponse.body.seatbid[0].bid[0].crid); expect(ads[0].cpm).to.exist.and.to.be.a('number').and.to.equal(serverResponse.body.seatbid[0].bid[0].price); @@ -2786,9 +2797,9 @@ describe('Adot Adapter', function () { expect(ads[0].mediaType).to.exist.and.to.be.a('string').and.to.equal('video'); expect(ads[0].renderer).to.equal(null); expect(ads[1].requestId).to.exist.and.to.be.a('string').and.to.equal(serverRequest._adot_internal.impressions[1].bidId); - expect(ads[1].ad).to.exist.and.to.be.a('string').and.to.have.string(serverResponse.body.seatbid[0].bid[1].adm); + expect(ads[1].ad).to.exist.and.to.be.a('string').and.to.have.string(adm2WithAuctionPriceReplaced); expect(ads[1].adUrl).to.equal(null); - expect(ads[1].vastXml).to.equal(serverResponse.body.seatbid[0].bid[0].adm); + expect(ads[1].vastXml).to.equal(adm2WithAuctionPriceReplaced); expect(ads[1].vastUrl).to.equal(null); expect(ads[1].creativeId).to.exist.and.to.be.a('string').and.to.equal(serverResponse.body.seatbid[0].bid[1].crid); expect(ads[1].cpm).to.exist.and.to.be.a('number').and.to.equal(serverResponse.body.seatbid[0].bid[1].price); @@ -2808,12 +2819,13 @@ describe('Adot Adapter', function () { serverResponse.body.seatbid[0].bid[0].nurl = undefined; const ads = spec.interpretResponse(serverResponse, serverRequest); + const admWithAuctionPriceReplaced = utils.replaceAuctionPrice(serverResponse.body.seatbid[0].bid[0].adm, serverResponse.body.seatbid[0].bid[0].price); expect(ads).to.be.an('array').and.to.have.length(1); expect(ads[0].requestId).to.exist.and.to.be.a('string').and.to.equal(serverRequest._adot_internal.impressions[0].bidId); - expect(ads[0].ad).to.exist.and.to.be.a('string').and.to.have.string(serverResponse.body.seatbid[0].bid[0].adm); + expect(ads[0].ad).to.exist.and.to.be.a('string').and.to.have.string(admWithAuctionPriceReplaced); expect(ads[0].adUrl).to.equal(null); - expect(ads[0].vastXml).to.equal(serverResponse.body.seatbid[0].bid[0].adm); + expect(ads[0].vastXml).to.equal(admWithAuctionPriceReplaced); expect(ads[0].vastUrl).to.equal(null); expect(ads[0].creativeId).to.exist.and.to.be.a('string').and.to.equal(serverResponse.body.seatbid[0].bid[0].crid); expect(ads[0].cpm).to.exist.and.to.be.a('number').and.to.equal(serverResponse.body.seatbid[0].bid[0].price); @@ -2833,13 +2845,14 @@ describe('Adot Adapter', function () { serverResponse.body.seatbid[0].bid[0].adm = undefined; const ads = spec.interpretResponse(serverResponse, serverRequest); + const nurlWithAuctionPriceReplaced = utils.replaceAuctionPrice(serverResponse.body.seatbid[0].bid[0].nurl, serverResponse.body.seatbid[0].bid[0].price); expect(ads).to.be.an('array').and.to.have.length(1); expect(ads[0].requestId).to.exist.and.to.be.a('string').and.to.equal(serverRequest._adot_internal.impressions[0].bidId); expect(ads[0].ad).to.equal(null); - expect(ads[0].adUrl).to.exist.and.to.be.a('string').and.to.have.string(serverResponse.body.seatbid[0].bid[0].nurl); + expect(ads[0].adUrl).to.exist.and.to.be.a('string').and.to.have.string(nurlWithAuctionPriceReplaced); expect(ads[0].vastXml).to.equal(null); - expect(ads[0].vastUrl).to.equal(serverResponse.body.seatbid[0].bid[0].nurl); + expect(ads[0].vastUrl).to.equal(nurlWithAuctionPriceReplaced); expect(ads[0].creativeId).to.exist.and.to.be.a('string').and.to.equal(serverResponse.body.seatbid[0].bid[0].crid); expect(ads[0].cpm).to.exist.and.to.be.a('number').and.to.equal(serverResponse.body.seatbid[0].bid[0].price); expect(ads[0].currency).to.exist.and.to.be.a('string').and.to.equal(serverResponse.body.cur); @@ -2858,12 +2871,13 @@ describe('Adot Adapter', function () { serverResponse.body.seatbid[0].bid[0].h = 500; const ads = spec.interpretResponse(serverResponse, serverRequest); + const admWithAuctionPriceReplaced = utils.replaceAuctionPrice(serverResponse.body.seatbid[0].bid[0].adm, serverResponse.body.seatbid[0].bid[0].price); expect(ads).to.be.an('array').and.to.have.length(1); expect(ads[0].requestId).to.exist.and.to.be.a('string').and.to.equal(serverRequest._adot_internal.impressions[0].bidId); - expect(ads[0].ad).to.exist.and.to.be.a('string').and.to.have.string(serverResponse.body.seatbid[0].bid[0].adm); + expect(ads[0].ad).to.exist.and.to.be.a('string').and.to.have.string(admWithAuctionPriceReplaced); expect(ads[0].adUrl).to.equal(null); - expect(ads[0].vastXml).to.equal(serverResponse.body.seatbid[0].bid[0].adm); + expect(ads[0].vastXml).to.equal(admWithAuctionPriceReplaced); expect(ads[0].vastUrl).to.equal(null); expect(ads[0].creativeId).to.exist.and.to.be.a('string').and.to.equal(serverResponse.body.seatbid[0].bid[0].crid); expect(ads[0].cpm).to.exist.and.to.be.a('number').and.to.equal(serverResponse.body.seatbid[0].bid[0].price); @@ -2883,12 +2897,13 @@ describe('Adot Adapter', function () { serverResponse.body.seatbid[0].bid[0].w = 500; const ads = spec.interpretResponse(serverResponse, serverRequest); + const admWithAuctionPriceReplaced = utils.replaceAuctionPrice(serverResponse.body.seatbid[0].bid[0].adm, serverResponse.body.seatbid[0].bid[0].price); expect(ads).to.be.an('array').and.to.have.length(1); expect(ads[0].requestId).to.exist.and.to.be.a('string').and.to.equal(serverRequest._adot_internal.impressions[0].bidId); - expect(ads[0].ad).to.exist.and.to.be.a('string').and.to.have.string(serverResponse.body.seatbid[0].bid[0].adm); + expect(ads[0].ad).to.exist.and.to.be.a('string').and.to.have.string(admWithAuctionPriceReplaced); expect(ads[0].adUrl).to.equal(null); - expect(ads[0].vastXml).to.equal(serverResponse.body.seatbid[0].bid[0].adm); + expect(ads[0].vastXml).to.equal(admWithAuctionPriceReplaced); expect(ads[0].vastUrl).to.equal(null); expect(ads[0].creativeId).to.exist.and.to.be.a('string').and.to.equal(serverResponse.body.seatbid[0].bid[0].crid); expect(ads[0].cpm).to.exist.and.to.be.a('number').and.to.equal(serverResponse.body.seatbid[0].bid[0].price); @@ -2909,12 +2924,13 @@ describe('Adot Adapter', function () { serverResponse.body.seatbid[0].bid[0].h = 400; const ads = spec.interpretResponse(serverResponse, serverRequest); + const admWithAuctionPriceReplaced = utils.replaceAuctionPrice(serverResponse.body.seatbid[0].bid[0].adm, serverResponse.body.seatbid[0].bid[0].price); expect(ads).to.be.an('array').and.to.have.length(1); expect(ads[0].requestId).to.exist.and.to.be.a('string').and.to.equal(serverRequest._adot_internal.impressions[0].bidId); - expect(ads[0].ad).to.exist.and.to.be.a('string').and.to.have.string(serverResponse.body.seatbid[0].bid[0].adm); + expect(ads[0].ad).to.exist.and.to.be.a('string').and.to.have.string(admWithAuctionPriceReplaced); expect(ads[0].adUrl).to.equal(null); - expect(ads[0].vastXml).to.equal(serverResponse.body.seatbid[0].bid[0].adm); + expect(ads[0].vastXml).to.equal(admWithAuctionPriceReplaced); expect(ads[0].vastUrl).to.equal(null); expect(ads[0].creativeId).to.exist.and.to.be.a('string').and.to.equal(serverResponse.body.seatbid[0].bid[0].crid); expect(ads[0].cpm).to.exist.and.to.be.a('number').and.to.equal(serverResponse.body.seatbid[0].bid[0].price); @@ -2934,12 +2950,13 @@ describe('Adot Adapter', function () { const serverResponse = utils.deepClone(examples.serverResponse_video_instream); const ads = spec.interpretResponse(serverResponse, serverRequest); + const admWithAuctionPriceReplaced = utils.replaceAuctionPrice(serverResponse.body.seatbid[0].bid[0].adm, serverResponse.body.seatbid[0].bid[0].price); expect(ads).to.be.an('array').and.to.have.length(1); expect(ads[0].requestId).to.exist.and.to.be.a('string').and.to.equal(serverRequest._adot_internal.impressions[0].bidId); - expect(ads[0].ad).to.exist.and.to.be.a('string').and.to.have.string(serverResponse.body.seatbid[0].bid[0].adm); + expect(ads[0].ad).to.exist.and.to.be.a('string').and.to.have.string(admWithAuctionPriceReplaced); expect(ads[0].adUrl).to.equal(null); - expect(ads[0].vastXml).to.equal(serverResponse.body.seatbid[0].bid[0].adm); + expect(ads[0].vastXml).to.equal(admWithAuctionPriceReplaced); expect(ads[0].vastUrl).to.equal(null); expect(ads[0].creativeId).to.exist.and.to.be.a('string').and.to.equal(serverResponse.body.seatbid[0].bid[0].crid); expect(ads[0].cpm).to.exist.and.to.be.a('number').and.to.equal(serverResponse.body.seatbid[0].bid[0].price); @@ -2959,12 +2976,13 @@ describe('Adot Adapter', function () { const serverResponse = utils.deepClone(examples.serverResponse_video_instream); const ads = spec.interpretResponse(serverResponse, serverRequest); + const admWithAuctionPriceReplaced = utils.replaceAuctionPrice(serverResponse.body.seatbid[0].bid[0].adm, serverResponse.body.seatbid[0].bid[0].price); expect(ads).to.be.an('array').and.to.have.length(1); expect(ads[0].requestId).to.exist.and.to.be.a('string').and.to.equal(serverRequest._adot_internal.impressions[0].bidId); - expect(ads[0].ad).to.exist.and.to.be.a('string').and.to.have.string(serverResponse.body.seatbid[0].bid[0].adm); + expect(ads[0].ad).to.exist.and.to.be.a('string').and.to.have.string(admWithAuctionPriceReplaced); expect(ads[0].adUrl).to.equal(null); - expect(ads[0].vastXml).to.equal(serverResponse.body.seatbid[0].bid[0].adm); + expect(ads[0].vastXml).to.equal(admWithAuctionPriceReplaced); expect(ads[0].vastUrl).to.equal(null); expect(ads[0].creativeId).to.exist.and.to.be.a('string').and.to.equal(serverResponse.body.seatbid[0].bid[0].crid); expect(ads[0].cpm).to.exist.and.to.be.a('number').and.to.equal(serverResponse.body.seatbid[0].bid[0].price); diff --git a/test/spec/modules/adrelevantisBidAdapter_spec.js b/test/spec/modules/adrelevantisBidAdapter_spec.js new file mode 100644 index 00000000000..11a6a14a353 --- /dev/null +++ b/test/spec/modules/adrelevantisBidAdapter_spec.js @@ -0,0 +1,769 @@ +import { expect } from 'chai'; +import { spec } from 'modules/adrelevantisBidAdapter.js'; +import { newBidder } from 'src/adapters/bidderFactory.js'; +import * as bidderFactory from 'src/adapters/bidderFactory.js'; +import { deepClone } from 'src/utils.js'; +import { config } from 'src/config.js'; + +const ENDPOINT = 'https://ssp.adrelevantis.com/prebid'; + +describe('AdrelevantisAdapter', function () { + const adapter = newBidder(spec); + + describe('inherited functions', function () { + it('exists and is a function', function () { + expect(adapter.callBids).to.exist.and.to.be.a('function'); + }); + }); + + describe('isBidRequestValid', function () { + let bid = { + 'bidder': 'adrelevantis', + 'params': { + 'placementId': '10433394' + }, + 'adUnitCode': 'adunit-code', + 'sizes': [[300, 250], [300, 600]], + 'bidId': '30b31c1838de1e', + 'bidderRequestId': '22edbae2733bf6', + 'auctionId': '1d1a030790a475', + }; + + it('should return true when required params found', function () { + expect(spec.isBidRequestValid(bid)).to.equal(true); + }); + + it('should return false when required params are not passed', function () { + let bid = Object.assign({}, bid); + delete bid.params; + bid.params = { + 'placementId': 0 + }; + expect(spec.isBidRequestValid(bid)).to.equal(false); + }); + }); + + describe('buildRequests', function () { + let bidRequests = [ + { + 'bidder': 'adrelevantis', + 'params': { + 'placementId': '10433394' + }, + 'adUnitCode': 'adunit-code', + 'sizes': [[300, 250], [300, 600]], + 'bidId': '30b31c1838de1e', + 'bidderRequestId': '22edbae2733bf6', + 'auctionId': '1d1a030790a475', + } + ]; + + it('should parse out private sizes', function () { + let bidRequest = Object.assign({}, + bidRequests[0], + { + params: { + placementId: '10433394', + privateSizes: [300, 250] + } + } + ); + + const request = spec.buildRequests([bidRequest]); + const payload = JSON.parse(request.data); + + expect(payload.tags[0].private_sizes).to.exist; + expect(payload.tags[0].private_sizes).to.deep.equal([{width: 300, height: 250}]); + }); + + it('should add source and verison to the tag', function () { + const request = spec.buildRequests(bidRequests); + const payload = JSON.parse(request.data); + expect(payload.sdk).to.exist; + expect(payload.sdk).to.deep.equal({ + source: 'pbjs', + version: '$prebid.version$' + }); + }); + + it('should populate the ad_types array on all requests', function () { + ['banner', 'video', 'native'].forEach(type => { + const bidRequest = Object.assign({}, bidRequests[0]); + bidRequest.mediaTypes = {}; + bidRequest.mediaTypes[type] = {}; + + const request = spec.buildRequests([bidRequest]); + const payload = JSON.parse(request.data); + + expect(payload.tags[0].ad_types).to.deep.equal([type]); + }); + }); + + it('should populate the ad_types array on outstream requests', function () { + const bidRequest = Object.assign({}, bidRequests[0]); + bidRequest.mediaTypes = {}; + bidRequest.mediaTypes.video = {context: 'outstream'}; + + const request = spec.buildRequests([bidRequest]); + const payload = JSON.parse(request.data); + + expect(payload.tags[0].ad_types).to.deep.equal(['video']); + }); + + it('sends bid request to ENDPOINT via POST', function () { + const request = spec.buildRequests(bidRequests); + expect(request.url).to.equal(ENDPOINT); + expect(request.method).to.equal('POST'); + }); + + it('should attach valid video params to the tag', function () { + let bidRequest = Object.assign({}, + bidRequests[0], + { + params: { + placementId: '10433394', + video: { + id: 123, + minduration: 100, + foobar: 'invalid' + } + } + } + ); + + const request = spec.buildRequests([bidRequest]); + const payload = JSON.parse(request.data); + expect(payload.tags[0].video).to.deep.equal({ + id: 123, + minduration: 100 + }); + }); + + it('should add video property when adUnit includes a renderer', function () { + const videoData = { + mediaTypes: { + video: { + context: 'outstream', + mimes: ['video/mp4'] + } + }, + params: { + placementId: '10433394', + video: { + skippable: true, + playback_method: ['auto_play_sound_off'] + } + } + }; + + let bidRequest1 = deepClone(bidRequests[0]); + bidRequest1 = Object.assign({}, bidRequest1, videoData, { + renderer: { + url: 'http://test.renderer.url', + render: function () {} + } + }); + + let bidRequest2 = deepClone(bidRequests[0]); + bidRequest2.adUnitCode = 'adUnit_code_2'; + bidRequest2 = Object.assign({}, bidRequest2, videoData); + + const request = spec.buildRequests([bidRequest1, bidRequest2]); + const payload = JSON.parse(request.data); + expect(payload.tags[0].video).to.deep.equal({ + skippable: true, + playback_method: ['auto_play_sound_off'], + custom_renderer_present: true + }); + expect(payload.tags[1].video).to.deep.equal({ + skippable: true, + playback_method: ['auto_play_sound_off'] + }); + }); + + it('should attach valid user params to the tag', function () { + let bidRequest = Object.assign({}, + bidRequests[0], + { + params: { + placementId: '10433394', + user: { + externalUid: '123', + foobar: 'invalid' + } + } + } + ); + + const request = spec.buildRequests([bidRequest]); + const payload = JSON.parse(request.data); + + expect(payload.user).to.exist; + expect(payload.user).to.deep.equal({ + externalUid: '123', + }); + }); + + it('should contain hb_source value for other media', function() { + let bidRequest = Object.assign({}, + bidRequests[0], + { + mediaType: 'banner', + params: { + sizes: [[300, 250], [300, 600]], + placementId: 10433394 + } + } + ); + const request = spec.buildRequests([bidRequest]); + const payload = JSON.parse(request.data); + expect(payload.tags[0].hb_source).to.deep.equal(1); + }); + + it('adds context data (category and keywords) to request when set', function() { + let bidRequest = Object.assign({}, bidRequests[0]); + sinon + .stub(config, 'getConfig') + .withArgs('fpd') + .returns({ + context: { + keywords: 'US Open', + data: { + category: 'sports/tennis' + } + } + }); + + const request = spec.buildRequests([bidRequest]); + const payload = JSON.parse(request.data); + + expect(payload.fpd.keywords).to.equal('US Open'); + expect(payload.fpd.category).to.equal('sports/tennis'); + + config.getConfig.restore(); + }); + + it('should attach native params to the request', function () { + let bidRequest = Object.assign({}, + bidRequests[0], + { + mediaType: 'native', + nativeParams: { + title: {required: true}, + body: {required: true}, + body2: {required: true}, + image: {required: true, sizes: [100, 100]}, + icon: {required: true}, + cta: {required: false}, + rating: {required: true}, + sponsoredBy: {required: true}, + privacyLink: {required: true}, + displayUrl: {required: true}, + address: {required: true}, + downloads: {required: true}, + likes: {required: true}, + phone: {required: true}, + price: {required: true}, + salePrice: {required: true} + } + } + ); + + const request = spec.buildRequests([bidRequest]); + const payload = JSON.parse(request.data); + + expect(payload.tags[0].native.layouts[0]).to.deep.equal({ + title: {required: true}, + description: {required: true}, + desc2: {required: true}, + main_image: {required: true, sizes: [{ width: 100, height: 100 }]}, + icon: {required: true}, + ctatext: {required: false}, + rating: {required: true}, + sponsored_by: {required: true}, + privacy_link: {required: true}, + displayurl: {required: true}, + address: {required: true}, + downloads: {required: true}, + likes: {required: true}, + phone: {required: true}, + price: {required: true}, + saleprice: {required: true}, + privacy_supported: true + }); + expect(payload.tags[0].hb_source).to.equal(1); + }); + + it('should always populated tags[].sizes with 1,1 for native if otherwise not defined', function () { + let bidRequest = Object.assign({}, + bidRequests[0], + { + mediaType: 'native', + nativeParams: { + image: { required: true } + } + } + ); + bidRequest.sizes = [[150, 100], [300, 250]]; + + let request = spec.buildRequests([bidRequest]); + let payload = JSON.parse(request.data); + expect(payload.tags[0].sizes).to.deep.equal([{width: 150, height: 100}, {width: 300, height: 250}]); + + delete bidRequest.sizes; + + request = spec.buildRequests([bidRequest]); + payload = JSON.parse(request.data); + + expect(payload.tags[0].sizes).to.deep.equal([{width: 1, height: 1}]); + }); + + it('should convert keyword params to proper form and attaches to request', function () { + let bidRequest = Object.assign({}, + bidRequests[0], + { + params: { + placementId: '10433394', + keywords: { + single: 'val', + singleArr: ['val'], + singleArrNum: [5], + multiValMixed: ['value1', 2, 'value3'], + singleValNum: 123, + emptyStr: '', + emptyArr: [''], + badValue: {'foo': 'bar'} // should be dropped + } + } + } + ); + + const request = spec.buildRequests([bidRequest]); + const payload = JSON.parse(request.data); + + expect(payload.tags[0].keywords).to.deep.equal([{ + 'key': 'single', + 'value': ['val'] + }, { + 'key': 'singleArr', + 'value': ['val'] + }, { + 'key': 'singleArrNum', + 'value': ['5'] + }, { + 'key': 'multiValMixed', + 'value': ['value1', '2', 'value3'] + }, { + 'key': 'singleValNum', + 'value': ['123'] + }, { + 'key': 'emptyStr' + }, { + 'key': 'emptyArr' + }]); + }); + + it('should add payment rules to the request', function () { + let bidRequest = Object.assign({}, + bidRequests[0], + { + params: { + placementId: '10433394', + usePaymentRule: true + } + } + ); + + const request = spec.buildRequests([bidRequest]); + const payload = JSON.parse(request.data); + + expect(payload.tags[0].use_pmt_rule).to.equal(true); + }); + + it('should add gdpr consent information to the request', function () { + let consentString = 'BOJ8RZsOJ8RZsABAB8AAAAAZ+A=='; + let bidderRequest = { + 'bidderCode': 'adrelevantis', + 'auctionId': '1d1a030790a475', + 'bidderRequestId': '22edbae2733bf6', + 'timeout': 3000, + 'gdprConsent': { + consentString: consentString, + gdprApplies: true + } + }; + bidderRequest.bids = bidRequests; + + const request = spec.buildRequests(bidRequests, bidderRequest); + const payload = JSON.parse(request.data); + + expect(payload.gdpr_consent).to.exist; + expect(payload.gdpr_consent.consent_string).to.exist.and.to.equal(consentString); + expect(payload.gdpr_consent.consent_required).to.exist.and.to.be.true; + }); + + it('supports sending hybrid mobile app parameters', function () { + let appRequest = Object.assign({}, + bidRequests[0], + { + params: { + placementId: '10433394', + app: { + id: 'B1O2W3M4AN.com.prebid.webview', + geo: { + lat: 40.0964439, + lng: -75.3009142 + }, + device_id: { + idfa: '4D12078D-3246-4DA4-AD5E-7610481E7AE', // Apple advertising identifier + aaid: '38400000-8cf0-11bd-b23e-10b96e40000d', // Android advertising identifier + md5udid: '5756ae9022b2ea1e47d84fead75220c8', // MD5 hash of the ANDROID_ID + sha1udid: '4DFAA92388699AC6539885AEF1719293879985BF', // SHA1 hash of the ANDROID_ID + windowsadid: '750c6be243f1c4b5c9912b95a5742fc5' // Windows advertising identifier + } + } + } + } + ); + const request = spec.buildRequests([appRequest]); + const payload = JSON.parse(request.data); + expect(payload.app).to.exist; + expect(payload.app).to.deep.equal({ + appid: 'B1O2W3M4AN.com.prebid.webview' + }); + expect(payload.device.device_id).to.exist; + expect(payload.device.device_id).to.deep.equal({ + aaid: '38400000-8cf0-11bd-b23e-10b96e40000d', + idfa: '4D12078D-3246-4DA4-AD5E-7610481E7AE', + md5udid: '5756ae9022b2ea1e47d84fead75220c8', + sha1udid: '4DFAA92388699AC6539885AEF1719293879985BF', + windowsadid: '750c6be243f1c4b5c9912b95a5742fc5' + }); + expect(payload.device.geo).to.exist; + expect(payload.device.geo).to.deep.equal({ + lat: 40.0964439, + lng: -75.3009142 + }); + }); + + it('should add referer info to payload', function () { + const bidRequest = Object.assign({}, bidRequests[0]) + const bidderRequest = { + refererInfo: { + referer: 'http://example.com/page.html', + reachedTop: true, + numIframes: 2, + stack: [ + 'http://example.com/page.html', + 'http://example.com/iframe1.html', + 'http://example.com/iframe2.html' + ] + } + } + const request = spec.buildRequests([bidRequest], bidderRequest); + const payload = JSON.parse(request.data); + + expect(payload.referrer_detection).to.exist; + expect(payload.referrer_detection).to.deep.equal({ + rd_ref: 'http%3A%2F%2Fexample.com%2Fpage.html', + rd_top: true, + rd_ifs: 2, + rd_stk: bidderRequest.refererInfo.stack.map((url) => encodeURIComponent(url)).join(',') + }); + }); + + it('should populate coppa if set in config', function () { + let bidRequest = Object.assign({}, bidRequests[0]); + sinon.stub(config, 'getConfig') + .withArgs('coppa') + .returns(true); + + const request = spec.buildRequests([bidRequest]); + const payload = JSON.parse(request.data); + + expect(payload.user.coppa).to.equal(true); + + config.getConfig.restore(); + }); + }) + + describe('interpretResponse', function () { + let bfStub; + before(function() { + bfStub = sinon.stub(bidderFactory, 'getIabSubCategory'); + }); + + after(function() { + bfStub.restore(); + }); + + let response = { + 'version': '3.0.0', + 'tags': [ + { + 'uuid': '3db3773286ee59', + 'tag_id': 10433394, + 'auction_id': '4534722592064951574', + 'nobid': false, + 'no_ad_url': 'http://lax1-ib.adnxs.com/no-ad', + 'timeout_ms': 10000, + 'ad_profile_id': 27079, + 'ads': [ + { + 'content_source': 'rtb', + 'ad_type': 'banner', + 'buyer_member_id': 958, + 'creative_id': 29681110, + 'media_type_id': 1, + 'media_subtype_id': 1, + 'cpm': 0.5, + 'cpm_publisher_currency': 0.5, + 'publisher_currency_code': '$', + 'client_initiated_ad_counting': true, + 'viewability': { + 'config': '' + }, + 'rtb': { + 'banner': { + 'content': '', + 'width': 300, + 'height': 250 + }, + 'trackers': [ + { + 'impression_urls': [ + 'http://lax1-ib.adnxs.com/impression' + ], + 'video_events': {} + } + ] + } + } + ] + } + ] + }; + + it('should get correct bid response', function () { + let expectedResponse = [ + { + 'requestId': '3db3773286ee59', + 'cpm': 0.5, + 'creativeId': 29681110, + 'dealId': undefined, + 'width': 300, + 'height': 250, + 'ad': '', + 'mediaType': 'banner', + 'currency': 'USD', + 'ttl': 300, + 'netRevenue': true, + 'adUnitCode': 'code', + 'adrelevantis': { + 'buyerMemberId': 958 + } + } + ]; + let bidderRequest = { + bids: [{ + bidId: '3db3773286ee59', + adUnitCode: 'code' + }] + } + let result = spec.interpretResponse({ body: response }, {bidderRequest}); + expect(Object.keys(result[0])).to.have.members(Object.keys(expectedResponse[0])); + }); + + it('handles nobid responses', function () { + let response = { + 'version': '0.0.1', + 'tags': [{ + 'uuid': '84ab500420319d', + 'tag_id': 5976557, + 'auction_id': '297492697822162468', + 'nobid': true + }] + }; + let bidderRequest; + + let result = spec.interpretResponse({ body: response }, {bidderRequest}); + expect(result.length).to.equal(0); + }); + + it('handles outstream video responses', function () { + let response = { + 'tags': [{ + 'uuid': '84ab500420319d', + 'ads': [{ + 'ad_type': 'video', + 'cpm': 0.500000, + 'notify_url': 'imptracker.com', + 'rtb': { + 'video': { + 'content': '' + } + }, + 'javascriptTrackers': '' + }] + }] + }; + let bidderRequest = { + bids: [{ + bidId: '84ab500420319d', + adUnitCode: 'code', + mediaTypes: { + video: { + context: 'outstream' + } + } + }] + } + + let result = spec.interpretResponse({ body: response }, {bidderRequest}); + expect(result[0]).to.have.property('vastXml'); + expect(result[0]).to.have.property('vastImpUrl'); + expect(result[0]).to.have.property('mediaType', 'video'); + }); + + it('handles instream video responses', function () { + let response = { + 'tags': [{ + 'uuid': '84ab500420319d', + 'ads': [{ + 'ad_type': 'video', + 'cpm': 0.500000, + 'notify_url': 'imptracker.com', + 'rtb': { + 'video': { + 'asset_url': 'https://sample.vastURL.com/here/vid' + } + }, + 'javascriptTrackers': '' + }] + }] + }; + let bidderRequest = { + bids: [{ + bidId: '84ab500420319d', + adUnitCode: 'code', + mediaTypes: { + video: { + context: 'instream' + } + } + }] + } + + let result = spec.interpretResponse({ body: response }, {bidderRequest}); + expect(result[0]).to.have.property('vastUrl'); + expect(result[0]).to.have.property('vastImpUrl'); + expect(result[0]).to.have.property('mediaType', 'video'); + }); + + it('handles native responses', function () { + let response1 = deepClone(response); + response1.tags[0].ads[0].ad_type = 'native'; + response1.tags[0].ads[0].rtb.native = { + 'title': 'Native Creative', + 'desc': 'Cool description great stuff', + 'desc2': 'Additional body text', + 'ctatext': 'Do it', + 'sponsored': 'AppNexus', + 'icon': { + 'width': 0, + 'height': 0, + 'url': 'https://cdn.adnxs.com/icon.png' + }, + 'main_img': { + 'width': 2352, + 'height': 1516, + 'url': 'https://cdn.adnxs.com/img.png' + }, + 'link': { + 'url': 'https://www.appnexus.com', + 'fallback_url': '', + 'click_trackers': ['https://nym1-ib.adnxs.com/click'] + }, + 'impression_trackers': ['https://example.com'], + 'rating': '5', + 'displayurl': 'https://AppNexus.com/?url=display_url', + 'likes': '38908320', + 'downloads': '874983', + 'price': '9.99', + 'saleprice': 'FREE', + 'phone': '1234567890', + 'address': '28 W 23rd St, New York, NY 10010', + 'privacy_link': 'https://appnexus.com/?url=privacy_url', + 'javascriptTrackers': '' + }; + let bidderRequest = { + bids: [{ + bidId: '3db3773286ee59', + adUnitCode: 'code' + }] + } + + let result = spec.interpretResponse({ body: response1 }, {bidderRequest}); + expect(result[0].native.title).to.equal('Native Creative'); + expect(result[0].native.body).to.equal('Cool description great stuff'); + expect(result[0].native.cta).to.equal('Do it'); + expect(result[0].native.image.url).to.equal('https://cdn.adnxs.com/img.png'); + }); + + it('supports configuring outstream renderers', function () { + const outstreamResponse = deepClone(response); + outstreamResponse.tags[0].ads[0].rtb.video = {}; + outstreamResponse.tags[0].ads[0].renderer_url = 'renderer.js'; + + const bidderRequest = { + bids: [{ + bidId: '3db3773286ee59', + renderer: { + options: { + adText: 'configured' + } + }, + mediaTypes: { + video: { + context: 'outstream' + } + } + }] + }; + + const result = spec.interpretResponse({ body: outstreamResponse }, {bidderRequest}); + expect(result[0].renderer.config).to.deep.equal( + bidderRequest.bids[0].renderer.options + ); + }); + + it('should add deal_priority and deal_code', function() { + let responseWithDeal = deepClone(response); + responseWithDeal.tags[0].ads[0].deal_priority = 'high'; + responseWithDeal.tags[0].ads[0].deal_code = '123'; + + let bidderRequest = { + bids: [{ + bidId: '3db3773286ee59', + adUnitCode: 'code' + }] + } + let result = spec.interpretResponse({ body: responseWithDeal }, {bidderRequest}); + expect(Object.keys(result[0].adrelevantis)).to.include.members(['buyerMemberId', 'dealPriority', 'dealCode']); + }); + + it('should add advertiser id', function() { + let responseAdvertiserId = deepClone(response); + responseAdvertiserId.tags[0].ads[0].advertiser_id = '123'; + + let bidderRequest = { + bids: [{ + bidId: '3db3773286ee59', + adUnitCode: 'code' + }] + } + let result = spec.interpretResponse({ body: responseAdvertiserId }, {bidderRequest}); + expect(Object.keys(result[0].meta)).to.include.members(['advertiserId']); + }) + }); +}); diff --git a/test/spec/modules/adtelligentBidAdapter_spec.js b/test/spec/modules/adtelligentBidAdapter_spec.js index 9c694668703..5c1e4a38d03 100644 --- a/test/spec/modules/adtelligentBidAdapter_spec.js +++ b/test/spec/modules/adtelligentBidAdapter_spec.js @@ -2,6 +2,7 @@ import { expect } from 'chai'; import { spec } from 'modules/adtelligentBidAdapter.js'; import { newBidder } from 'src/adapters/bidderFactory.js'; import { config } from 'src/config.js'; +import { deepClone } from 'src/utils.js'; const EXPECTED_ENDPOINTS = [ 'https://ghb.adtelligent.com/v2/auction/', @@ -9,7 +10,14 @@ const EXPECTED_ENDPOINTS = [ 'https://ghb2.adtelligent.com/v2/auction/', 'https://ghb.adtelligent.com/v2/auction/' ]; +const aliasEP = { + appaloosa: 'https://ghb.hb.appaloosa.media/v2/auction/', + appaloosa_publisherSuffix: 'https://ghb.hb.appaloosa.media/v2/auction/', + onefiftytwomedia: 'https://ghb.ads.152media.com/v2/auction/', + mediafuse: 'https://ghb.hbmp.mediafuse.com/v2/auction/' +}; +const DEFAULT_ADATPER_REQ = { bidderCode: 'adtelligent' }; const DISPLAY_REQUEST = { 'bidder': 'adtelligent', 'params': { @@ -241,15 +249,24 @@ describe('adtelligentBidAdapter', () => { let videoBidRequests = [VIDEO_REQUEST]; let displayBidRequests = [DISPLAY_REQUEST]; let videoAndDisplayBidRequests = [DISPLAY_REQUEST, VIDEO_REQUEST]; - const displayRequest = spec.buildRequests(displayBidRequests, {}); - const videoRequest = spec.buildRequests(videoBidRequests, {}); - const videoAndDisplayRequests = spec.buildRequests(videoAndDisplayBidRequests, {}); - const rotatingRequest = spec.buildRequests(displayBidRequests, {}); + const displayRequest = spec.buildRequests(displayBidRequests, DEFAULT_ADATPER_REQ); + const videoRequest = spec.buildRequests(videoBidRequests, DEFAULT_ADATPER_REQ); + const videoAndDisplayRequests = spec.buildRequests(videoAndDisplayBidRequests, DEFAULT_ADATPER_REQ); + const rotatingRequest = spec.buildRequests(displayBidRequests, DEFAULT_ADATPER_REQ); it('rotates endpoints', () => { const bidReqUrls = [displayRequest[0], videoRequest[0], videoAndDisplayRequests[0], rotatingRequest[0]].map(br => br.url); expect(bidReqUrls).to.deep.equal(EXPECTED_ENDPOINTS); }) + it('makes correct host for aliases', () => { + for (const alias in aliasEP) { + const bidReq = deepClone(DISPLAY_REQUEST) + bidReq.bidder = alias; + const [bidderRequest] = spec.buildRequests([bidReq], { bidderCode: alias }); + expect(bidderRequest.url).to.equal(aliasEP[alias]); + } + }) + it('building requests as arrays', () => { expect(videoRequest).to.be.a('array'); expect(displayRequest).to.be.a('array'); @@ -264,7 +281,7 @@ describe('adtelligentBidAdapter', () => { expect(videoAndDisplayRequests.every(comparator)).to.be.true; }); it('forms correct ADPOD request', () => { - const pbBidReqData = spec.buildRequests([ADPOD_REQUEST], {})[0].data; + const pbBidReqData = spec.buildRequests([ADPOD_REQUEST], DEFAULT_ADATPER_REQ)[0].data; const impRequest = pbBidReqData.BidRequests[0] expect(impRequest.AdType).to.be.equal('video'); expect(impRequest.Adpod).to.be.a('object'); diff --git a/test/spec/modules/adxcgBidAdapter_spec.js b/test/spec/modules/adxcgBidAdapter_spec.js index 306914960c3..f9aaea308a7 100644 --- a/test/spec/modules/adxcgBidAdapter_spec.js +++ b/test/spec/modules/adxcgBidAdapter_spec.js @@ -281,7 +281,7 @@ describe('AdxcgAdapter', function () { let bid = deepClone([bidBanner]); let bidderRequests = {}; - bid[0].userId = {id5id: 'id5idsample'}; + bid[0].userId = {id5id: {uid: 'id5idsample'}}; it('should send pubcid if available', function () { let request = spec.buildRequests(bid, bidderRequests); diff --git a/test/spec/modules/adyoulikeBidAdapter_spec.js b/test/spec/modules/adyoulikeBidAdapter_spec.js index d2d4e10c17f..ec8f2f00923 100644 --- a/test/spec/modules/adyoulikeBidAdapter_spec.js +++ b/test/spec/modules/adyoulikeBidAdapter_spec.js @@ -306,6 +306,7 @@ describe('Adyoulike Adapter', function () { expect(request.method).to.equal('POST'); expect(request.url).to.contains('CanonicalUrl=' + encodeURIComponent(canonicalUrl)); expect(request.url).to.contains('RefererUrl=' + encodeURIComponent(referrerUrl)); + expect(request.url).to.contains('PublisherDomain=http%3A%2F%2Flocalhost%3A9876'); expect(payload.Version).to.equal('1.0'); expect(payload.Bids['bid_id_0'].PlacementID).to.be.equal('placement_0'); diff --git a/test/spec/modules/amxBidAdapter_spec.js b/test/spec/modules/amxBidAdapter_spec.js index 4b21244501d..766045b0f3e 100644 --- a/test/spec/modules/amxBidAdapter_spec.js +++ b/test/spec/modules/amxBidAdapter_spec.js @@ -189,6 +189,45 @@ describe('AmxBidAdapter', () => { expect(data.tm).to.equal(true); }); + it('if prebid is in an iframe, will use the frame url as domain, if the topmost is not avialable', () => { + const { data } = spec.buildRequests([sampleBidRequestBase], { + ...sampleBidderRequest, + refererInfo: { + numIframes: 1, + referer: 'http://search-traffic-source.com', + stack: [] + } + }); + expect(data.do).to.equal('localhost') + expect(data.re).to.equal('http://search-traffic-source.com'); + }); + + it('if we are in AMP, make sure we use the canonical URL or the referrer (which is sourceUrl)', () => { + const { data } = spec.buildRequests([sampleBidRequestBase], { + ...sampleBidderRequest, + refererInfo: { + isAmp: true, + referer: 'http://real-publisher-site.com/content', + stack: [] + } + }); + expect(data.do).to.equal('real-publisher-site.com') + expect(data.re).to.equal('http://real-publisher-site.com/content'); + }) + + it('if prebid is in an iframe, will use the topmost url as domain', () => { + const { data } = spec.buildRequests([sampleBidRequestBase], { + ...sampleBidderRequest, + refererInfo: { + numIframes: 1, + referer: 'http://search-traffic-source.com', + stack: ['http://top-site.com', 'http://iframe.com'] + } + }); + expect(data.do).to.equal('top-site.com'); + expect(data.re).to.equal('http://search-traffic-source.com'); + }); + it('handles referer data and GDPR, USP Consent, COPPA', () => { const { data } = spec.buildRequests([sampleBidRequestBase], sampleBidderRequest); delete data.m; // don't deal with "m" in this test @@ -224,7 +263,7 @@ describe('AmxBidAdapter', () => { britepoolid: 'sample-britepool', criteoId: 'sample-criteo', digitrustid: {data: {id: 'sample-digitrust'}}, - id5id: 'sample-id5', + id5id: {uid: 'sample-id5'}, idl_env: 'sample-liveramp', lipb: {lipbid: 'sample-liveintent'}, netId: 'sample-netid', diff --git a/test/spec/modules/aolBidAdapter_spec.js b/test/spec/modules/aolBidAdapter_spec.js index dd10a57bbfe..8e74e19f420 100644 --- a/test/spec/modules/aolBidAdapter_spec.js +++ b/test/spec/modules/aolBidAdapter_spec.js @@ -1,7 +1,7 @@ import {expect} from 'chai'; import * as utils from 'src/utils.js'; import {spec} from 'modules/aolBidAdapter.js'; -import {config} from 'src/config.js'; +import {createEidsArray} from '../../../modules/userId/eids.js'; const DEFAULT_AD_CONTENT = ''; @@ -80,6 +80,33 @@ describe('AolAdapter', function () { const NEXAGE_URL = 'https://c2shb.ssp.yahoo.com/bidRequest?'; const ONE_DISPLAY_TTL = 60; const ONE_MOBILE_TTL = 3600; + const SUPPORTED_USER_ID_SOURCES = { + 'adserver.org': '100', + 'criteo.com': '200', + 'id5-sync.com': '300', + 'intentiq.com': '400', + 'liveintent.com': '500', + 'quantcast.com': '600', + 'verizonmedia.com': '700', + 'liveramp.com': '800' + }; + + const USER_ID_DATA = { + criteoId: SUPPORTED_USER_ID_SOURCES['criteo.com'], + connectid: SUPPORTED_USER_ID_SOURCES['verizonmedia.com'], + idl_env: SUPPORTED_USER_ID_SOURCES['liveramp.com'], + lipb: { + lipbid: SUPPORTED_USER_ID_SOURCES['liveintent.com'], + segments: ['100', '200'] + }, + tdid: SUPPORTED_USER_ID_SOURCES['adserver.org'], + id5id: { + uid: SUPPORTED_USER_ID_SOURCES['id5-sync.com'], + ext: {foo: 'bar'} + }, + intentIqId: SUPPORTED_USER_ID_SOURCES['intentiq.com'], + quantcastId: SUPPORTED_USER_ID_SOURCES['quantcast.com'] + }; function createCustomBidRequest({bids, params} = {}) { var bidderRequest = getDefaultBidRequest(); @@ -463,6 +490,18 @@ describe('AolAdapter', function () { '¶m1=val1¶m2=val2¶m3=val3¶m4=val4'); }); + Object.keys(SUPPORTED_USER_ID_SOURCES).forEach(source => { + it(`should set the user ID query param for ${source}`, function () { + let bidRequest = createCustomBidRequest({ + params: getNexageGetBidParams() + }); + bidRequest.bids[0].userId = {}; + bidRequest.bids[0].userIdAsEids = createEidsArray(USER_ID_DATA); + let [request] = spec.buildRequests(bidRequest.bids); + expect(request.url).to.contain(`&eid${source}=${encodeURIComponent(SUPPORTED_USER_ID_SOURCES[source])}`); + }); + }); + it('should return request object for One Mobile POST endpoint when POST configuration is present', function () { let bidConfig = getNexagePostBidParams(); let bidRequest = createCustomBidRequest({ @@ -581,6 +620,22 @@ describe('AolAdapter', function () { } }); }); + + it('returns the bid object with eid array populated with PB set eids', () => { + let userIdBid = Object.assign({ + userId: {} + }, bid); + userIdBid.userIdAsEids = createEidsArray(USER_ID_DATA); + expect(spec.buildOpenRtbRequestData(userIdBid)).to.deep.equal({ + id: 'bid-id', + imp: [], + user: { + ext: { + eids: userIdBid.userIdAsEids + } + } + }); + }); }); describe('getUserSyncs()', function () { diff --git a/test/spec/modules/quantumdexBidAdapter_spec.js b/test/spec/modules/apacdexBidAdapter_spec.js similarity index 92% rename from test/spec/modules/quantumdexBidAdapter_spec.js rename to test/spec/modules/apacdexBidAdapter_spec.js index d1817493b36..da9a050a8de 100644 --- a/test/spec/modules/quantumdexBidAdapter_spec.js +++ b/test/spec/modules/apacdexBidAdapter_spec.js @@ -1,14 +1,14 @@ import { expect } from 'chai' -import { spec } from 'modules/quantumdexBidAdapter.js' +import { spec } from 'modules/apacdexBidAdapter.js' import { newBidder } from 'src/adapters/bidderFactory.js' import { userSync } from '../../../src/userSync.js'; -describe('QuantumdexBidAdapter', function () { +describe('ApacdexBidAdapter', function () { const adapter = newBidder(spec) describe('.code', function () { - it('should return a bidder code of quantumdex', function () { - expect(spec.code).to.equal('quantumdex') + it('should return a bidder code of apacdex', function () { + expect(spec.code).to.equal('apacdex') }) }) @@ -21,7 +21,7 @@ describe('QuantumdexBidAdapter', function () { describe('.isBidRequestValid', function () { it('should return false if there are no params', () => { const bid = { - 'bidder': 'quantumdex', + 'bidder': 'apacdex', 'adUnitCode': 'adunit-code', 'mediaTypes': { banner: { @@ -37,7 +37,7 @@ describe('QuantumdexBidAdapter', function () { it('should return false if there is no siteId param', () => { const bid = { - 'bidder': 'quantumdex', + 'bidder': 'apacdex', 'adUnitCode': 'adunit-code', params: { site_id: '1a2b3c4d5e6f1a2b3c4d', @@ -56,7 +56,7 @@ describe('QuantumdexBidAdapter', function () { it('should return false if there is no mediaTypes', () => { const bid = { - 'bidder': 'quantumdex', + 'bidder': 'apacdex', 'adUnitCode': 'adunit-code', params: { siteId: '1a2b3c4d5e6f1a2b3c4d' @@ -72,7 +72,7 @@ describe('QuantumdexBidAdapter', function () { it('should return true if the bid is valid', () => { const bid = { - 'bidder': 'quantumdex', + 'bidder': 'apacdex', 'adUnitCode': 'adunit-code', params: { siteId: '1a2b3c4d5e6f1a2b3c4d' @@ -92,7 +92,7 @@ describe('QuantumdexBidAdapter', function () { describe('banner', () => { it('should return false if there are no banner sizes', () => { const bid = { - 'bidder': 'quantumdex', + 'bidder': 'apacdex', 'adUnitCode': 'adunit-code', params: { siteId: '1a2b3c4d5e6f1a2b3c4d' @@ -111,7 +111,7 @@ describe('QuantumdexBidAdapter', function () { it('should return true if there is banner sizes', () => { const bid = { - 'bidder': 'quantumdex', + 'bidder': 'apacdex', 'adUnitCode': 'adunit-code', params: { siteId: '1a2b3c4d5e6f1a2b3c4d' @@ -132,7 +132,7 @@ describe('QuantumdexBidAdapter', function () { describe('video', () => { it('should return false if there is no playerSize defined in the video mediaType', () => { const bid = { - 'bidder': 'quantumdex', + 'bidder': 'apacdex', 'adUnitCode': 'adunit-code', params: { siteId: '1a2b3c4d5e6f1a2b3c4d', @@ -152,7 +152,7 @@ describe('QuantumdexBidAdapter', function () { it('should return true if there is playerSize defined on the video mediaType', () => { const bid = { - 'bidder': 'quantumdex', + 'bidder': 'apacdex', 'adUnitCode': 'adunit-code', params: { siteId: '1a2b3c4d5e6f1a2b3c4d', @@ -196,7 +196,7 @@ describe('QuantumdexBidAdapter', function () { }, ] }, - 'bidder': 'quantumdex', + 'bidder': 'apacdex', 'params': { 'siteId': '1a2b3c4d5e6f1a2b3c4d', }, @@ -206,7 +206,7 @@ describe('QuantumdexBidAdapter', function () { 'bidId': '30b31c1838de1f', }, { - 'bidder': 'quantumdex', + 'bidder': 'apacdex', 'params': { 'ad_unit': '/7780971/sparks_prebid_LB', 'sizes': [[300, 250], [300, 600]], @@ -235,14 +235,14 @@ describe('QuantumdexBidAdapter', function () { it('should return a properly formatted request', function () { const bidRequests = spec.buildRequests(bidRequest, bidderRequests) - expect(bidRequests.url).to.equal('https://useast.quantumdex.io/auction/quantumdex') + expect(bidRequests.url).to.equal('https://useast.quantumdex.io/auction/apacdex') expect(bidRequests.method).to.equal('POST') expect(bidRequests.bidderRequests).to.eql(bidRequest); }) it('should return a properly formatted request with GDPR applies set to true', function () { const bidRequests = spec.buildRequests(bidRequest, bidderRequests) - expect(bidRequests.url).to.equal('https://useast.quantumdex.io/auction/quantumdex') + expect(bidRequests.url).to.equal('https://useast.quantumdex.io/auction/apacdex') expect(bidRequests.method).to.equal('POST') expect(bidRequests.data.gdpr.gdprApplies).to.equal(true) expect(bidRequests.data.gdpr.consentString).to.equal('BOJ/P2HOJ/P2HABABMAAAAAZ+A==') @@ -251,7 +251,7 @@ describe('QuantumdexBidAdapter', function () { it('should return a properly formatted request with GDPR applies set to false', function () { bidderRequests.gdprConsent.gdprApplies = false; const bidRequests = spec.buildRequests(bidRequest, bidderRequests) - expect(bidRequests.url).to.equal('https://useast.quantumdex.io/auction/quantumdex') + expect(bidRequests.url).to.equal('https://useast.quantumdex.io/auction/apacdex') expect(bidRequests.method).to.equal('POST') expect(bidRequests.data.gdpr.gdprApplies).to.equal(false) expect(bidRequests.data.gdpr.consentString).to.equal('BOJ/P2HOJ/P2HABABMAAAAAZ+A==') @@ -271,7 +271,7 @@ describe('QuantumdexBidAdapter', function () { } }; const bidRequests = spec.buildRequests(bidRequest, bidderRequests) - expect(bidRequests.url).to.equal('https://useast.quantumdex.io/auction/quantumdex') + expect(bidRequests.url).to.equal('https://useast.quantumdex.io/auction/apacdex') expect(bidRequests.method).to.equal('POST') expect(bidRequests.data.gdpr.gdprApplies).to.equal(false) expect(bidRequests.data.gdpr).to.not.include.keys('consentString') @@ -291,7 +291,7 @@ describe('QuantumdexBidAdapter', function () { } }; const bidRequests = spec.buildRequests(bidRequest, bidderRequests) - expect(bidRequests.url).to.equal('https://useast.quantumdex.io/auction/quantumdex') + expect(bidRequests.url).to.equal('https://useast.quantumdex.io/auction/apacdex') expect(bidRequests.method).to.equal('POST') expect(bidRequests.data.gdpr.gdprApplies).to.equal(true) expect(bidRequests.data.gdpr).to.not.include.keys('consentString') @@ -309,7 +309,7 @@ describe('QuantumdexBidAdapter', function () { describe('.interpretResponse', function () { const bidRequests = { 'method': 'POST', - 'url': 'https://useast.quantumdex.io/auction/quantumdex', + 'url': 'https://useast.quantumdex.io/auction/apacdex', 'withCredentials': true, 'data': { 'device': { @@ -328,7 +328,7 @@ describe('QuantumdexBidAdapter', function () { }, 'bidderRequests': [ { - 'bidder': 'quantumdex', + 'bidder': 'apacdex', 'params': { 'siteId': '343' }, @@ -363,7 +363,7 @@ describe('QuantumdexBidAdapter', function () { } }, { - 'bidder': 'quantumdex', + 'bidder': 'apacdex', 'params': { 'siteId': '343' }, @@ -398,7 +398,7 @@ describe('QuantumdexBidAdapter', function () { } }, { - 'bidder': 'quantumdex', + 'bidder': 'apacdex', 'params': { 'siteId': '343' }, @@ -464,12 +464,12 @@ describe('QuantumdexBidAdapter', function () { 'cpm': 1.07, 'width': 160, 'height': 600, - 'ad': `
Quantumdex AD
`, + 'ad': `
Apacdex AD
`, 'ttl': 500, 'creativeId': '1234abcd', 'netRevenue': true, 'currency': 'USD', - 'dealId': 'quantumdex', + 'dealId': 'apacdex', 'mediaType': 'banner' }, { @@ -477,12 +477,12 @@ describe('QuantumdexBidAdapter', function () { 'cpm': 1, 'width': 300, 'height': 250, - 'ad': `
Quantumdex AD
`, + 'ad': `
Apacdex AD
`, 'ttl': 500, 'creativeId': '1234abcd', 'netRevenue': true, 'currency': 'USD', - 'dealId': 'quantumdex', + 'dealId': 'apacdex', 'mediaType': 'banner' }, { @@ -490,12 +490,12 @@ describe('QuantumdexBidAdapter', function () { 'cpm': 1.25, 'width': 300, 'height': 250, - 'vastXml': 'quantumdex', + 'vastXml': 'apacdex', 'ttl': 500, 'creativeId': '30292e432662bd5f86d90774b944b038', 'netRevenue': true, 'currency': 'USD', - 'dealId': 'quantumdex', + 'dealId': 'apacdex', 'mediaType': 'video' } ], @@ -512,12 +512,12 @@ describe('QuantumdexBidAdapter', function () { 'cpm': 1.07, 'width': 160, 'height': 600, - 'ad': `
Quantumdex AD
`, + 'ad': `
Apacdex AD
`, 'ttl': 500, 'creativeId': '1234abcd', 'netRevenue': true, 'currency': 'USD', - 'dealId': 'quantumdex', + 'dealId': 'apacdex', 'mediaType': 'banner' }, { @@ -525,12 +525,12 @@ describe('QuantumdexBidAdapter', function () { 'cpm': 1, 'width': 300, 'height': 250, - 'ad': `
Quantumdex AD
`, + 'ad': `
Apacdex AD
`, 'ttl': 500, 'creativeId': '1234abcd', 'netRevenue': true, 'currency': 'USD', - 'dealId': 'quantumdex', + 'dealId': 'apacdex', 'mediaType': 'banner' }, { @@ -538,12 +538,12 @@ describe('QuantumdexBidAdapter', function () { 'cpm': 1.25, 'width': 300, 'height': 250, - 'vastXml': 'quantumdex', + 'vastXml': 'apacdex', 'ttl': 500, 'creativeId': '30292e432662bd5f86d90774b944b038', 'netRevenue': true, 'currency': 'USD', - 'dealId': 'quantumdex', + 'dealId': 'apacdex', 'mediaType': 'video' } ]; @@ -561,10 +561,10 @@ describe('QuantumdexBidAdapter', function () { expect(resp.currency).to.equal(prebidResponse[i].currency); expect(resp.dealId).to.equal(prebidResponse[i].dealId); if (resp.mediaType === 'video') { - expect(resp.vastXml.indexOf('quantumdex')).to.be.greaterThan(0); + expect(resp.vastXml.indexOf('apacdex')).to.be.greaterThan(0); } if (resp.mediaType === 'banner') { - expect(resp.ad.indexOf('Quantumdex AD')).to.be.greaterThan(0); + expect(resp.ad.indexOf('Apacdex AD')).to.be.greaterThan(0); } }); }); diff --git a/test/spec/modules/appnexusBidAdapter_spec.js b/test/spec/modules/appnexusBidAdapter_spec.js index 7e985045c45..a36eab03fe1 100644 --- a/test/spec/modules/appnexusBidAdapter_spec.js +++ b/test/spec/modules/appnexusBidAdapter_spec.js @@ -265,6 +265,7 @@ describe('AppNexusAdapter', function () { placementId: '10433394', user: { externalUid: '123', + segments: [123, { id: 987, value: 876 }], foobar: 'invalid' } } @@ -277,6 +278,7 @@ describe('AppNexusAdapter', function () { expect(payload.user).to.exist; expect(payload.user).to.deep.equal({ external_uid: '123', + segments: [{id: 123}, {id: 987, value: 876}] }); }); @@ -782,6 +784,18 @@ describe('AppNexusAdapter', function () { config.getConfig.restore(); }); + it('should set the X-Is-Test customHeader if test flag is enabled', function () { + let bidRequest = Object.assign({}, bidRequests[0]); + sinon.stub(config, 'getConfig') + .withArgs('apn_test') + .returns(true); + + const request = spec.buildRequests([bidRequest]); + expect(request.options.customHeaders).to.deep.equal({'X-Is-Test': 1}); + + config.getConfig.restore(); + }); + it('should set withCredentials to false if purpose 1 consent is not given', function () { let consentString = 'BOJ8RZsOJ8RZsABAB8AAAAAZ+A=='; let bidderRequest = { @@ -808,7 +822,7 @@ describe('AppNexusAdapter', function () { expect(request.options).to.deep.equal({withCredentials: false}); }); - it('should populate eids array when ttd id and criteo is available', function () { + it('should populate eids when ttd id and criteo is available', function () { const bidRequest = Object.assign({}, bidRequests[0], { userId: { tdid: 'sample-userid', @@ -829,6 +843,52 @@ describe('AppNexusAdapter', function () { id: 'sample-criteo-userid', }); }); + + it('should populate iab_support object at the root level if omid support is detected', function () { + // with bid.params.frameworks + let bidRequest_A = Object.assign({}, bidRequests[0], { + params: { + frameworks: [1, 2, 5, 6], + video: { + frameworks: [1, 2, 5, 6] + } + } + }); + let request = spec.buildRequests([bidRequest_A]); + let payload = JSON.parse(request.data); + expect(payload.iab_support).to.be.an('object'); + expect(payload.iab_support).to.deep.equal({ + omidpn: 'Appnexus', + omidpv: '$prebid.version$' + }); + expect(payload.tags[0].banner_frameworks).to.be.an('array'); + expect(payload.tags[0].banner_frameworks).to.deep.equal([1, 2, 5, 6]); + expect(payload.tags[0].video_frameworks).to.be.an('array'); + expect(payload.tags[0].video_frameworks).to.deep.equal([1, 2, 5, 6]); + expect(payload.tags[0].video.frameworks).to.not.exist; + + // without bid.params.frameworks + const bidRequest_B = Object.assign({}, bidRequests[0]); + request = spec.buildRequests([bidRequest_B]); + payload = JSON.parse(request.data); + expect(payload.iab_support).to.not.exist; + expect(payload.tags[0].banner_frameworks).to.not.exist; + expect(payload.tags[0].video_frameworks).to.not.exist; + + // with video.frameworks but it is not an array + const bidRequest_C = Object.assign({}, bidRequests[0], { + params: { + video: { + frameworks: "'1', '2', '3', '6'" + } + } + }); + request = spec.buildRequests([bidRequest_C]); + payload = JSON.parse(request.data); + expect(payload.iab_support).to.not.exist; + expect(payload.tags[0].banner_frameworks).to.not.exist; + expect(payload.tags[0].video_frameworks).to.not.exist; + }); }) describe('interpretResponse', function () { diff --git a/test/spec/modules/atsAnalyticsAdapter_spec.js b/test/spec/modules/atsAnalyticsAdapter_spec.js index 849ccf88c51..84206337fad 100644 --- a/test/spec/modules/atsAnalyticsAdapter_spec.js +++ b/test/spec/modules/atsAnalyticsAdapter_spec.js @@ -18,7 +18,7 @@ describe('ats analytics adapter', function () { describe('track', function () { it('builds and sends request and response data', function () { - sinon.spy(atsAnalyticsAdapter, 'track'); + sinon.stub(atsAnalyticsAdapter, 'shouldFireRequest').returns(true); let initOptions = { pid: '10433394', @@ -134,7 +134,6 @@ describe('ats analytics adapter', function () { let requests = server.requests.filter(req => { return req.url.indexOf(initOptions.host) > -1; }); - expect(requests.length).to.equal(1); let realAfterBid = JSON.parse(requests[0].requestBody); diff --git a/test/spec/modules/avocetBidAdapter_spec.js b/test/spec/modules/avocetBidAdapter_spec.js index 4cfd8ab89d4..2a2f29e48d2 100644 --- a/test/spec/modules/avocetBidAdapter_spec.js +++ b/test/spec/modules/avocetBidAdapter_spec.js @@ -69,7 +69,9 @@ describe('Avocet adapter', function () { placement: '012345678901234567890123', }, userId: { - id5id: 'test' + id5id: { + uid: 'test' + } } }, { diff --git a/test/spec/modules/beachfrontBidAdapter_spec.js b/test/spec/modules/beachfrontBidAdapter_spec.js index 5711e111e30..43c71dd6349 100644 --- a/test/spec/modules/beachfrontBidAdapter_spec.js +++ b/test/spec/modules/beachfrontBidAdapter_spec.js @@ -223,17 +223,33 @@ describe('BeachfrontAdapter', function () { expect(data.imp[0].video).to.deep.contain({ w: width, h: height }); }); - it('must override video targeting params', function () { + it('must set video params from the standard object', function () { const bidRequest = bidRequests[0]; const mimes = ['video/webm']; const playbackmethod = 2; const maxduration = 30; const placement = 4; - bidRequest.mediaTypes = { video: {} }; - bidRequest.params.video = { mimes, playbackmethod, maxduration, placement }; + const skip = 1; + bidRequest.mediaTypes = { + video: { mimes, playbackmethod, maxduration, placement, skip } + }; const requests = spec.buildRequests([ bidRequest ]); const data = requests[0].data; - expect(data.imp[0].video).to.deep.contain({ mimes, playbackmethod, maxduration, placement }); + expect(data.imp[0].video).to.deep.contain({ mimes, playbackmethod, maxduration, placement, skip }); + }); + + it('must override video params from the bidder object', function () { + const bidRequest = bidRequests[0]; + const mimes = ['video/webm']; + const playbackmethod = 2; + const maxduration = 30; + const placement = 4; + const skip = 1; + bidRequest.mediaTypes = { video: { placement: 3, skip: 0 } }; + bidRequest.params.video = { mimes, playbackmethod, maxduration, placement, skip }; + const requests = spec.buildRequests([ bidRequest ]); + const data = requests[0].data; + expect(data.imp[0].video).to.deep.contain({ mimes, playbackmethod, maxduration, placement, skip }); }); it('must add US privacy data to the request', function () { @@ -279,6 +295,24 @@ describe('BeachfrontAdapter', function () { }] }); }); + + it('must add the IdentityLink ID to the request', () => { + const idl_env = '4321'; + const bidRequest = bidRequests[0]; + bidRequest.mediaTypes = { video: {} }; + bidRequest.userId = { idl_env }; + const requests = spec.buildRequests([ bidRequest ]); + const data = requests[0].data; + expect(data.user.ext.eids[0]).to.deep.equal({ + source: 'liveramp.com', + uids: [{ + id: idl_env, + ext: { + rtiPartner: 'idl' + } + }] + }); + }); }); describe('for banner bids', function () { @@ -302,6 +336,7 @@ describe('BeachfrontAdapter', function () { const width = 300; const height = 250; const bidRequest = bidRequests[0]; + bidRequest.params.tagid = '7cd7a7b4-ef3f-4aeb-9565-3627f255fa10'; bidRequest.mediaTypes = { banner: { sizes: [ width, height ] @@ -320,6 +355,7 @@ describe('BeachfrontAdapter', function () { slot: bidRequest.adUnitCode, id: bidRequest.params.appId, bidfloor: bidRequest.params.bidfloor, + tagid: bidRequest.params.tagid, sizes: [{ w: width, h: height }] } ]); @@ -419,6 +455,16 @@ describe('BeachfrontAdapter', function () { const data = requests[0].data; expect(data.tdid).to.equal(tdid); }); + + it('must add the IdentityLink ID to the request', () => { + const idl_env = '4321'; + const bidRequest = bidRequests[0]; + bidRequest.mediaTypes = { banner: {} }; + bidRequest.userId = { idl_env }; + const requests = spec.buildRequests([ bidRequest ]); + const data = requests[0].data; + expect(data.idl).to.equal(idl_env); + }); }); describe('for multi-format bids', function () { @@ -486,16 +532,6 @@ describe('BeachfrontAdapter', function () { expect(bidResponse.length).to.equal(0); }); - it('should return no bids if the response "url" is missing', function () { - const bidRequest = bidRequests[0]; - bidRequest.mediaTypes = { video: {} }; - const serverResponse = { - bidPrice: 5.00 - }; - const bidResponse = spec.interpretResponse({ body: serverResponse }, { bidRequest }); - expect(bidResponse.length).to.equal(0); - }); - it('should return no bids if the response "bidPrice" is missing', function () { const bidRequest = bidRequests[0]; bidRequest.mediaTypes = { video: {} }; @@ -518,6 +554,7 @@ describe('BeachfrontAdapter', function () { const serverResponse = { bidPrice: 5.00, url: 'http://reachms.bfmio.com/getmu?aid=bid:19c4a196-fb21-4c81-9a1a-ecc5437a39da', + vast: '', crid: '123abc' }; const bidResponse = spec.interpretResponse({ body: serverResponse }, { bidRequest }); @@ -527,7 +564,7 @@ describe('BeachfrontAdapter', function () { cpm: serverResponse.bidPrice, creativeId: serverResponse.crid, vastUrl: serverResponse.url, - vastXml: undefined, + vastXml: serverResponse.vast, width: width, height: height, renderer: null, @@ -558,7 +595,7 @@ describe('BeachfrontAdapter', function () { }); }); - it('should return vast xml if found on the bid response', () => { + it('should return only vast url if the response type is "nurl"', () => { const width = 640; const height = 480; const bidRequest = bidRequests[0]; @@ -567,6 +604,7 @@ describe('BeachfrontAdapter', function () { playerSize: [ width, height ] } }; + bidRequest.params.video = { responseType: 'nurl' }; const serverResponse = { bidPrice: 5.00, url: 'http://reachms.bfmio.com/getmu?aid=bid:19c4a196-fb21-4c81-9a1a-ecc5437a39da', @@ -574,10 +612,29 @@ describe('BeachfrontAdapter', function () { crid: '123abc' }; const bidResponse = spec.interpretResponse({ body: serverResponse }, { bidRequest }); - expect(bidResponse).to.deep.contain({ - vastUrl: serverResponse.url, - vastXml: serverResponse.vast - }); + expect(bidResponse.vastUrl).to.equal(serverResponse.url); + expect(bidResponse.vastXml).to.equal(undefined); + }); + + it('should return only vast xml if the response type is "adm"', () => { + const width = 640; + const height = 480; + const bidRequest = bidRequests[0]; + bidRequest.mediaTypes = { + video: { + playerSize: [ width, height ] + } + }; + bidRequest.params.video = { responseType: 'adm' }; + const serverResponse = { + bidPrice: 5.00, + url: 'http://reachms.bfmio.com/getmu?aid=bid:19c4a196-fb21-4c81-9a1a-ecc5437a39da', + vast: '', + crid: '123abc' + }; + const bidResponse = spec.interpretResponse({ body: serverResponse }, { bidRequest }); + expect(bidResponse.vastUrl).to.equal(undefined); + expect(bidResponse.vastXml).to.equal(serverResponse.vast); }); it('should return a renderer for outstream video bids', function () { diff --git a/test/spec/modules/bizzclickBidAdapter_spec.js b/test/spec/modules/bizzclickBidAdapter_spec.js new file mode 100644 index 00000000000..39ad4ae39c9 --- /dev/null +++ b/test/spec/modules/bizzclickBidAdapter_spec.js @@ -0,0 +1,369 @@ +import { expect } from 'chai'; +import { spec } from 'modules/bizzclickBidAdapter.js'; + +const NATIVE_BID_REQUEST = { + code: 'native_example', + mediaTypes: { + native: { + title: { + required: true, + len: 800 + }, + image: { + required: true, + len: 80 + }, + sponsoredBy: { + required: true + }, + clickUrl: { + required: true + }, + privacyLink: { + required: false + }, + body: { + required: true + }, + icon: { + required: true, + sizes: [50, 50] + } + } + }, + bidder: 'bizzclick', + params: { + placementId: 'hash', + accountId: 'accountId' + }, + timeout: 1000 + +}; + +const BANNER_BID_REQUEST = { + code: 'banner_example', + mediaTypes: { + banner: { + sizes: [[300, 250], [300, 600]] + } + }, + bidder: 'bizzclick', + params: { + placementId: 'hash', + accountId: 'accountId' + }, + timeout: 1000, + +} + +const bidRequest = { + refererInfo: { + referer: 'test.com' + } +} + +const VIDEO_BID_REQUEST = { + code: 'video1', + sizes: [640, 480], + mediaTypes: { video: { + minduration: 0, + maxduration: 999, + boxingallowed: 1, + skip: 0, + mimes: [ + 'application/javascript', + 'video/mp4' + ], + w: 1920, + h: 1080, + protocols: [ + 2 + ], + linearity: 1, + api: [ + 1, + 2 + ] + } + }, + + bidder: 'bizzclick', + params: { + placementId: 'hash', + accountId: 'accountId' + }, + timeout: 1000 + +} + +const BANNER_BID_RESPONSE = { + id: 'request_id', + bidid: 'request_imp_id', + seatbid: [{ + bid: [{ + id: 'bid_id', + impid: 'request_imp_id', + price: 5, + adomain: ['example.com'], + adm: 'admcode', + crid: 'crid', + ext: { + mediaType: 'banner' + } + }], + }], +}; + +const VIDEO_BID_RESPONSE = { + id: 'request_id', + bidid: 'request_imp_id', + seatbid: [{ + bid: [{ + id: 'bid_id', + impid: 'request_imp_id', + price: 5, + adomain: ['example.com'], + adm: 'admcode', + crid: 'crid', + ext: { + mediaType: 'video', + vastUrl: 'http://example.vast', + } + }], + }], +}; + +let imgData = { + url: `https://example.com/image`, + w: 1200, + h: 627 +}; + +const NATIVE_BID_RESPONSE = { + id: 'request_id', + bidid: 'request_imp_id', + seatbid: [{ + bid: [{ + id: 'bid_id', + impid: 'request_imp_id', + price: 5, + adomain: ['example.com'], + adm: { native: + { + assets: [ + {id: 0, title: 'dummyText'}, + {id: 3, image: imgData}, + { + id: 5, + data: {value: 'organization.name'} + } + ], + link: {url: 'example.com'}, + imptrackers: ['tracker1.com', 'tracker2.com', 'tracker3.com'], + jstracker: 'tracker1.com' + } + }, + crid: 'crid', + ext: { + mediaType: 'native' + } + }], + }], +}; + +describe('BizzclickAdapter', function() { + describe('isBidRequestValid', function() { + it('should return true when required params found', function () { + expect(spec.isBidRequestValid(NATIVE_BID_REQUEST)).to.equal(true); + }); + + it('should return false when required params are not passed', function () { + let bid = Object.assign({}, NATIVE_BID_REQUEST); + delete bid.params; + bid.params = { + 'IncorrectParam': 0 + }; + expect(spec.isBidRequestValid(bid)).to.equal(false); + }); + }); + + describe('build Native Request', function () { + const request = spec.buildRequests([NATIVE_BID_REQUEST], bidRequest); + + it('Creates a ServerRequest object with method, URL and data', function () { + expect(request).to.exist; + expect(request.method).to.exist; + expect(request.url).to.exist; + expect(request.data).to.exist; + }); + + it('sends bid request to our endpoint via POST', function () { + expect(request.method).to.equal('POST'); + }); + + it('Returns valid URL', function () { + expect(request.url).to.equal('https://us-e-node1.bizzclick.com/bid?rtb_seat_id=prebidjs&secret_key=accountId'); + }); + + it('Returns empty data if no valid requests are passed', function () { + let serverRequest = spec.buildRequests([]); + expect(serverRequest).to.be.an('array').that.is.empty; + }); + }); + + describe('build Banner Request', function () { + const request = spec.buildRequests([BANNER_BID_REQUEST]); + + it('Creates a ServerRequest object with method, URL and data', function () { + expect(request).to.exist; + expect(request.method).to.exist; + expect(request.url).to.exist; + expect(request.data).to.exist; + }); + + it('sends bid request to our endpoint via POST', function () { + expect(request.method).to.equal('POST'); + }); + + it('Returns valid URL', function () { + expect(request.url).to.equal('https://us-e-node1.bizzclick.com/bid?rtb_seat_id=prebidjs&secret_key=accountId'); + }); + }); + + describe('build Video Request', function () { + const request = spec.buildRequests([VIDEO_BID_REQUEST]); + + it('Creates a ServerRequest object with method, URL and data', function () { + expect(request).to.exist; + expect(request.method).to.exist; + expect(request.url).to.exist; + expect(request.data).to.exist; + }); + + it('sends bid request to our endpoint via POST', function () { + expect(request.method).to.equal('POST'); + }); + + it('Returns valid URL', function () { + expect(request.url).to.equal('https://us-e-node1.bizzclick.com/bid?rtb_seat_id=prebidjs&secret_key=accountId'); + }); + }); + + describe('interpretResponse', function () { + it('Empty response must return empty array', function() { + const emptyResponse = null; + let response = spec.interpretResponse(emptyResponse); + + expect(response).to.be.an('array').that.is.empty; + }) + + it('Should interpret banner response', function () { + const bannerResponse = { + body: [BANNER_BID_RESPONSE] + } + + const expectedBidResponse = { + requestId: BANNER_BID_RESPONSE.id, + cpm: BANNER_BID_RESPONSE.seatbid[0].bid[0].price, + width: BANNER_BID_RESPONSE.seatbid[0].bid[0].w, + height: BANNER_BID_RESPONSE.seatbid[0].bid[0].h, + ttl: BANNER_BID_RESPONSE.ttl || 1200, + currency: BANNER_BID_RESPONSE.cur || 'USD', + netRevenue: true, + creativeId: BANNER_BID_RESPONSE.seatbid[0].bid[0].crid, + dealId: BANNER_BID_RESPONSE.seatbid[0].bid[0].dealid, + mediaType: 'banner', + ad: BANNER_BID_RESPONSE.seatbid[0].bid[0].adm + } + + let bannerResponses = spec.interpretResponse(bannerResponse); + + expect(bannerResponses).to.be.an('array').that.is.not.empty; + let dataItem = bannerResponses[0]; + expect(dataItem).to.have.all.keys('requestId', 'cpm', 'width', 'height', 'ad', 'ttl', 'creativeId', + 'netRevenue', 'currency', 'dealId', 'mediaType'); + expect(dataItem.requestId).to.equal(expectedBidResponse.requestId); + expect(dataItem.cpm).to.equal(expectedBidResponse.cpm); + expect(dataItem.ad).to.equal(expectedBidResponse.ad); + expect(dataItem.ttl).to.equal(expectedBidResponse.ttl); + expect(dataItem.creativeId).to.equal(expectedBidResponse.creativeId); + expect(dataItem.netRevenue).to.be.true; + expect(dataItem.currency).to.equal(expectedBidResponse.currency); + expect(dataItem.width).to.equal(expectedBidResponse.width); + expect(dataItem.height).to.equal(expectedBidResponse.height); + }); + + it('Should interpret video response', function () { + const videoResponse = { + body: [VIDEO_BID_RESPONSE] + } + + const expectedBidResponse = { + requestId: VIDEO_BID_RESPONSE.id, + cpm: VIDEO_BID_RESPONSE.seatbid[0].bid[0].price, + width: VIDEO_BID_RESPONSE.seatbid[0].bid[0].w, + height: VIDEO_BID_RESPONSE.seatbid[0].bid[0].h, + ttl: VIDEO_BID_RESPONSE.ttl || 1200, + currency: VIDEO_BID_RESPONSE.cur || 'USD', + netRevenue: true, + creativeId: VIDEO_BID_RESPONSE.seatbid[0].bid[0].crid, + dealId: VIDEO_BID_RESPONSE.seatbid[0].bid[0].dealid, + mediaType: 'video', + vastXml: VIDEO_BID_RESPONSE.seatbid[0].bid[0].adm, + vastUrl: VIDEO_BID_RESPONSE.seatbid[0].bid[0].ext.vastUrl + } + + let videoResponses = spec.interpretResponse(videoResponse); + + expect(videoResponses).to.be.an('array').that.is.not.empty; + let dataItem = videoResponses[0]; + expect(dataItem).to.have.all.keys('requestId', 'cpm', 'width', 'height', 'vastXml', 'vastUrl', 'ttl', 'creativeId', + 'netRevenue', 'currency', 'dealId', 'mediaType'); + expect(dataItem.requestId).to.equal(expectedBidResponse.requestId); + expect(dataItem.cpm).to.equal(expectedBidResponse.cpm); + expect(dataItem.vastXml).to.equal(expectedBidResponse.vastXml) + expect(dataItem.ttl).to.equal(expectedBidResponse.ttl); + expect(dataItem.creativeId).to.equal(expectedBidResponse.creativeId); + expect(dataItem.netRevenue).to.be.true; + expect(dataItem.currency).to.equal(expectedBidResponse.currency); + expect(dataItem.width).to.equal(expectedBidResponse.width); + expect(dataItem.height).to.equal(expectedBidResponse.height); + }); + + it('Should interpret native response', function () { + const nativeResponse = { + body: [NATIVE_BID_RESPONSE] + } + + const expectedBidResponse = { + requestId: NATIVE_BID_RESPONSE.id, + cpm: NATIVE_BID_RESPONSE.seatbid[0].bid[0].price, + width: NATIVE_BID_RESPONSE.seatbid[0].bid[0].w, + height: NATIVE_BID_RESPONSE.seatbid[0].bid[0].h, + ttl: NATIVE_BID_RESPONSE.ttl || 1200, + currency: NATIVE_BID_RESPONSE.cur || 'USD', + netRevenue: true, + creativeId: NATIVE_BID_RESPONSE.seatbid[0].bid[0].crid, + dealId: NATIVE_BID_RESPONSE.seatbid[0].bid[0].dealid, + mediaType: 'native', + native: {clickUrl: NATIVE_BID_RESPONSE.seatbid[0].bid[0].adm.native.link.url} + } + + let nativeResponses = spec.interpretResponse(nativeResponse); + + expect(nativeResponses).to.be.an('array').that.is.not.empty; + let dataItem = nativeResponses[0]; + expect(dataItem).to.have.all.keys('requestId', 'cpm', 'width', 'height', 'native', 'ttl', 'creativeId', + 'netRevenue', 'currency', 'dealId', 'mediaType'); + expect(dataItem.requestId).to.equal(expectedBidResponse.requestId); + expect(dataItem.cpm).to.equal(expectedBidResponse.cpm); + expect(dataItem.native.clickUrl).to.equal(expectedBidResponse.native.clickUrl) + expect(dataItem.ttl).to.equal(expectedBidResponse.ttl); + expect(dataItem.creativeId).to.equal(expectedBidResponse.creativeId); + expect(dataItem.netRevenue).to.be.true; + expect(dataItem.currency).to.equal(expectedBidResponse.currency); + expect(dataItem.width).to.equal(expectedBidResponse.width); + expect(dataItem.height).to.equal(expectedBidResponse.height); + }); + }); +}) diff --git a/test/spec/modules/bridgewellBidAdapter_spec.js b/test/spec/modules/bridgewellBidAdapter_spec.js index fea2454393d..24ec523ac67 100644 --- a/test/spec/modules/bridgewellBidAdapter_spec.js +++ b/test/spec/modules/bridgewellBidAdapter_spec.js @@ -143,12 +143,58 @@ describe('bridgewellBidAdapter', function () { expect(payload.url).to.exist.and.to.equal('https://www.bridgewell.com/'); for (let i = 0, max_i = payload.adUnits.length; i < max_i; i++) { expect(payload.adUnits[i]).to.have.property('ChannelID').that.is.a('string'); + expect(payload.adUnits[i]).to.not.have.property('cid'); expect(payload.adUnits[i]).to.have.property('adUnitCode').and.to.equal('adunit-code-2'); + expect(payload.adUnits[i]).to.have.property('requestId').and.to.equal('3150ccb55da321'); + } + }); + + it('should attach valid params to the tag, part2', function() { + const bidderRequest = { + refererInfo: { + referer: 'https://www.bridgewell.com/' + } + } + const bidRequests2 = [ + { + 'bidder': 'bridgewell', + 'params': { + 'cid': 1234, + }, + 'adUnitCode': 'adunit-code-2', + 'mediaTypes': { + 'banner': { + 'sizes': [728, 90] + } + }, + 'bidId': '3150ccb55da321', + 'bidderRequestId': '22edbae2733bf6', + 'auctionId': '1d1a030790a475', + }, + ]; + + const request = spec.buildRequests(bidRequests2, bidderRequest); + const payload = request.data; + + expect(payload).to.be.an('object'); + expect(payload.adUnits).to.be.an('array'); + expect(payload.url).to.exist.and.to.equal('https://www.bridgewell.com/'); + for (let i = 0, max_i = payload.adUnits.length; i < max_i; i++) { + expect(payload.adUnits[i]).to.have.property('cid').that.is.a('number'); + expect(payload.adUnits[i]).to.not.have.property('ChannelID'); + expect(payload.adUnits[i]).to.have.property('adUnitCode').and.to.equal('adunit-code-2'); + expect(payload.adUnits[i]).to.have.property('requestId').and.to.equal('3150ccb55da321'); } }); it('should attach validBidRequests to the tag', function () { - const request = spec.buildRequests(bidRequests); + const bidderRequest = { + refererInfo: { + referer: 'https://www.bridgewell.com/' + } + } + + const request = spec.buildRequests(bidRequests, bidderRequest); const validBidRequests = request.validBidRequests; expect(validBidRequests).to.deep.equal(bidRequests); }); diff --git a/test/spec/modules/brightMountainMediaBidAdapter_spec.js b/test/spec/modules/brightMountainMediaBidAdapter_spec.js index 3b7e46e55a7..f6312253722 100644 --- a/test/spec/modules/brightMountainMediaBidAdapter_spec.js +++ b/test/spec/modules/brightMountainMediaBidAdapter_spec.js @@ -133,7 +133,7 @@ describe('brightMountainMediaBidAdapter_spec', function () { expect(spec.getUserSyncs(syncoptionsIframe)[0].type).to.exist; expect(spec.getUserSyncs(syncoptionsIframe)[0].url).to.exist; expect(spec.getUserSyncs(syncoptionsIframe)[0].type).to.equal('iframe') - expect(spec.getUserSyncs(syncoptionsIframe)[0].url).to.equal('https://console.brightmountainmedia.com:4444/cookieSync') + expect(spec.getUserSyncs(syncoptionsIframe)[0].url).to.equal('https://console.brightmountainmedia.com:8443/cookieSync') }); }); }); diff --git a/test/spec/modules/britepoolIdSystem_spec.js b/test/spec/modules/britepoolIdSystem_spec.js index 2c6dd234a90..ddb61806006 100644 --- a/test/spec/modules/britepoolIdSystem_spec.js +++ b/test/spec/modules/britepoolIdSystem_spec.js @@ -1,5 +1,5 @@ -import { expect } from 'chai'; import {britepoolIdSubmodule} from 'modules/britepoolIdSystem.js'; +import * as utils from '../../../src/utils.js'; describe('BritePool Submodule', () => { const api_key = '1111'; @@ -16,6 +16,32 @@ describe('BritePool Submodule', () => { }; }; + let triggerPixelStub; + + beforeEach(function (done) { + triggerPixelStub = sinon.stub(utils, 'triggerPixel'); + done(); + }); + + afterEach(function () { + triggerPixelStub.restore(); + }); + + it('trigger id resolution pixel when no identifiers set', () => { + britepoolIdSubmodule.getId({ params: {} }); + expect(triggerPixelStub.called).to.be.true; + }); + + it('trigger id resolution pixel when no identifiers set with api_key param', () => { + britepoolIdSubmodule.getId({ params: { api_key } }); + expect(triggerPixelStub.called).to.be.true; + }); + + it('does not trigger id resolution pixel when identifiers set', () => { + britepoolIdSubmodule.getId({ params: { api_key, aaid } }); + expect(triggerPixelStub.called).to.be.false; + }); + it('sends x-api-key in header and one identifier', () => { const { params, headers, url, errors } = britepoolIdSubmodule.createParams({ api_key, aaid }); assert(errors.length === 0, errors); @@ -43,12 +69,48 @@ describe('BritePool Submodule', () => { expect(params.url).to.be.undefined; }); + it('test gdpr consent string in url', () => { + const { params, headers, url, errors } = britepoolIdSubmodule.createParams({ api_key, aaid }, { gdprApplies: true, consentString: 'expectedConsentString' }); + expect(url).to.equal('https://api.britepool.com/v1/britepool/id?gdprString=expectedConsentString'); + }); + + it('test gdpr consent string not in url if gdprApplies false', () => { + const { params, headers, url, errors } = britepoolIdSubmodule.createParams({ api_key, aaid }, { gdprApplies: false, consentString: 'expectedConsentString' }); + expect(url).to.equal('https://api.britepool.com/v1/britepool/id'); + }); + + it('test gdpr consent string not in url if consent string undefined', () => { + const { params, headers, url, errors } = britepoolIdSubmodule.createParams({ api_key, aaid }, { gdprApplies: true, consentString: undefined }); + expect(url).to.equal('https://api.britepool.com/v1/britepool/id'); + }); + + it('dynamic pub params should be added to params', () => { + window.britepool_pubparams = { ppid: '12345' }; + const { params, headers, url, errors } = britepoolIdSubmodule.createParams({ api_key, aaid }); + expect(params).to.eql({ aaid, ppid: '12345' }); + window.britepool_pubparams = undefined; + }); + + it('dynamic pub params should override submodule params', () => { + window.britepool_pubparams = { ppid: '67890' }; + const { params, headers, url, errors } = britepoolIdSubmodule.createParams({ api_key, ppid: '12345' }); + expect(params).to.eql({ ppid: '67890' }); + window.britepool_pubparams = undefined; + }); + + it('if dynamic pub params undefined do nothing', () => { + window.britepool_pubparams = undefined; + const { params, headers, url, errors } = britepoolIdSubmodule.createParams({ api_key, aaid }); + expect(params).to.eql({ aaid }); + window.britepool_pubparams = undefined; + }); + it('test getter override with value', () => { const { params, headers, url, getter, errors } = britepoolIdSubmodule.createParams({ api_key, aaid, url: url_override, getter: getter_override }); expect(getter).to.equal(getter_override); // Making sure it did not become part of params expect(params.getter).to.be.undefined; - const response = britepoolIdSubmodule.getId({ api_key, aaid, url: url_override, getter: getter_override }); + const response = britepoolIdSubmodule.getId({ params: { api_key, aaid, url: url_override, getter: getter_override } }); assert.deepEqual(response, { id: { 'primaryBPID': bpid } }); }); @@ -57,7 +119,7 @@ describe('BritePool Submodule', () => { expect(getter).to.equal(getter_callback_override); // Making sure it did not become part of params expect(params.getter).to.be.undefined; - const response = britepoolIdSubmodule.getId({ api_key, aaid, url: url_override, getter: getter_callback_override }); + const response = britepoolIdSubmodule.getId({ params: { api_key, aaid, url: url_override, getter: getter_callback_override } }); expect(response.callback).to.not.be.undefined; response.callback(result => { assert.deepEqual(result, { 'primaryBPID': bpid }); diff --git a/test/spec/modules/browsiRtdProvider_spec.js b/test/spec/modules/browsiRtdProvider_spec.js new file mode 100644 index 00000000000..ee37d16905b --- /dev/null +++ b/test/spec/modules/browsiRtdProvider_spec.js @@ -0,0 +1,83 @@ +import * as browsiRTD from '../../../modules/browsiRtdProvider.js'; +import {makeSlot} from '../integration/faker/googletag.js'; + +describe('browsi Real time data sub module', function () { + const conf = { + 'auctionDelay': 250, + dataProviders: [{ + 'name': 'browsi', + 'params': { + 'url': 'testUrl.com', + 'siteKey': 'testKey', + 'pubKey': 'testPub', + 'keyName': 'bv' + } + }] + }; + + it('should init and return true', function () { + browsiRTD.collectData(); + expect(browsiRTD.browsiSubmodule.init(conf.dataProviders[0])).to.equal(true) + }); + + it('should create browsi script', function () { + const script = browsiRTD.addBrowsiTag('scriptUrl.com'); + expect(script.getAttribute('data-sitekey')).to.equal('testKey'); + expect(script.getAttribute('data-pubkey')).to.equal('testPub'); + expect(script.async).to.equal(true); + expect(script.prebidData.kn).to.equal(conf.dataProviders[0].params.keyName); + }); + + it('should match placement with ad unit', function () { + const slot = makeSlot({code: '/57778053/Browsi_Demo_300x250', divId: 'browsiAd_1'}); + + const test1 = browsiRTD.isIdMatchingAdUnit(slot, ['/57778053/Browsi_Demo_300x250']); // true + const test2 = browsiRTD.isIdMatchingAdUnit(slot, ['/57778053/Browsi_Demo_300x250', '/57778053/Browsi']); // true + const test3 = browsiRTD.isIdMatchingAdUnit(slot, ['/57778053/Browsi_Demo_Low']); // false + const test4 = browsiRTD.isIdMatchingAdUnit(slot, []); // true + + expect(test1).to.equal(true); + expect(test2).to.equal(true); + expect(test3).to.equal(false); + expect(test4).to.equal(true); + }); + + it('should return correct macro values', function () { + const slot = makeSlot({code: '/57778053/Browsi_Demo_300x250', divId: 'browsiAd_1'}); + + slot.setTargeting('test', ['test', 'value']); + // slot getTargeting doesn't act like GPT so we can't expect real value + const macroResult = browsiRTD.getMacroId({p: '/'}, slot); + expect(macroResult).to.equal('/57778053/Browsi_Demo_300x250/NA'); + + const macroResultB = browsiRTD.getMacroId({}, slot); + expect(macroResultB).to.equal('browsiAd_1'); + + const macroResultC = browsiRTD.getMacroId({p: '', s: {s: 0, e: 1}}, slot); + expect(macroResultC).to.equal('/'); + }); + + describe('should return data to RTD module', function () { + it('should return empty if no ad units defined', function () { + browsiRTD.setData({}); + expect(browsiRTD.browsiSubmodule.getTargetingData([])).to.eql({}); + }); + + it('should return NA if no prediction for ad unit', function () { + makeSlot({code: 'adMock', divId: 'browsiAd_2'}); + browsiRTD.setData({}); + expect(browsiRTD.browsiSubmodule.getTargetingData(['adMock'])).to.eql({adMock: {bv: 'NA'}}); + }); + + it('should return prediction from server', function () { + makeSlot({code: 'hasPrediction', divId: 'hasPrediction'}); + const data = { + p: {'hasPrediction': {p: 0.234}}, + kn: 'bv', + pmd: undefined + }; + browsiRTD.setData(data); + expect(browsiRTD.browsiSubmodule.getTargetingData(['hasPrediction'])).to.eql({hasPrediction: {bv: '0.20'}}); + }) + }) +}); diff --git a/test/spec/modules/cointrafficBidAdapter_spec.js b/test/spec/modules/cointrafficBidAdapter_spec.js index 6d948e36cb9..a2ce4cedea0 100644 --- a/test/spec/modules/cointrafficBidAdapter_spec.js +++ b/test/spec/modules/cointrafficBidAdapter_spec.js @@ -1,5 +1,7 @@ import { expect } from 'chai'; import { spec } from 'modules/cointrafficBidAdapter.js'; +import { config } from 'src/config.js' +import * as utils from 'src/utils.js' const ENDPOINT_URL = 'https://appspb.cointraffic.io/pb/tmp'; @@ -37,7 +39,7 @@ describe('cointrafficBidAdapter', function () { ], bidId: 'bidId12345', bidderRequestId: 'bidderRequestId12345', - auctionId: 'auctionId12345', + auctionId: 'auctionId12345' }, { bidder: 'cointraffic', @@ -50,7 +52,7 @@ describe('cointrafficBidAdapter', function () { ], bidId: 'bidId67890"', bidderRequestId: 'bidderRequestId67890', - auctionId: 'auctionId12345', + auctionId: 'auctionId12345' } ]; @@ -65,34 +67,71 @@ describe('cointrafficBidAdapter', function () { } }; - const request = spec.buildRequests(bidRequests, bidderRequests); + it('replaces currency with EUR if there is no currency provided', function () { + const request = spec.buildRequests(bidRequests, bidderRequests); + + expect(request[0].data.currency).to.equal('EUR'); + expect(request[1].data.currency).to.equal('EUR'); + }); + + it('replaces currency with EUR if there is no currency provided', function () { + const getConfigStub = sinon.stub(config, 'getConfig').callsFake( + arg => arg === 'currency.bidderCurrencyDefault.cointraffic' ? 'USD' : 'EUR' + ); + + const request = spec.buildRequests(bidRequests, bidderRequests); + + expect(request[0].data.currency).to.equal('USD'); + expect(request[1].data.currency).to.equal('USD'); + + getConfigStub.restore(); + }); + + it('throws an error if currency provided in params is not allowed', function () { + const utilsMock = sinon.mock(utils).expects('logError').twice() + const getConfigStub = sinon.stub(config, 'getConfig').callsFake( + arg => arg === 'currency.bidderCurrencyDefault.cointraffic' ? 'BTC' : 'EUR' + ); + + const request = spec.buildRequests(bidRequests, bidderRequests); + + expect(request[0]).to.undefined; + expect(request[1]).to.undefined; + + utilsMock.restore() + getConfigStub.restore(); + }); it('sends bid request to our endpoint via POST', function () { + const request = spec.buildRequests(bidRequests, bidderRequests); + expect(request[0].method).to.equal('POST'); expect(request[1].method).to.equal('POST'); }); + it('attaches source and version to endpoint URL as query params', function () { + const request = spec.buildRequests(bidRequests, bidderRequests); + expect(request[0].url).to.equal(ENDPOINT_URL); expect(request[1].url).to.equal(ENDPOINT_URL); }); }); describe('interpretResponse', function () { - let bidRequest = [ - { + it('should get the correct bid response', function () { + let bidRequest = [{ method: 'POST', url: ENDPOINT_URL, data: { placementId: 'testPlacementId', device: 'desktop', + currency: 'EUR', sizes: ['300x250'], bidId: 'bidId12345', referer: 'www.example.com' } - } - ]; + }]; - it('should get the correct bid response', function () { let serverResponse = { body: { requestId: 'bidId12345', @@ -103,7 +142,7 @@ describe('cointrafficBidAdapter', function () { height: 250, creativeId: 'creativeId12345', ttl: 90, - ad: '

I am an ad

', + ad: '

I am an ad

' } }; @@ -118,22 +157,99 @@ describe('cointrafficBidAdapter', function () { ttl: 90, ad: '

I am an ad

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

I am an ad

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

I am an ad

' + }]; + + const getConfigStub = sinon.stub(config, 'getConfig').returns('USD'); + + let result = spec.interpretResponse(serverResponse, bidRequest[0]); + expect(Object.keys(result)).to.deep.equal(Object.keys(expectedResponse)); + + getConfigStub.restore(); + }); + + it('should get empty bid response requested currency is not available', function () { + let bidRequest = [{ + method: 'POST', + url: ENDPOINT_URL, + data: { + placementId: 'testPlacementId', + device: 'desktop', + currency: 'BTC', + sizes: ['300x250'], + bidId: 'bidId12345', + referer: 'www.example.com' + } + }]; + + let serverResponse = {}; + let expectedResponse = []; + const getConfigStub = sinon.stub(config, 'getConfig').returns('BTC'); + let result = spec.interpretResponse(serverResponse, bidRequest[0]); expect(Object.keys(result)).to.deep.equal(Object.keys(expectedResponse)); + + getConfigStub.restore(); }); it('should get empty bid response if no server response', function () { + let bidRequest = [{ + method: 'POST', + url: ENDPOINT_URL, + data: { + placementId: 'testPlacementId', + device: 'desktop', + currency: 'EUR', + sizes: ['300x250'], + bidId: 'bidId12345', + referer: 'www.example.com' + } + }]; + let serverResponse = {}; let expectedResponse = []; diff --git a/test/spec/modules/colossussspBidAdapter_spec.js b/test/spec/modules/colossussspBidAdapter_spec.js index df9bdcbd47b..a10a7590677 100644 --- a/test/spec/modules/colossussspBidAdapter_spec.js +++ b/test/spec/modules/colossussspBidAdapter_spec.js @@ -108,7 +108,7 @@ describe('ColossussspAdapter', function () { bid.userId.britepoolid = 'britepoolid123'; bid.userId.idl_env = 'idl_env123'; bid.userId.tdid = 'tdid123'; - bid.userId.id5id = 'id5id123' + bid.userId.id5id = { uid: 'id5id123' }; let serverRequest = spec.buildRequests([bid], bidderRequest); it('Returns valid data if array of bids is valid', function () { let data = serverRequest.data; diff --git a/test/spec/modules/consentManagementUsp_spec.js b/test/spec/modules/consentManagementUsp_spec.js index ee4140afa10..7d3cd48a8e4 100644 --- a/test/spec/modules/consentManagementUsp_spec.js +++ b/test/spec/modules/consentManagementUsp_spec.js @@ -64,6 +64,13 @@ describe('consentManagement', function () { sinon.assert.calledOnce(utils.logWarn); sinon.assert.notCalled(utils.logInfo); }); + + it('should exit consentManagementUsp module if config is "undefined"', function() { + setConsentConfig(undefined); + expect(consentAPI).to.be.undefined; + sinon.assert.calledOnce(utils.logWarn); + sinon.assert.notCalled(utils.logInfo); + }); }); describe('valid setConsentConfig value', function () { diff --git a/test/spec/modules/consentManagement_spec.js b/test/spec/modules/consentManagement_spec.js index deaacbc5a28..5e9b0f07f46 100644 --- a/test/spec/modules/consentManagement_spec.js +++ b/test/spec/modules/consentManagement_spec.js @@ -39,6 +39,12 @@ describe('consentManagement', function () { expect(userCMP).to.be.undefined; sinon.assert.calledOnce(utils.logWarn); }); + + it('should exit consentManagement module if config is "undefined"', function() { + setConsentConfig(undefined); + expect(userCMP).to.be.undefined; + sinon.assert.calledOnce(utils.logWarn); + }); }); describe('valid setConsentConfig value', function () { @@ -531,6 +537,15 @@ describe('consentManagement', function () { // from CMP window postMessage listener. testIFramedPage('with/JSON response', false, 'encoded_consent_data_via_post_message', 1); testIFramedPage('with/String response', true, 'encoded_consent_data_via_post_message', 1); + + it('should contain correct V1 CMP definition', (done) => { + setConsentConfig(goodConfigWithAllowAuction); + requestBidsHook(() => { + const nbArguments = window.__cmp.toString().split('\n')[0].split(', ').length; + expect(nbArguments).to.equal(3); + done(); + }, {}); + }); }); describe('v2 CMP workflow for iframe pages:', function () { @@ -556,6 +571,15 @@ describe('consentManagement', function () { testIFramedPage('with/JSON response', false, 'abc12345234', 2); testIFramedPage('with/String response', true, 'abc12345234', 2); + + it('should contain correct v2 CMP definition', (done) => { + setConsentConfig(goodConfigWithAllowAuction); + requestBidsHook(() => { + const nbArguments = window.__tcfapi.toString().split('\n')[0].split(', ').length; + expect(nbArguments).to.equal(4); + done(); + }, {}); + }); }); }); diff --git a/test/spec/modules/craftBidAdapter_spec.js b/test/spec/modules/craftBidAdapter_spec.js index ef7dd7c3232..3f4bc977016 100644 --- a/test/spec/modules/craftBidAdapter_spec.js +++ b/test/spec/modules/craftBidAdapter_spec.js @@ -81,7 +81,7 @@ describe('craftAdapter', function () { it('sends bid request to ENDPOINT via POST', function () { let request = spec.buildRequests(bidRequests, bidderRequest); expect(request.method).to.equal('POST'); - expect(request.url).to.equal('https://gacraft.jp/prebid-v3'); + expect(request.url).to.equal('https://gacraft.jp/prebid-v3/craft-prebid-example'); let data = JSON.parse(request.data); expect(data.tags).to.deep.equals([{ sitekey: 'craft-prebid-example', diff --git a/test/spec/modules/criteoIdSystem_spec.js b/test/spec/modules/criteoIdSystem_spec.js index aa5807da0da..65e5aaf741d 100644 --- a/test/spec/modules/criteoIdSystem_spec.js +++ b/test/spec/modules/criteoIdSystem_spec.js @@ -71,7 +71,6 @@ describe('CriteoId module', function () { }); it('should call user sync url with the right params', function () { - getCookieStub.withArgs('cto_test_cookie').returns('1'); getCookieStub.withArgs('cto_bundle').returns('bundle'); window.criteo_pubtag = {} @@ -80,7 +79,7 @@ describe('CriteoId module', function () { ajaxBuilderStub.callsFake(mockResponse(undefined, ajaxStub)) criteoIdSubmodule.getId(); - const expectedUrl = `https://gum.criteo.com/sid/json?origin=prebid&topUrl=https%3A%2F%2Ftestdev.com%2F&domain=testdev.com&bundle=bundle&cw=1&pbt=1`; + const expectedUrl = `https://gum.criteo.com/sid/json?origin=prebid&topUrl=https%3A%2F%2Ftestdev.com%2F&domain=testdev.com&bundle=bundle&cw=1&pbt=1&lsw=1`; expect(ajaxStub.calledWith(expectedUrl)).to.be.true; diff --git a/test/spec/modules/dfpAdServerVideo_spec.js b/test/spec/modules/dfpAdServerVideo_spec.js index c0ecb9cad5e..ed9c968cfa2 100644 --- a/test/spec/modules/dfpAdServerVideo_spec.js +++ b/test/spec/modules/dfpAdServerVideo_spec.js @@ -7,6 +7,7 @@ import * as utils from 'src/utils.js'; import { config } from 'src/config.js'; import { targeting } from 'src/targeting.js'; import { auctionManager } from 'src/auctionManager.js'; +import { uspDataHandler } from 'src/adapterManager.js'; import * as adpod from 'modules/adpod.js'; import { server } from 'test/mocks/xhr.js'; @@ -115,6 +116,44 @@ describe('The DFP video support module', function () { expect(customParams).to.have.property('hb_cache_id', bid.videoCacheKey); }); + it('should include the us_privacy key when USP Consent is available', function () { + let uspDataHandlerStub = sinon.stub(uspDataHandler, 'getConsentData'); + uspDataHandlerStub.returns('1YYY'); + + const bidCopy = utils.deepClone(bid); + bidCopy.adserverTargeting = Object.assign(bidCopy.adserverTargeting, { + hb_adid: 'ad_id', + }); + + const url = parse(buildDfpVideoUrl({ + adUnit: adUnit, + bid: bidCopy, + params: { + 'iu': 'my/adUnit' + } + })); + const queryObject = utils.parseQS(url.query); + expect(queryObject.us_privacy).to.equal('1YYY'); + uspDataHandlerStub.restore(); + }); + + it('should not include the us_privacy key when USP Consent is not available', function () { + const bidCopy = utils.deepClone(bid); + bidCopy.adserverTargeting = Object.assign(bidCopy.adserverTargeting, { + hb_adid: 'ad_id', + }); + + const url = parse(buildDfpVideoUrl({ + adUnit: adUnit, + bid: bidCopy, + params: { + 'iu': 'my/adUnit' + } + })); + const queryObject = utils.parseQS(url.query); + expect(queryObject.us_privacy).to.equal(undefined); + }); + describe('special targeting unit test', function () { const allTargetingData = { 'hb_format': 'video', @@ -350,6 +389,8 @@ describe('The DFP video support module', function () { it('should return masterTag url', function() { amStub.returns(getBidsReceived()); + let uspDataHandlerStub = sinon.stub(uspDataHandler, 'getConsentData'); + uspDataHandlerStub.returns('1YYY'); let url; parse(buildAdpodVideoUrl({ code: 'adUnitCode-1', @@ -380,10 +421,12 @@ describe('The DFP video support module', function () { expect(queryParams).to.have.property('unviewed_position_start', '1'); expect(queryParams).to.have.property('url'); expect(queryParams).to.have.property('cust_params'); + expect(queryParams).to.have.property('us_privacy', '1YYY'); const custParams = utils.parseQS(decodeURIComponent(queryParams.cust_params)); expect(custParams).to.have.property('hb_cache_id', '123'); expect(custParams).to.have.property('hb_pb_cat_dur', '15.00_395_15s,15.00_406_30s,10.00_395_15s'); + uspDataHandlerStub.restore(); } }); diff --git a/test/spec/modules/districtmDmxBidAdapter_spec.js b/test/spec/modules/districtmDmxBidAdapter_spec.js index 9b65ab855d8..5d1f299dad5 100644 --- a/test/spec/modules/districtmDmxBidAdapter_spec.js +++ b/test/spec/modules/districtmDmxBidAdapter_spec.js @@ -1,6 +1,6 @@ -import {expect} from 'chai'; +import { expect } from 'chai'; import * as _ from 'lodash'; -import {spec, matchRequest, checkDeepArray, defaultSize, upto5, cleanSizes, shuffle, getApi, bindUserId, getPlaybackmethod, getProtocols, cleanVast} from '../../../modules/districtmDMXBidAdapter.js'; +import { spec, matchRequest, checkDeepArray, defaultSize, upto5, cleanSizes, shuffle, getApi, bindUserId, getPlaybackmethod, getProtocols, cleanVast } from '../../../modules/districtmDMXBidAdapter.js'; const sample_vast = ` @@ -103,7 +103,9 @@ const bidRequest = [{ id: {} } }, - id5id: {}, + id5id: { + uid: '' + }, pubcid: {}, tdid: {}, criteoId: {}, @@ -142,8 +144,12 @@ const bidRequestVideo = [{ 'video/mp4'], } }, - 'mediaTypes': { video: {context: 'instream', // or 'outstream' - playerSize: [[640, 480]]} }, + 'mediaTypes': { + video: { + context: 'instream', // or 'outstream' + playerSize: [[640, 480]] + } + }, 'adUnitCode': 'div-gpt-ad-12345678-1', 'transactionId': 'f6d13fa6-ebc1-41ac-9afa-d8171d22d2c2', 'sizes': [ @@ -600,53 +606,53 @@ const emptyResponseSeatBid = { body: { seatbid: [] } }; describe('DistrictM Adaptor', function () { const districtm = spec; - describe('verification of upto5', function() { - it('upto5 function should always break 12 imps into 3 request same for 15', function() { + describe('verification of upto5', function () { + it('upto5 function should always break 12 imps into 3 request same for 15', function () { expect(upto5([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12], bidRequest, bidderRequest, 'https://google').length).to.be.equal(3) expect(upto5([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15], bidRequest, bidderRequest, 'https://google').length).to.be.equal(3) }) }) - describe('test vast tag', function() { - it('img tag should not be present', function() { + describe('test vast tag', function () { + it('img tag should not be present', function () { expect(cleanVast(sample_vast).indexOf('img') !== -1).to.be.equal(false) }) }) - describe('Test getApi function', function() { + describe('Test getApi function', function () { const data = { - protocols: ['VPAID_1_0'] + api: [1] } - it('Will return 1 for vpaid version 1', function() { + it('Will return 1 for vpaid version 1', function () { expect(getApi(data)[0]).to.be.equal(1) }) - it('Will return 2 for vpaid default', function() { + it('Will return 2 for vpaid default', function () { expect(getApi({})[0]).to.be.equal(2) }) }) - describe('Test cleanSizes function', function() { - it('sequence will be respected', function() { + describe('Test cleanSizes function', function () { + it('sequence will be respected', function () { expect(cleanSizes(bidderRequest.bids[0].sizes).toString()).to.be.equal('300,250,300,600') }) - it('sequence will be respected', function() { + it('sequence will be respected', function () { expect(cleanSizes([[728, 90], [970, 90], [300, 600], [320, 50]]).toString()).to.be.equal('728,90,320,50,300,600,970,90') }) }) - describe('Test getPlaybackmethod function', function() { - it('getPlaybackmethod will return 2', function() { + describe('Test getPlaybackmethod function', function () { + it('getPlaybackmethod will return 2', function () { expect(getPlaybackmethod([])[0]).to.be.equal(2) }) - it('getPlaybackmethod will return 6', function() { + it('getPlaybackmethod will return 6', function () { expect(getPlaybackmethod(['viewport_sound_off'])[0]).to.be.equal(6) }) }) - describe('Test getProtocols function', function() { - it('getProtocols will return 3', function() { - expect(getProtocols({protocols: ['VAST_3_0']})[0]).to.be.equal(3) + describe('Test getProtocols function', function () { + it('getProtocols will return 3', function () { + expect(getProtocols({ protocols: [3] })[0]).to.be.equal(3) }) - it('getProtocols will return 6', function() { + it('getProtocols will return 6', function () { expect(_.isEqual(getProtocols({}), [2, 3, 5, 6, 7, 8])).to.be.equal(true) }) }) @@ -685,7 +691,7 @@ describe('DistrictM Adaptor', function () { memberid: 10003, }; it(`function should return true`, function () { - expect(districtm.isBidRequestValid({params})).to.be.equal(true); + expect(districtm.isBidRequestValid({ params })).to.be.equal(true); }); it(`function should return false`, function () { expect(districtm.isBidRequestValid({ params: { memberid: 12345 } })).to.be.equal(false); @@ -714,19 +720,19 @@ describe('DistrictM Adaptor', function () { it(`the function should return an array`, function () { expect(buildRequestResults).to.be.an('object'); }); - it(`contain gdpr consent & ccpa`, function() { + it(`contain gdpr consent & ccpa`, function () { const bidr = JSON.parse(buildRequestResults.data) expect(bidr.regs.ext.gdpr).to.be.equal(1); expect(bidr.regs.ext.us_privacy).to.be.equal('1NY'); expect(bidr.user.ext.consent).to.be.an('string'); }); - it(`test contain COPPA`, function() { + it(`test contain COPPA`, function () { const bidr = JSON.parse(buildRequestResults.data) bidr.regs = bidr.regs || {}; bidr.regs.coppa = 1; expect(bidr.regs.coppa).to.be.equal(1) }) - it(`test should not contain COPPA`, function() { + it(`test should not contain COPPA`, function () { const bidr = JSON.parse(buildRequestResultsNoCoppa.data) expect(bidr.regs.coppa).to.be.equal(0) }) @@ -735,17 +741,17 @@ describe('DistrictM Adaptor', function () { }); }); - describe('bidRequest Video testing', function() { + describe('bidRequest Video testing', function () { const request = districtm.buildRequests(bidRequestVideo, bidRequestVideo); const data = JSON.parse(request.data) expect(data instanceof Object).to.be.equal(true) }) describe(`interpretResponse test usage`, function () { - const responseResults = districtm.interpretResponse(responses, {bidderRequest}); - const emptyResponseResults = districtm.interpretResponse(emptyResponse, {bidderRequest}); - const emptyResponseResultsNegation = districtm.interpretResponse(responsesNegative, {bidderRequest}); - const emptyResponseResultsEmptySeat = districtm.interpretResponse(emptyResponseSeatBid, {bidderRequest}); + const responseResults = districtm.interpretResponse(responses, { bidderRequest }); + const emptyResponseResults = districtm.interpretResponse(emptyResponse, { bidderRequest }); + const emptyResponseResultsNegation = districtm.interpretResponse(responsesNegative, { bidderRequest }); + const emptyResponseResultsEmptySeat = districtm.interpretResponse(emptyResponseSeatBid, { bidderRequest }); it(`the function should return an array`, function () { expect(responseResults).to.be.an('array'); }); @@ -765,10 +771,10 @@ describe('DistrictM Adaptor', function () { }); describe(`check validation for id sync gdpr ccpa`, () => { - let allin = spec.getUserSyncs({iframeEnabled: true}, {}, bidderRequest.gdprConsent, bidderRequest.uspConsent)[0] - let noCCPA = spec.getUserSyncs({iframeEnabled: true}, {}, bidderRequest.gdprConsent, null)[0] - let noGDPR = spec.getUserSyncs({iframeEnabled: true}, {}, null, bidderRequest.uspConsent)[0] - let nothing = spec.getUserSyncs({iframeEnabled: true}, {}, null, null)[0] + let allin = spec.getUserSyncs({ iframeEnabled: true }, {}, bidderRequest.gdprConsent, bidderRequest.uspConsent)[0] + let noCCPA = spec.getUserSyncs({ iframeEnabled: true }, {}, bidderRequest.gdprConsent, null)[0] + let noGDPR = spec.getUserSyncs({ iframeEnabled: true }, {}, null, bidderRequest.uspConsent)[0] + let nothing = spec.getUserSyncs({ iframeEnabled: true }, {}, null, null)[0] /* @@ -791,10 +797,10 @@ describe('DistrictM Adaptor', function () { }) describe(`Helper function testing`, function () { - const bid = matchRequest('29a28a1bbc8a8d', {bidderRequest}); - const {width, height} = defaultSize(bid); + const bid = matchRequest('29a28a1bbc8a8d', { bidderRequest }); + const { width, height } = defaultSize(bid); it(`test matchRequest`, function () { - expect(matchRequest('29a28a1bbc8a8d', {bidderRequest})).to.be.an('object'); + expect(matchRequest('29a28a1bbc8a8d', { bidderRequest })).to.be.an('object'); }); it(`test checkDeepArray`, function () { expect(_.isEqual(checkDeepArray([728, 90]), [728, 90])).to.be.equal(true); diff --git a/test/spec/modules/eids_spec.js b/test/spec/modules/eids_spec.js index a6a44f9296d..26dbad2f153 100644 --- a/test/spec/modules/eids_spec.js +++ b/test/spec/modules/eids_spec.js @@ -29,15 +29,39 @@ describe('eids array generation for known sub-modules', function() { }); }); - it('id5Id', function() { - const userId = { - id5id: 'some-random-id-value' - }; - const newEids = createEidsArray(userId); - expect(newEids.length).to.equal(1); - expect(newEids[0]).to.deep.equal({ - source: 'id5-sync.com', - uids: [{id: 'some-random-id-value', atype: 1}] + describe('id5Id', function() { + it('does not include an ext if not provided', function() { + const userId = { + id5id: { + uid: 'some-random-id-value' + } + }; + const newEids = createEidsArray(userId); + expect(newEids.length).to.equal(1); + expect(newEids[0]).to.deep.equal({ + source: 'id5-sync.com', + uids: [{ id: 'some-random-id-value', atype: 1 }] + }); + }); + + it('includes ext if provided', function() { + const userId = { + id5id: { + uid: 'some-random-id-value', + ext: { + linkType: 0 + } + } + }; + const newEids = createEidsArray(userId); + expect(newEids.length).to.equal(1); + expect(newEids[0]).to.deep.equal({ + source: 'id5-sync.com', + uids: [{ id: 'some-random-id-value', atype: 1 }], + ext: { + linkType: 0 + } + }); }); }); @@ -237,8 +261,42 @@ describe('eids array generation for known sub-modules', function() { }] }); }); + it('pubProvidedId', function() { + const userId = { + pubProvidedId: [{ + source: 'example.com', + uids: [{ + id: 'value read from cookie or local storage', + ext: { + stype: 'ppuid' + } + }] + }, { + source: 'id-partner.com', + uids: [{ + id: 'value read from cookie or local storage' + }] + }] + }; + const newEids = createEidsArray(userId); + expect(newEids.length).to.equal(2); + expect(newEids[0]).to.deep.equal({ + source: 'example.com', + uids: [{ + id: 'value read from cookie or local storage', + ext: { + stype: 'ppuid' + } + }] + }); + expect(newEids[1]).to.deep.equal({ + source: 'id-partner.com', + uids: [{ + id: 'value read from cookie or local storage' + }] + }); + }); }); - describe('Negative case', function() { it('eids array generation for UN-known sub-module', function() { // UnknownCommonId diff --git a/test/spec/modules/emx_digitalBidAdapter_spec.js b/test/spec/modules/emx_digitalBidAdapter_spec.js index 138786b9c74..39e56638ece 100644 --- a/test/spec/modules/emx_digitalBidAdapter_spec.js +++ b/test/spec/modules/emx_digitalBidAdapter_spec.js @@ -367,6 +367,27 @@ describe('emx_digital Adapter', function () { expect(request.us_privacy).to.exist; expect(request.us_privacy).to.exist.and.to.equal(consentString); }); + + it('should add schain object to request', function() { + const schainBidderRequest = utils.deepClone(bidderRequest); + schainBidderRequest.schain = { + 'complete': 1, + 'ver': '1.0', + 'nodes': [ + { + 'asi': 'testing.com', + 'sid': 'abc', + 'hp': 1 + } + ] + }; + let request = spec.buildRequests(schainBidderRequest.bids, schainBidderRequest); + request = JSON.parse(request.data); + expect(request.source.ext.schain).to.exist; + expect(request.source.ext.schain).to.have.property('complete', 1); + expect(request.source.ext.schain).to.have.property('ver', '1.0'); + expect(request.source.ext.schain.nodes[0].asi).to.equal(schainBidderRequest.schain.nodes[0].asi); + }); }); describe('interpretResponse', function () { diff --git a/test/spec/modules/etargetBidAdapter_spec.js b/test/spec/modules/etargetBidAdapter_spec.js index 4f5e0c224ec..2dbf6cd68c5 100644 --- a/test/spec/modules/etargetBidAdapter_spec.js +++ b/test/spec/modules/etargetBidAdapter_spec.js @@ -154,6 +154,7 @@ describe('etarget adapter', function () { assert.equal(result.height, 250); assert.equal(result.currency, 'EUR'); assert.equal(result.netRevenue, true); + assert.isNotNull(result.reason); assert.equal(result.ttl, 360); assert.equal(result.ad, ''); assert.equal(result.transactionId, '5f33781f-9552-4ca1'); diff --git a/test/spec/modules/fabrickIdSystem_spec.js b/test/spec/modules/fabrickIdSystem_spec.js new file mode 100644 index 00000000000..cbd538816ab --- /dev/null +++ b/test/spec/modules/fabrickIdSystem_spec.js @@ -0,0 +1,106 @@ +import * as utils from '../../../src/utils.js'; +import {server} from '../../mocks/xhr.js'; + +import * as fabrickIdSystem from 'modules/fabrickIdSystem.js'; + +const defaultConfigParams = { + apiKey: '123', + e: 'abc', + p: ['def', 'hij'], + url: 'http://localhost:9999/test/mocks/fabrickId.json?' +}; +const responseHeader = {'Content-Type': 'application/json'} +const fabrickIdSubmodule = fabrickIdSystem.fabrickIdSubmodule; + +describe('Fabrick ID System', function() { + let logErrorStub; + + beforeEach(function () { + logErrorStub = sinon.stub(utils, 'logError'); + }); + + afterEach(function () { + logErrorStub.restore(); + fabrickIdSubmodule.getRefererInfoOverride = null; + }); + + it('should log an error if no configParams were passed into getId', function () { + fabrickIdSubmodule.getId(); + expect(logErrorStub.calledOnce).to.be.true; + }); + + it('should error on json parsing', function() { + let submoduleCallback = fabrickIdSubmodule.getId({ + name: 'fabrickId', + params: defaultConfigParams + }).callback; + let callBackSpy = sinon.spy(); + submoduleCallback(callBackSpy); + let request = server.requests[0]; + request.respond( + 200, + responseHeader, + '] this is not json {' + ); + expect(callBackSpy.calledOnce).to.be.true; + expect(logErrorStub.calledOnce).to.be.true; + }); + + it('should truncate the params', function() { + let r = ''; + for (let i = 0; i < 300; i++) { + r += 'r'; + } + let configParams = Object.assign({}, defaultConfigParams, { + refererInfo: { + referer: r, + stack: ['s-0'], + canonicalUrl: 'cu-0' + } + }); + let submoduleCallback = fabrickIdSubmodule.getId({ + name: 'fabrickId', + params: configParams + }).callback; + let callBackSpy = sinon.spy(); + submoduleCallback(callBackSpy); + let request = server.requests[0]; + r = ''; + for (let i = 0; i < 200; i++) { + r += 'r'; + } + expect(request.url).to.match(new RegExp(`r=${r}&r=`)); + request.respond( + 200, + responseHeader, + JSON.stringify({}) + ); + expect(callBackSpy.calledOnce).to.be.true; + expect(logErrorStub.calledOnce).to.be.false; + }); + + it('should complete successfully', function() { + let configParams = Object.assign({}, defaultConfigParams, { + refererInfo: { + referer: 'r-0', + stack: ['s-0'], + canonicalUrl: 'cu-0' + } + }); + let submoduleCallback = fabrickIdSubmodule.getId({ + name: 'fabrickId', + params: configParams + }).callback; + let callBackSpy = sinon.spy(); + submoduleCallback(callBackSpy); + let request = server.requests[0]; + expect(request.url).to.match(/r=r-0&r=s-0&r=cu-0&r=http/); + request.respond( + 200, + responseHeader, + JSON.stringify({}) + ); + expect(callBackSpy.calledOnce).to.be.true; + expect(logErrorStub.calledOnce).to.be.false; + }); +}); diff --git a/test/spec/modules/freewheel-sspBidAdapter_spec.js b/test/spec/modules/freewheel-sspBidAdapter_spec.js index 3047b635d13..c44d7908ba8 100644 --- a/test/spec/modules/freewheel-sspBidAdapter_spec.js +++ b/test/spec/modules/freewheel-sspBidAdapter_spec.js @@ -152,6 +152,19 @@ describe('freewheelSSP BidAdapter Test', () => { expect(payload.playerSize).to.equal('300x600'); expect(payload._fw_gdpr_consent).to.exist.and.to.be.a('string'); expect(payload._fw_gdpr_consent).to.equal(gdprConsentString); + + let gdprConsent = { + 'gdprApplies': true, + 'consentString': gdprConsentString + } + let syncOptions = { + 'pixelEnabled': true + } + const userSyncs = spec.getUserSyncs(syncOptions, null, gdprConsent, null); + expect(userSyncs).to.deep.equal([{ + type: 'image', + url: 'https://ads.stickyadstv.com/auto-user-sync?gdpr=1&gdpr_consent=1FW-SSP-gdprConsent-' + }]); }); }) @@ -226,6 +239,19 @@ describe('freewheelSSP BidAdapter Test', () => { expect(payload.playerSize).to.equal('300x600'); expect(payload._fw_gdpr_consent).to.exist.and.to.be.a('string'); expect(payload._fw_gdpr_consent).to.equal(gdprConsentString); + + let gdprConsent = { + 'gdprApplies': true, + 'consentString': gdprConsentString + } + let syncOptions = { + 'pixelEnabled': true + } + const userSyncs = spec.getUserSyncs(syncOptions, null, gdprConsent, null); + expect(userSyncs).to.deep.equal([{ + type: 'image', + url: 'https://ads.stickyadstv.com/auto-user-sync?gdpr=1&gdpr_consent=1FW-SSP-gdprConsent-' + }]); }); }) diff --git a/test/spec/modules/gamoshiBidAdapter_spec.js b/test/spec/modules/gamoshiBidAdapter_spec.js index 79f58470cb3..2996b853046 100644 --- a/test/spec/modules/gamoshiBidAdapter_spec.js +++ b/test/spec/modules/gamoshiBidAdapter_spec.js @@ -423,7 +423,7 @@ describe('GamoshiAdapter', () => { it('build request with ID5 Id', () => { const bidRequestClone = utils.deepClone(bidRequest); bidRequestClone.userId = {}; - bidRequestClone.userId.id5id = 'id5-user-id'; + bidRequestClone.userId.id5id = { uid: 'id5-user-id' }; let request = spec.buildRequests([bidRequestClone], bidRequestClone)[0]; expect(request.data.user.ext.eids).to.deep.equal([{ 'source': 'id5-sync.com', diff --git a/test/spec/modules/geoedgeRtdProvider_spec.js b/test/spec/modules/geoedgeRtdProvider_spec.js new file mode 100644 index 00000000000..cf4e0b53fde --- /dev/null +++ b/test/spec/modules/geoedgeRtdProvider_spec.js @@ -0,0 +1,111 @@ +import * as utils from '../../../src/utils.js'; +import * as hook from '../../../src/hook.js' +import { beforeInit, geoedgeSubmodule, setWrapper, wrapper, htmlPlaceholder, WRAPPER_URL, getClientUrl } from '../../../modules/geoedgeRtdProvider.js'; +import { server } from '../../../test/mocks/xhr.js'; + +let key = '123123123'; +function makeConfig() { + return { + name: 'geoedge', + params: { + wap: false, + key: key, + bidders: { + bidderA: true, + bidderB: false + } + } + }; +} + +function mockBid(bidderCode) { + return { + 'ad': '', + 'cpm': '1.00', + 'width': 300, + 'height': 250, + 'bidderCode': bidderCode, + 'requestId': utils.getUniqueIdentifierStr(), + 'creativeId': 'id', + 'currency': 'USD', + 'netRevenue': true, + 'ttl': 360 + }; +} + +let mockWrapper = `${htmlPlaceholder}`; + +describe('Geoedge RTD module', function () { + describe('beforeInit', function () { + let submoduleStub; + + before(function () { + submoduleStub = sinon.stub(hook, 'submodule'); + }); + after(function () { + submoduleStub.restore(); + }); + it('should fetch the wrapper', function () { + beforeInit(); + let request = server.requests[0]; + let isWrapperRequest = request && request.url && request.url && request.url === WRAPPER_URL; + expect(isWrapperRequest).to.equal(true); + }); + it('should register RTD submodule provider', function () { + expect(submoduleStub.calledWith('realTimeData', geoedgeSubmodule)).to.equal(true); + }); + }); + describe('setWrapper', function () { + it('should set the wrapper', function () { + setWrapper(mockWrapper); + expect(wrapper).to.equal(mockWrapper); + }); + }); + describe('submodule', function () { + describe('name', function () { + it('should be geoedge', function () { + expect(geoedgeSubmodule.name).to.equal('geoedge'); + }); + }); + describe('init', function () { + let insertElementStub; + + before(function () { + insertElementStub = sinon.stub(utils, 'insertElement'); + }); + after(function () { + utils.insertElement.restore(); + }); + it('should return false when missing params or key', function () { + let missingParams = geoedgeSubmodule.init({}); + let missingKey = geoedgeSubmodule.init({ params: {} }); + expect(missingParams || missingKey).to.equal(false); + }); + it('should return true when params are ok', function () { + expect(geoedgeSubmodule.init(makeConfig())).to.equal(true); + }); + it('should preload the client', function () { + let isLinkPreloadAsScript = arg => arg.tagName === 'LINK' && arg.rel === 'preload' && arg.as === 'script' && arg.href === getClientUrl(key); + expect(insertElementStub.calledWith(sinon.match(isLinkPreloadAsScript))).to.equal(true); + }); + }); + describe('onBidResponseEvent', function () { + let bidFromA = mockBid('bidderA'); + it('should wrap bid html when bidder is configured', function () { + geoedgeSubmodule.onBidResponseEvent(bidFromA, makeConfig()); + expect(bidFromA.ad.indexOf('')).to.equal(0); + }); + it('should not wrap bid html when bidder is not configured', function () { + let bidFromB = mockBid('bidderB'); + geoedgeSubmodule.onBidResponseEvent(bidFromB, makeConfig()); + expect(bidFromB.ad.indexOf('')).to.equal(-1); + }); + it('should only muatate the bid ad porperty', function () { + let copy = Object.assign({}, bidFromA); + delete copy.ad; + let equalsOriginal = Object.keys(copy).every(key => copy[key] === bidFromA[key]); + expect(equalsOriginal).to.equal(true); + }); + }); + }); +}); diff --git a/test/spec/modules/gjirafaBidAdapter_spec.js b/test/spec/modules/gjirafaBidAdapter_spec.js index 566b1243f62..db9b82e0a10 100644 --- a/test/spec/modules/gjirafaBidAdapter_spec.js +++ b/test/spec/modules/gjirafaBidAdapter_spec.js @@ -31,7 +31,7 @@ describe('gjirafaAdapterTest', () => { })).to.equal(false); }); - it('bidRequest without propertyId orplacementId', () => { + it('bidRequest without propertyId or placementId', () => { expect(spec.isBidRequestValid({ bidder: 'gjirafa', params: { @@ -80,7 +80,11 @@ describe('gjirafaAdapterTest', () => { it('bidRequest sizes', () => { const requests = spec.buildRequests(bidRequests); - expect(requests[0].data.sizes).to.equal('728x90'); + requests.forEach(function (requestItem) { + expect(requestItem.data.placements).to.exist; + expect(requestItem.data.placements.length).to.equal(1); + expect(requestItem.data.placements[0].sizes).to.equal('728x90'); + }); }); }); @@ -128,7 +132,9 @@ describe('gjirafaAdapterTest', () => { 'netRevenue', 'ttl', 'referrer', - 'ad' + 'ad', + 'vastUrl', + 'mediaType' ]; let resultKeys = Object.keys(result[0]); diff --git a/test/spec/modules/gridBidAdapter_spec.js b/test/spec/modules/gridBidAdapter_spec.js index 650712e435f..e884df40c5e 100644 --- a/test/spec/modules/gridBidAdapter_spec.js +++ b/test/spec/modules/gridBidAdapter_spec.js @@ -40,229 +40,6 @@ describe('TheMediaGrid Adapter', function () { }); describe('buildRequests', function () { - function parseRequest(url) { - const res = {}; - url.split('&').forEach((it) => { - const couple = it.split('='); - res[couple[0]] = decodeURIComponent(couple[1]); - }); - return res; - } - const bidderRequest = {refererInfo: {referer: 'https://example.com'}}; - const referrer = bidderRequest.refererInfo.referer; - let bidRequests = [ - { - 'bidder': 'grid', - 'params': { - 'uid': '1' - }, - 'adUnitCode': 'adunit-code-1', - 'sizes': [[300, 250], [300, 600]], - 'bidId': '30b31c1838de1e', - 'bidderRequestId': '22edbae2733bf6', - 'auctionId': '1d1a030790a475', - }, - { - 'bidder': 'grid', - 'params': { - 'uid': '1' - }, - 'adUnitCode': 'adunit-code-2', - 'sizes': [[728, 90]], - 'mediaTypes': { - 'video': { - 'playerSize': [400, 600] - }, - 'banner': { - 'sizes': [[728, 90]] - } - }, - 'bidId': '3150ccb55da321', - 'bidderRequestId': '22edbae2733bf6', - 'auctionId': '1d1a030790a475', - }, - { - 'bidder': 'grid', - 'params': { - 'uid': '2' - }, - 'adUnitCode': 'adunit-code-1', - 'sizes': [[300, 250], [300, 600]], - 'mediaTypes': { - 'video': { - 'playerSize': [400, 600] - }, - 'banner': { - 'sizes': [[300, 250], [300, 600]] - } - }, - 'bidId': '42dbe3a7168a6a', - 'bidderRequestId': '22edbae2733bf6', - 'auctionId': '1d1a030790a475', - } - ]; - - it('should attach valid params to the tag', function () { - const [request] = spec.buildRequests([bidRequests[0]], bidderRequest); - expect(request.data).to.be.an('string'); - const payload = parseRequest(request.data); - expect(payload).to.have.property('u', referrer); - expect(payload).to.have.property('auids', '1'); - expect(payload).to.have.property('sizes', '300x250,300x600'); - expect(payload).to.have.property('r', '22edbae2733bf6'); - expect(payload).to.have.property('wrapperType', 'Prebid_js'); - expect(payload).to.have.property('wrapperVersion', '$prebid.version$'); - }); - - it('sizes must be added from mediaTypes', function () { - const [request] = spec.buildRequests([bidRequests[0], bidRequests[1]], bidderRequest); - expect(request.data).to.be.an('string'); - const payload = parseRequest(request.data); - expect(payload).to.have.property('u', referrer); - expect(payload).to.have.property('auids', '1,1'); - expect(payload).to.have.property('sizes', '300x250,300x600,728x90,400x600'); - expect(payload).to.have.property('r', '22edbae2733bf6'); - }); - - it('sizes must not be duplicated', function () { - const [request] = spec.buildRequests(bidRequests, bidderRequest); - expect(request.data).to.be.an('string'); - const payload = parseRequest(request.data); - expect(payload).to.have.property('u', referrer); - expect(payload).to.have.property('auids', '1,1,2'); - expect(payload).to.have.property('sizes', '300x250,300x600,728x90,400x600'); - expect(payload).to.have.property('r', '22edbae2733bf6'); - }); - - it('if gdprConsent is present payload must have gdpr params', function () { - const [request] = spec.buildRequests(bidRequests, {gdprConsent: {consentString: 'AAA', gdprApplies: true}, refererInfo: bidderRequest.refererInfo}); - expect(request.data).to.be.an('string'); - const payload = parseRequest(request.data); - expect(payload).to.have.property('u', referrer); - expect(payload).to.have.property('gdpr_consent', 'AAA'); - expect(payload).to.have.property('gdpr_applies', '1'); - }); - - it('if gdprApplies is false gdpr_applies must be 0', function () { - const [request] = spec.buildRequests(bidRequests, {gdprConsent: {consentString: 'AAA', gdprApplies: false}}); - expect(request.data).to.be.an('string'); - const payload = parseRequest(request.data); - expect(payload).to.have.property('gdpr_consent', 'AAA'); - expect(payload).to.have.property('gdpr_applies', '0'); - }); - - it('if gdprApplies is undefined gdpr_applies must be 1', function () { - const [request] = spec.buildRequests(bidRequests, {gdprConsent: {consentString: 'AAA'}}); - expect(request.data).to.be.an('string'); - const payload = parseRequest(request.data); - expect(payload).to.have.property('gdpr_consent', 'AAA'); - expect(payload).to.have.property('gdpr_applies', '1'); - }); - - it('if usPrivacy is present payload must have us_privacy param', function () { - const bidderRequestWithUSP = Object.assign({uspConsent: '1YNN'}, bidderRequest); - const [request] = spec.buildRequests(bidRequests, bidderRequestWithUSP); - expect(request.data).to.be.an('string'); - const payload = parseRequest(request.data); - expect(payload).to.have.property('us_privacy', '1YNN'); - }); - - it('should convert keyword params to proper form and attaches to request', function () { - const bidRequestWithKeywords = [].concat(bidRequests); - bidRequestWithKeywords[1] = Object.assign({}, - bidRequests[1], - { - params: { - uid: '1', - keywords: { - single: 'val', - singleArr: ['val'], - singleArrNum: [3], - multiValMixed: ['value1', 2, 'value3'], - singleValNum: 123, - emptyStr: '', - emptyArr: [''], - badValue: {'foo': 'bar'} // should be dropped - } - } - } - ); - - const [request] = spec.buildRequests(bidRequestWithKeywords, bidderRequest); - expect(request.data).to.be.an('string'); - const payload = parseRequest(request.data); - expect(payload.keywords).to.be.an('string'); - payload.keywords = JSON.parse(payload.keywords); - - expect(payload.keywords).to.deep.equal([{ - 'key': 'single', - 'value': ['val'] - }, { - 'key': 'singleArr', - 'value': ['val'] - }, { - 'key': 'singleArrNum', - 'value': ['3'] - }, { - 'key': 'multiValMixed', - 'value': ['value1', '2', 'value3'] - }, { - 'key': 'singleValNum', - 'value': ['123'] - }, { - 'key': 'emptyStr' - }, { - 'key': 'emptyArr' - }]); - }); - - it('should mix keyword param with keywords from config', function () { - const getConfigStub = sinon.stub(config, 'getConfig').callsFake( - arg => arg === 'fpd.user' ? {'keywords': ['a', 'b']} : arg === 'fpd.context' ? {'keywords': ['any words']} : null); - - const bidRequestWithKeywords = [].concat(bidRequests); - bidRequestWithKeywords[1] = Object.assign({}, - bidRequests[1], - { - params: { - uid: '1', - keywords: { - single: 'val', - singleArr: ['val'], - multiValMixed: ['value1', 2, 'value3'] - } - } - } - ); - - const [request] = spec.buildRequests(bidRequestWithKeywords, bidderRequest); - expect(request.data).to.be.an('string'); - const payload = parseRequest(request.data); - expect(payload.keywords).to.be.an('string'); - payload.keywords = JSON.parse(payload.keywords); - - expect(payload.keywords).to.deep.equal([{ - 'key': 'single', - 'value': ['val'] - }, { - 'key': 'singleArr', - 'value': ['val'] - }, { - 'key': 'multiValMixed', - 'value': ['value1', '2', 'value3'] - }, { - 'key': 'user', - 'value': ['a', 'b'] - }, { - 'key': 'context', - 'value': ['any words'] - }]); - - getConfigStub.restore(); - }); - }); - - describe('buildRequests in new format', function () { function parseRequest(data) { return JSON.parse(data); } @@ -278,7 +55,7 @@ describe('TheMediaGrid Adapter', function () { 'bidder': 'grid', 'params': { 'uid': '1', - 'useNewFormat': true + 'bidFloor': 1.25 }, 'adUnitCode': 'adunit-code-1', 'sizes': [[300, 250], [300, 600]], @@ -294,8 +71,7 @@ describe('TheMediaGrid Adapter', function () { { 'bidder': 'grid', 'params': { - 'uid': '2', - 'useNewFormat': true + 'uid': '2' }, 'adUnitCode': 'adunit-code-1', 'sizes': [[300, 250], [300, 600]], @@ -306,8 +82,7 @@ describe('TheMediaGrid Adapter', function () { { 'bidder': 'grid', 'params': { - 'uid': '11', - 'useNewFormat': true + 'uid': '11' }, 'adUnitCode': 'adunit-code-2', 'sizes': [[728, 90]], @@ -324,8 +99,7 @@ describe('TheMediaGrid Adapter', function () { { 'bidder': 'grid', 'params': { - 'uid': '3', - 'useNewFormat': true + 'uid': '3' }, 'adUnitCode': 'adunit-code-2', 'sizes': [[728, 90]], @@ -344,7 +118,7 @@ describe('TheMediaGrid Adapter', function () { ]; it('should attach valid params to the tag', function () { - const [request] = spec.buildRequests([bidRequests[0]], bidderRequest); + const request = spec.buildRequests([bidRequests[0]], bidderRequest); expect(request.data).to.be.an('string'); const payload = parseRequest(request.data); expect(payload).to.deep.equal({ @@ -361,6 +135,7 @@ describe('TheMediaGrid Adapter', function () { 'id': bidRequests[0].bidId, 'tagid': bidRequests[0].params.uid, 'ext': {'divid': bidRequests[0].adUnitCode}, + 'bidfloor': bidRequests[0].params.bidFloor, 'banner': { 'w': 300, 'h': 250, @@ -371,7 +146,7 @@ describe('TheMediaGrid Adapter', function () { }); it('make possible to process request without mediaTypes', function () { - const [request] = spec.buildRequests([bidRequests[0], bidRequests[1]], bidderRequest); + const request = spec.buildRequests([bidRequests[0], bidRequests[1]], bidderRequest); expect(request.data).to.be.an('string'); const payload = parseRequest(request.data); expect(payload).to.deep.equal({ @@ -388,6 +163,7 @@ describe('TheMediaGrid Adapter', function () { 'id': bidRequests[0].bidId, 'tagid': bidRequests[0].params.uid, 'ext': {'divid': bidRequests[0].adUnitCode}, + 'bidfloor': bidRequests[0].params.bidFloor, 'banner': { 'w': 300, 'h': 250, @@ -407,7 +183,7 @@ describe('TheMediaGrid Adapter', function () { }); it('should attach valid params to the video tag', function () { - const [request] = spec.buildRequests(bidRequests.slice(0, 3), bidderRequest); + const request = spec.buildRequests(bidRequests.slice(0, 3), bidderRequest); expect(request.data).to.be.an('string'); const payload = parseRequest(request.data); expect(payload).to.deep.equal({ @@ -424,6 +200,7 @@ describe('TheMediaGrid Adapter', function () { 'id': bidRequests[0].bidId, 'tagid': bidRequests[0].params.uid, 'ext': {'divid': bidRequests[0].adUnitCode}, + 'bidfloor': bidRequests[0].params.bidFloor, 'banner': { 'w': 300, 'h': 250, @@ -452,7 +229,7 @@ describe('TheMediaGrid Adapter', function () { }); it('should support mixed mediaTypes', function () { - const [request] = spec.buildRequests(bidRequests, bidderRequest); + const request = spec.buildRequests(bidRequests, bidderRequest); expect(request.data).to.be.an('string'); const payload = parseRequest(request.data); expect(payload).to.deep.equal({ @@ -469,6 +246,7 @@ describe('TheMediaGrid Adapter', function () { 'id': bidRequests[0].bidId, 'tagid': bidRequests[0].params.uid, 'ext': {'divid': bidRequests[0].adUnitCode}, + 'bidfloor': bidRequests[0].params.bidFloor, 'banner': { 'w': 300, 'h': 250, @@ -511,7 +289,7 @@ describe('TheMediaGrid Adapter', function () { it('if gdprConsent is present payload must have gdpr params', function () { const gdprBidderRequest = Object.assign({gdprConsent: {consentString: 'AAA', gdprApplies: true}}, bidderRequest); - const [request] = spec.buildRequests(bidRequests, gdprBidderRequest); + const request = spec.buildRequests(bidRequests, gdprBidderRequest); expect(request.data).to.be.an('string'); const payload = parseRequest(request.data); expect(payload).to.have.property('user'); @@ -524,7 +302,7 @@ describe('TheMediaGrid Adapter', function () { it('if usPrivacy is present payload must have us_privacy param', function () { const bidderRequestWithUSP = Object.assign({uspConsent: '1YNN'}, bidderRequest); - const [request] = spec.buildRequests(bidRequests, bidderRequestWithUSP); + const request = spec.buildRequests(bidRequests, bidderRequestWithUSP); expect(request.data).to.be.an('string'); const payload = parseRequest(request.data); expect(payload).to.have.property('regs'); @@ -536,22 +314,65 @@ describe('TheMediaGrid Adapter', function () { const bidRequestsWithUserIds = bidRequests.map((bid) => { return Object.assign({ userId: { - id5id: 'id5id_1', + id5id: { uid: 'id5id_1', ext: { linkType: 2 } }, tdid: 'tdid_1', digitrustid: {data: {id: 'DTID', keyv: 4, privacy: {optout: false}, producer: 'ABC', version: 2}}, - lipb: {lipbid: 'lipb_1'} + lipb: {lipbid: 'lipb_1'}, + idl_env: 'idl_env_1', + criteoId: 'criteoId_1' } }, bid); }); - const [request] = spec.buildRequests(bidRequestsWithUserIds, bidderRequest); + const request = spec.buildRequests(bidRequestsWithUserIds, bidderRequest); expect(request.data).to.be.an('string'); const payload = parseRequest(request.data); expect(payload).to.have.property('user'); expect(payload.user).to.have.property('ext'); - expect(payload.user.ext).to.have.property('unifiedid', 'tdid_1'); - expect(payload.user.ext).to.have.property('id5id', 'id5id_1'); - expect(payload.user.ext).to.have.property('digitrustid', 'DTID'); - expect(payload.user.ext).to.have.property('liveintentid', 'lipb_1'); + expect(payload.user.ext.digitrust).to.deep.equal({ + id: 'DTID', + keyv: 4, + privacy: { + optout: false + }, + producer: 'ABC', + version: 2 + }); + expect(payload.user.ext.eids).to.deep.equal([ + { + source: 'adserver.org', + uids: [{ + id: 'tdid_1', + ext: { + rtiPartner: 'TDID' + } + }] + }, + { + source: 'id5-sync.com', + uids: [{ + id: 'id5id_1' + }], + ext: { linkType: 2 } + }, + { + source: 'liveintent.com', + uids: [{ + id: 'lipb_1' + }] + }, + { + source: 'identityLink', + uids: [{ + id: 'idl_env_1' + }] + }, + { + source: 'criteo.com', + uids: [{ + id: 'criteoId_1' + }] + } + ]); }); it('if schain is present payload must have source.ext.schain param', function () { @@ -570,7 +391,7 @@ describe('TheMediaGrid Adapter', function () { schain: schain }, bid); }); - const [request] = spec.buildRequests(bidRequestsWithSChain, bidderRequest); + const request = spec.buildRequests(bidRequestsWithSChain, bidderRequest); expect(request.data).to.be.an('string'); const payload = parseRequest(request.data); expect(payload).to.have.property('source'); @@ -579,20 +400,22 @@ describe('TheMediaGrid Adapter', function () { expect(payload.source.ext.schain).to.deep.equal(schain); }); - it('if content and segment is present in realTimeData.jwTargeting, payload must have right params', function () { + it('if content and segment is present in jwTargeting, payload must have right params', function () { const jsContent = {id: 'test_jw_content_id'}; const jsSegments = ['test_seg_1', 'test_seg_2']; const bidRequestsWithUserIds = bidRequests.map((bid) => { return Object.assign({ - realTimeData: { - jwTargeting: { - segments: jsSegments, - content: jsContent + rtd: { + jwplayer: { + targeting: { + segments: jsSegments, + content: jsContent + } } } }, bid); }); - const [request] = spec.buildRequests(bidRequestsWithUserIds, bidderRequest); + const request = spec.buildRequests(bidRequestsWithUserIds, bidderRequest); expect(request.data).to.be.an('string'); const payload = parseRequest(request.data); expect(payload).to.have.property('user'); @@ -606,14 +429,68 @@ describe('TheMediaGrid Adapter', function () { expect(payload).to.have.property('site'); expect(payload.site.content).to.deep.equal(jsContent); }); + + it('shold be right tmax when timeout in config is less then timeout in bidderRequest', function() { + const getConfigStub = sinon.stub(config, 'getConfig').callsFake( + arg => arg === 'bidderTimeout' ? 2000 : null); + const request = spec.buildRequests([bidRequests[0]], bidderRequest); + expect(request.data).to.be.an('string'); + const payload = parseRequest(request.data); + expect(payload.tmax).to.equal(2000); + getConfigStub.restore(); + }); + it('shold be right tmax when timeout in bidderRequest is less then timeout in config', function() { + const getConfigStub = sinon.stub(config, 'getConfig').callsFake( + arg => arg === 'bidderTimeout' ? 5000 : null); + const request = spec.buildRequests([bidRequests[0]], bidderRequest); + expect(request.data).to.be.an('string'); + const payload = parseRequest(request.data); + expect(payload.tmax).to.equal(3000); + getConfigStub.restore(); + }); + describe('floorModule', function () { + const floorTestData = { + 'currency': 'USD', + 'floor': 1.50 + }; + const bidRequest = Object.assign({ + getFloor: (_) => { + return floorTestData; + } + }, bidRequests[1]); + it('should return the value from getFloor if present', function () { + const request = spec.buildRequests([bidRequest], bidderRequest); + expect(request.data).to.be.an('string'); + const payload = parseRequest(request.data); + expect(payload.imp[0].bidfloor).to.equal(floorTestData.floor); + }); + it('should return the getFloor.floor value if it is greater than bidfloor', function () { + const bidfloor = 0.80; + const bidRequestsWithFloor = { ...bidRequest }; + bidRequestsWithFloor.params = Object.assign({bidFloor: bidfloor}, bidRequestsWithFloor.params); + const request = spec.buildRequests([bidRequestsWithFloor], bidderRequest); + expect(request.data).to.be.an('string'); + const payload = parseRequest(request.data); + expect(payload.imp[0].bidfloor).to.equal(floorTestData.floor); + }); + it('should return the bidfloor value if it is greater than getFloor.floor', function () { + const bidfloor = 1.80; + const bidRequestsWithFloor = { ...bidRequest }; + bidRequestsWithFloor.params = Object.assign({bidFloor: bidfloor}, bidRequestsWithFloor.params); + const request = spec.buildRequests([bidRequestsWithFloor], bidderRequest); + expect(request.data).to.be.an('string'); + const payload = parseRequest(request.data); + expect(payload.imp[0].bidfloor).to.equal(bidfloor); + }); + }); }); describe('interpretResponse', function () { const responses = [ - {'bid': [{'price': 1.15, 'adm': '
test content 1
', 'auid': 1, 'h': 250, 'w': 300, dealid: 11}], 'seat': '1'}, - {'bid': [{'price': 0.5, 'adm': '
test content 2
', 'auid': 2, 'h': 600, 'w': 300}], 'seat': '1'}, - {'bid': [{'price': 0.15, 'adm': '
test content 3
', 'auid': 1, 'h': 90, 'w': 728}], 'seat': '1'}, - {'bid': [{'price': 0, 'auid': 3, 'h': 250, 'w': 300}], 'seat': '1'}, + {'bid': [{'impid': '659423fff799cb', 'price': 1.15, 'adm': '
test content 1
', 'auid': 1, 'h': 250, 'w': 300, 'dealid': 11}], 'seat': '1'}, + {'bid': [{'impid': '4dff80cc4ee346', 'price': 0.5, 'adm': '
test content 2
', 'auid': 2, 'h': 600, 'w': 300}], 'seat': '1'}, + {'bid': [{'impid': '5703af74d0472a', 'price': 0.15, 'adm': '
test content 3
', 'auid': 1, 'h': 90, 'w': 728}], 'seat': '1'}, + {'bid': [{'impid': '2344da98f78b42', 'price': 0, 'auid': 3, 'h': 250, 'w': 300}], 'seat': '1'}, {'bid': [{'price': 0, 'adm': '
test content 5
', 'h': 250, 'w': 300}], 'seat': '1'}, undefined, {'bid': [], 'seat': '1'}, @@ -634,7 +511,7 @@ describe('TheMediaGrid Adapter', function () { 'auctionId': '1cbd2feafe5e8b', } ]; - const [request] = spec.buildRequests(bidRequests); + const request = spec.buildRequests(bidRequests); const expectedResponse = [ { 'requestId': '659423fff799cb', @@ -665,7 +542,7 @@ describe('TheMediaGrid Adapter', function () { }, 'adUnitCode': 'adunit-code-1', 'sizes': [[300, 250], [300, 600]], - 'bidId': '300bfeb0d71a5b', + 'bidId': '659423fff799cb', 'bidderRequestId': '2c2bb1972df9a', 'auctionId': '1fa09aee5c8c99', }, @@ -692,10 +569,10 @@ describe('TheMediaGrid Adapter', function () { 'auctionId': '1fa09aee5c8c99', } ]; - const [request] = spec.buildRequests(bidRequests); + const request = spec.buildRequests(bidRequests); const expectedResponse = [ { - 'requestId': '300bfeb0d71a5b', + 'requestId': '659423fff799cb', 'cpm': 1.15, 'creativeId': 1, 'dealId': 11, @@ -778,10 +655,10 @@ describe('TheMediaGrid Adapter', function () { } ]; const response = [ - {'bid': [{'price': 1.15, 'adm': '\n<\/Ad>\n<\/VAST>', 'auid': 11, content_type: 'video', w: 300, h: 600}], 'seat': '2'}, - {'bid': [{'price': 1.00, 'adm': '\n<\/Ad>\n<\/VAST>', 'auid': 12, content_type: 'video'}], 'seat': '2'} + {'bid': [{'impid': '659423fff799cb', 'price': 1.15, 'adm': '\n<\/Ad>\n<\/VAST>', 'auid': 11, content_type: 'video', w: 300, h: 600}], 'seat': '2'}, + {'bid': [{'impid': '2bc598e42b6a', 'price': 1.00, 'adm': '\n<\/Ad>\n<\/VAST>', 'auid': 12, content_type: 'video'}], 'seat': '2'} ]; - const [request] = spec.buildRequests(bidRequests); + const request = spec.buildRequests(bidRequests); const expectedResponse = [ { 'requestId': '659423fff799cb', @@ -799,6 +676,23 @@ describe('TheMediaGrid Adapter', function () { 'adResponse': { 'content': '\n<\/Ad>\n<\/VAST>' } + }, + { + 'requestId': '2bc598e42b6a', + 'cpm': 1.00, + 'creativeId': 12, + 'dealId': undefined, + 'width': undefined, + 'height': undefined, + 'bidderCode': 'grid', + 'currency': 'USD', + 'mediaType': 'video', + 'netRevenue': false, + 'ttl': 360, + 'vastXml': '\n<\/Ad>\n<\/VAST>', + 'adResponse': { + 'content': '\n<\/Ad>\n<\/VAST>' + } } ]; @@ -842,18 +736,18 @@ describe('TheMediaGrid Adapter', function () { 'auctionId': '1fa09aee5c84d34', } ]; - const [request] = spec.buildRequests(bidRequests); + const request = spec.buildRequests(bidRequests); const result = spec.interpretResponse({'body': {'seatbid': responses.slice(2)}}, request); expect(result.length).to.equal(0); }); it('complicated case', function () { const fullResponse = [ - {'bid': [{'price': 1.15, 'adm': '
test content 1
', 'auid': 1, 'h': 250, 'w': 300, dealid: 11}], 'seat': '1'}, - {'bid': [{'price': 0.5, 'adm': '
test content 2
', 'auid': 2, 'h': 600, 'w': 300, dealid: 12}], 'seat': '1'}, - {'bid': [{'price': 0.15, 'adm': '
test content 3
', 'auid': 1, 'h': 90, 'w': 728}], 'seat': '1'}, - {'bid': [{'price': 0.15, 'adm': '
test content 4
', 'auid': 1, 'h': 600, 'w': 300}], 'seat': '1'}, - {'bid': [{'price': 0.5, 'adm': '
test content 5
', 'auid': 2, 'h': 600, 'w': 350}], 'seat': '1'}, + {'bid': [{'impid': '2164be6358b9', 'price': 1.15, 'adm': '
test content 1
', 'auid': 1, 'h': 250, 'w': 300, dealid: 11}], 'seat': '1'}, + {'bid': [{'impid': '4e111f1b66e4', 'price': 0.5, 'adm': '
test content 2
', 'auid': 2, 'h': 600, 'w': 300, dealid: 12}], 'seat': '1'}, + {'bid': [{'impid': '26d6f897b516', 'price': 0.15, 'adm': '
test content 3
', 'auid': 1, 'h': 90, 'w': 728}], 'seat': '1'}, + {'bid': [{'impid': '326bde7fbf69', 'price': 0.15, 'adm': '
test content 4
', 'auid': 1, 'h': 600, 'w': 300}], 'seat': '1'}, + {'bid': [{'impid': '2234f233b22a', 'price': 0.5, 'adm': '
test content 5
', 'auid': 2, 'h': 600, 'w': 350}], 'seat': '1'}, ]; const bidRequests = [ { @@ -912,7 +806,7 @@ describe('TheMediaGrid Adapter', function () { 'auctionId': '32a1f276cb87cb8', } ]; - const [request] = spec.buildRequests(bidRequests); + const request = spec.buildRequests(bidRequests); const expectedResponse = [ { 'requestId': '2164be6358b9', @@ -975,82 +869,6 @@ describe('TheMediaGrid Adapter', function () { const result = spec.interpretResponse({'body': {'seatbid': fullResponse}}, request); expect(result).to.deep.equal(expectedResponse); }); - - it('dublicate uids and sizes in one slot', function () { - const fullResponse = [ - {'bid': [{'price': 1.15, 'adm': '
test content 1
', 'auid': 1, 'h': 250, 'w': 300}], 'seat': '1'}, - {'bid': [{'price': 0.5, 'adm': '
test content 2
', 'auid': 1, 'h': 250, 'w': 300}], 'seat': '1'}, - ]; - const bidRequests = [ - { - 'bidder': 'grid', - 'params': { - 'uid': '1' - }, - 'adUnitCode': 'adunit-code-1', - 'sizes': [[300, 250], [300, 600]], - 'bidId': '5126e301f4be', - 'bidderRequestId': '171c5405a390', - 'auctionId': '35bcbc0f7e79c', - }, - { - 'bidder': 'grid', - 'params': { - 'uid': '1' - }, - 'adUnitCode': 'adunit-code-1', - 'sizes': [[300, 250], [300, 600]], - 'bidId': '57b2ebe70e16', - 'bidderRequestId': '171c5405a390', - 'auctionId': '35bcbc0f7e79c', - }, - { - 'bidder': 'grid', - 'params': { - 'uid': '1' - }, - 'adUnitCode': 'adunit-code-1', - 'sizes': [[300, 250], [300, 600]], - 'bidId': '225fcd44b18c', - 'bidderRequestId': '171c5405a390', - 'auctionId': '35bcbc0f7e79c', - } - ]; - const [request] = spec.buildRequests(bidRequests); - const expectedResponse = [ - { - 'requestId': '5126e301f4be', - 'cpm': 1.15, - 'creativeId': 1, - 'dealId': undefined, - 'width': 300, - 'height': 250, - 'ad': '
test content 1
', - 'bidderCode': 'grid', - 'currency': 'USD', - 'mediaType': 'banner', - 'netRevenue': false, - 'ttl': 360, - }, - { - 'requestId': '57b2ebe70e16', - 'cpm': 0.5, - 'creativeId': 1, - 'dealId': undefined, - 'width': 300, - 'height': 250, - 'ad': '
test content 2
', - 'bidderCode': 'grid', - 'currency': 'USD', - 'mediaType': 'banner', - 'netRevenue': false, - 'ttl': 360, - } - ]; - - const result = spec.interpretResponse({'body': {'seatbid': fullResponse}}, request); - expect(result).to.deep.equal(expectedResponse); - }); }); describe('user sync', function () { diff --git a/test/spec/modules/gumgumBidAdapter_spec.js b/test/spec/modules/gumgumBidAdapter_spec.js index f24fdc87e45..52a3a21db4e 100644 --- a/test/spec/modules/gumgumBidAdapter_spec.js +++ b/test/spec/modules/gumgumBidAdapter_spec.js @@ -1,6 +1,7 @@ import { expect } from 'chai'; import { newBidder } from 'src/adapters/bidderFactory.js'; import { spec } from 'modules/gumgumBidAdapter.js'; +import { BANNER, VIDEO } from 'src/mediaTypes.js'; const ENDPOINT = 'https://g2.gumgum.com/hbid/imp'; const JCSI = { t: 0, rq: 8, pbv: '$prebid.version$' } @@ -252,6 +253,25 @@ describe('gumgumAdapter', function () { expect(bidRequest.data).to.include.any.keys('t'); expect(bidRequest.data).to.include.any.keys('fp'); }); + it('should set iriscat parameter if iriscat param is found and is of type string', function () { + const iriscat = 'segment'; + const request = { ...bidRequests[0] }; + request.params = { ...request.params, iriscat }; + const bidRequest = spec.buildRequests([request])[0]; + expect(bidRequest.data.iriscat).to.equal(iriscat); + }); + it('should not send iriscat parameter if iriscat param is not found', function () { + const request = { ...bidRequests[0] }; + const bidRequest = spec.buildRequests([request])[0]; + expect(bidRequest.data.iriscat).to.be.undefined; + }); + it('should not send iriscat parameter if iriscat param is not of type string', function () { + const iriscat = 123; + const request = { ...bidRequests[0] }; + request.params = { ...request.params, iriscat }; + const bidRequest = spec.buildRequests([request])[0]; + expect(bidRequest.data.iriscat).to.be.undefined; + }); it('should send pubId if inScreenPubID param is specified', function () { const request = Object.assign({}, bidRequests[0]); delete request.params; @@ -443,16 +463,15 @@ describe('gumgumAdapter', function () { pi: 3 } let expectedResponse = { - 'ad': '

I am an ad

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

I am an ad

', + cpm: 0, + creativeId: 29593, + currency: 'USD', + height: '250', + netRevenue: true, + requestId: 12345, + width: '300', + mediaType: BANNER, ttl: 60 }; @@ -533,6 +552,20 @@ describe('gumgumAdapter', function () { const decodedResponse = JSON.parse(atob(bidResponse)); expect(decodedResponse.jcsi).to.eql(JCSI); }); + + it('sets the correct mediaType depending on product', function () { + const bannerBidResponse = spec.interpretResponse({ body: serverResponse }, bidRequest)[0]; + const invideoBidResponse = spec.interpretResponse({ body: serverResponse }, { ...bidRequest, data: { pi: 6 } })[0]; + const videoBidResponse = spec.interpretResponse({ body: serverResponse }, { ...bidRequest, data: { pi: 7 } })[0]; + expect(bannerBidResponse.mediaType).to.equal(BANNER); + expect(invideoBidResponse.mediaType).to.equal(VIDEO); + expect(videoBidResponse.mediaType).to.equal(VIDEO); + }); + + it('sets a vastXml property if mediaType is video', function () { + const videoBidResponse = spec.interpretResponse({ body: serverResponse }, { ...bidRequest, data: { pi: 7 } })[0]; + expect(videoBidResponse.vastXml).to.exist; + }); }) describe('getUserSyncs', function () { const syncOptions = { diff --git a/test/spec/modules/haloRtdProvider_spec.js b/test/spec/modules/haloRtdProvider_spec.js new file mode 100644 index 00000000000..69ea4bc997c --- /dev/null +++ b/test/spec/modules/haloRtdProvider_spec.js @@ -0,0 +1,220 @@ +import { HALOID_LOCAL_NAME, SEG_LOCAL_NAME, addSegmentData, getSegments, haloSubmodule, storage } from 'modules/haloRtdProvider.js'; +import { server } from 'test/mocks/xhr.js'; + +const responseHeader = {'Content-Type': 'application/json'}; + +describe('haloRtdProvider', function() { + describe('haloSubmodule', function() { + it('successfully instantiates', function () { + expect(haloSubmodule.init()).to.equal(true); + }); + }); + + describe('Add Segment Data', function() { + it('adds segment data', function() { + const config = { + params: { + mapSegments: { + 'appnexus': true, + 'generic': true + } + } + }; + + let adUnits = [ + { + bids: [ + // bid with existing segment data in bid obj and fpd + { + bidder: 'appnexus', + fpd: { + user: { + data: [ + { + id: 'appnexus', + segment: [ + { + id: '0' + } + ] + } + ] + } + }, + params: { + user: { + segments: [0] + } + } + } + ] + }, + + // bids with fpd data definitions but without existing segment data + { + bids: [ + { + bidder: 'appnexus', + fpd: { + user: { + data: [ + { + id: 'appnexus' + } + ] + } + } + }, + { + bidder: 'generic', + fpd: { + user: { + data: [ + { + id: 'generic' + } + ] + } + } + } + ] + } + ]; + + const data = { + appnexus: [{id: '1'}, {id: '2'}, {id: '3'}], + generic: [{id: 'seg1'}, {id: 'seg2'}, {id: 'seg3'}] + }; + + addSegmentData(adUnits, data, config); + + expect(adUnits[0].bids[0].fpd.user.data[0].segment[0]).to.have.deep.property('id', '0'); + expect(adUnits[0].bids[0].fpd.user.data[0].segment[1]).to.have.deep.property('id', '1'); + expect(adUnits[0].bids[0].fpd.user.data[0].segment[2]).to.have.deep.property('id', '2'); + expect(adUnits[0].bids[0].fpd.user.data[0].segment[3]).to.have.deep.property('id', '3'); + expect(adUnits[0].bids[0].params.user).to.have.deep.property('segments', [0, 1, 2, 3]); + + expect(adUnits[1].bids[0].fpd.user.data[0].segment[0]).to.have.deep.property('id', '1'); + expect(adUnits[1].bids[0].fpd.user.data[0].segment[1]).to.have.deep.property('id', '2'); + expect(adUnits[1].bids[0].fpd.user.data[0].segment[2]).to.have.deep.property('id', '3'); + expect(adUnits[1].bids[0].params.user).to.have.deep.property('segments', [1, 2, 3]); + + expect(adUnits[1].bids[1].fpd.user.data[0].segment[0]).to.have.deep.property('id', 'seg1'); + expect(adUnits[1].bids[1].fpd.user.data[0].segment[1]).to.have.deep.property('id', 'seg2'); + expect(adUnits[1].bids[1].fpd.user.data[0].segment[2]).to.have.deep.property('id', 'seg3'); + expect(adUnits[1].bids[1].segments[0]).to.have.deep.property('id', 'seg1'); + expect(adUnits[1].bids[1].segments[1]).to.have.deep.property('id', 'seg2'); + expect(adUnits[1].bids[1].segments[2]).to.have.deep.property('id', 'seg3'); + }); + + it('allows mapper extensions and overrides', function() { + const config = { + params: { + mapSegments: { + generic: (bid, segments) => { + bid.overrideSegments = segments; + }, + newBidder: (bid, segments) => { + bid.newBidderSegments = segments; + } + } + } + }; + + let adUnits = [ + { + bids: [ {bidder: 'newBidder'}, {bidder: 'generic'} ] + } + ]; + + const data = { + newBidder: [{id: 'nbseg1', name: 'New Bidder Segment 1'}, {id: 'nbseg2', name: 'New Bidder Segment 2'}, {id: 'nbseg3', name: 'New Bidder Segment 3'}], + generic: [{id: 'seg1'}, {id: 'seg2'}, {id: 'seg3'}] + }; + + addSegmentData(adUnits, data, config); + + expect(adUnits[0].bids[0].newBidderSegments[0]).to.have.deep.property('id', 'nbseg1'); + expect(adUnits[0].bids[0].newBidderSegments[0]).to.have.deep.property('name', 'New Bidder Segment 1'); + expect(adUnits[0].bids[0].newBidderSegments[1]).to.have.deep.property('id', 'nbseg2'); + expect(adUnits[0].bids[0].newBidderSegments[1]).to.have.deep.property('name', 'New Bidder Segment 2'); + expect(adUnits[0].bids[0].newBidderSegments[2]).to.have.deep.property('id', 'nbseg3'); + expect(adUnits[0].bids[0].newBidderSegments[2]).to.have.deep.property('name', 'New Bidder Segment 3'); + + expect(adUnits[0].bids[1].overrideSegments[0]).to.have.deep.property('id', 'seg1'); + expect(adUnits[0].bids[1].overrideSegments[1]).to.have.deep.property('id', 'seg2'); + expect(adUnits[0].bids[1].overrideSegments[2]).to.have.deep.property('id', 'seg3'); + }); + }); + + describe('Get Segments', function() { + it('gets segment data from local storage cache', function() { + const config = { + params: { + segmentCache: true, + mapSegments: { + 'generic': true + } + } + }; + + let reqBidsConfigObj = { + adUnits: [ + { + bids: [{bidder: 'generic'}] + } + ] + }; + + const data = { + audigent_segments: { + generic: [{id: 'seg1'}] + } + }; + + storage.setDataInLocalStorage(SEG_LOCAL_NAME, JSON.stringify(data)); + + getSegments(reqBidsConfigObj, () => {}, config, {}); + + expect(reqBidsConfigObj.adUnits[0].bids[0].segments[0]).to.have.deep.property('id', 'seg1'); + }); + + it('gets segment data via async request', function() { + const config = { + params: { + segmentCache: false, + mapSegments: { + 'generic': true + }, + requestParams: { + 'publisherId': 1234 + } + } + }; + + let reqBidsConfigObj = { + adUnits: [ + { + bids: [{bidder: 'generic'}] + } + ] + }; + const data = { + audigent_segments: { + generic: [{id: 'seg1'}] + } + }; + + storage.setDataInLocalStorage(HALOID_LOCAL_NAME, 'haloid'); + getSegments(reqBidsConfigObj, () => {}, config, {}); + + let request = server.requests[0]; + let postData = JSON.parse(request.requestBody); + expect(postData.config).to.have.deep.property('publisherId', 1234); + + request.respond(200, responseHeader, JSON.stringify(data)); + + expect(reqBidsConfigObj.adUnits[0].bids[0].segments[0]).to.have.deep.property('id', 'seg1'); + }); + }); +}); diff --git a/test/spec/modules/haxmediaBidAdapter_spec.js b/test/spec/modules/haxmediaBidAdapter_spec.js new file mode 100644 index 00000000000..2e39d771bdf --- /dev/null +++ b/test/spec/modules/haxmediaBidAdapter_spec.js @@ -0,0 +1,304 @@ +import {expect} from 'chai'; +import {spec} from '../../../modules/haxmediaBidAdapter.js'; +import { BANNER, VIDEO, NATIVE } from '../../../src/mediaTypes.js'; + +describe('haxmediaBidAdapter', function () { + const bid = { + bidId: '23fhj33i987f', + bidder: 'haxmedia', + mediaTypes: { + [BANNER]: { + sizes: [[300, 250]] + } + }, + params: { + placementId: 783, + traffic: BANNER + } + }; + + const bidderRequest = { + refererInfo: { + referer: 'test.com' + } + }; + + describe('isBidRequestValid', function () { + it('Should return true if there are bidId, params and key parameters present', function () { + expect(spec.isBidRequestValid(bid)).to.be.true; + }); + it('Should return false if at least one of parameters is not present', function () { + delete bid.params.placementId; + expect(spec.isBidRequestValid(bid)).to.be.false; + }); + }); + + describe('buildRequests', function () { + let serverRequest = spec.buildRequests([bid], bidderRequest); + it('Creates a ServerRequest object with method, URL and data', function () { + expect(serverRequest).to.exist; + expect(serverRequest.method).to.exist; + expect(serverRequest.url).to.exist; + expect(serverRequest.data).to.exist; + }); + it('Returns POST method', function () { + expect(serverRequest.method).to.equal('POST'); + }); + it('Returns valid URL', function () { + expect(serverRequest.url).to.equal('https://balancer.haxmedia.io/?c=o&m=multi'); + }); + it('Returns valid data if array of bids is valid', function () { + let data = serverRequest.data; + expect(data).to.be.an('object'); + expect(data).to.have.all.keys('deviceWidth', 'deviceHeight', 'language', 'secure', 'host', 'page', 'placements'); + expect(data.deviceWidth).to.be.a('number'); + expect(data.deviceHeight).to.be.a('number'); + expect(data.language).to.be.a('string'); + expect(data.secure).to.be.within(0, 1); + expect(data.host).to.be.a('string'); + expect(data.page).to.be.a('string'); + expect(data.gdpr).to.not.exist; + expect(data.ccpa).to.not.exist; + let placement = data['placements'][0]; + expect(placement).to.have.keys('placementId', 'bidId', 'traffic', 'sizes', 'schain'); + expect(placement.placementId).to.equal(783); + expect(placement.bidId).to.equal('23fhj33i987f'); + expect(placement.traffic).to.equal(BANNER); + expect(placement.schain).to.be.an('object'); + expect(placement.sizes).to.be.an('array'); + }); + + it('Returns valid data for mediatype video', function () { + const playerSize = [300, 300]; + bid.mediaTypes = {}; + bid.params.traffic = VIDEO; + bid.mediaTypes[VIDEO] = { + playerSize + }; + serverRequest = spec.buildRequests([bid], bidderRequest); + let data = serverRequest.data; + expect(data).to.be.an('object'); + let placement = data['placements'][0]; + expect(placement).to.be.an('object'); + expect(placement).to.have.keys('placementId', 'bidId', 'traffic', 'wPlayer', 'hPlayer', 'schain'); + expect(placement.traffic).to.equal(VIDEO); + expect(placement.wPlayer).to.equal(playerSize[0]); + expect(placement.hPlayer).to.equal(playerSize[1]); + }); + + it('Returns valid data for mediatype native', function () { + const native = { + title: { + required: true + }, + body: { + required: true + }, + icon: { + required: true, + size: [64, 64] + } + }; + + bid.mediaTypes = {}; + bid.params.traffic = NATIVE; + bid.mediaTypes[NATIVE] = native; + serverRequest = spec.buildRequests([bid], bidderRequest); + let data = serverRequest.data; + expect(data).to.be.an('object'); + let placement = data['placements'][0]; + expect(placement).to.be.an('object'); + expect(placement).to.have.keys('placementId', 'bidId', 'traffic', 'native', 'schain'); + expect(placement.traffic).to.equal(NATIVE); + expect(placement.native).to.equal(native); + }); + + it('Returns data with gdprConsent and without uspConsent', function () { + bidderRequest.gdprConsent = 'test'; + serverRequest = spec.buildRequests([bid], bidderRequest); + let data = serverRequest.data; + expect(data.gdpr).to.exist; + expect(data.gdpr).to.be.a('string'); + expect(data.gdpr).to.equal(bidderRequest.gdprConsent); + expect(data.ccpa).to.not.exist; + delete bidderRequest.gdprConsent; + }); + + it('Returns data with uspConsent and without gdprConsent', function () { + bidderRequest.uspConsent = 'test'; + serverRequest = spec.buildRequests([bid], bidderRequest); + let data = serverRequest.data; + expect(data.ccpa).to.exist; + expect(data.ccpa).to.be.a('string'); + expect(data.ccpa).to.equal(bidderRequest.uspConsent); + expect(data.gdpr).to.not.exist; + }); + + it('Returns empty data if no valid requests are passed', function () { + serverRequest = spec.buildRequests([]); + let data = serverRequest.data; + expect(data.placements).to.be.an('array').that.is.empty; + }); + }); + describe('interpretResponse', function () { + it('Should interpret banner response', function () { + const banner = { + body: [{ + mediaType: 'banner', + width: 300, + height: 250, + cpm: 0.4, + ad: 'Test', + requestId: '23fhj33i987f', + ttl: 120, + creativeId: '2', + netRevenue: true, + currency: 'USD', + dealId: '1' + }] + }; + let bannerResponses = spec.interpretResponse(banner); + expect(bannerResponses).to.be.an('array').that.is.not.empty; + let dataItem = bannerResponses[0]; + expect(dataItem).to.have.all.keys('requestId', 'cpm', 'width', 'height', 'ad', 'ttl', 'creativeId', + 'netRevenue', 'currency', 'dealId', 'mediaType'); + expect(dataItem.requestId).to.equal('23fhj33i987f'); + expect(dataItem.cpm).to.equal(0.4); + expect(dataItem.width).to.equal(300); + expect(dataItem.height).to.equal(250); + expect(dataItem.ad).to.equal('Test'); + expect(dataItem.ttl).to.equal(120); + expect(dataItem.creativeId).to.equal('2'); + expect(dataItem.netRevenue).to.be.true; + expect(dataItem.currency).to.equal('USD'); + }); + it('Should interpret video response', function () { + const video = { + body: [{ + vastUrl: 'test.com', + mediaType: 'video', + cpm: 0.5, + requestId: '23fhj33i987f', + ttl: 120, + creativeId: '2', + netRevenue: true, + currency: 'USD', + dealId: '1' + }] + }; + let videoResponses = spec.interpretResponse(video); + expect(videoResponses).to.be.an('array').that.is.not.empty; + + let dataItem = videoResponses[0]; + expect(dataItem).to.have.all.keys('requestId', 'cpm', 'vastUrl', 'ttl', 'creativeId', + 'netRevenue', 'currency', 'dealId', 'mediaType'); + expect(dataItem.requestId).to.equal('23fhj33i987f'); + expect(dataItem.cpm).to.equal(0.5); + expect(dataItem.vastUrl).to.equal('test.com'); + expect(dataItem.ttl).to.equal(120); + expect(dataItem.creativeId).to.equal('2'); + expect(dataItem.netRevenue).to.be.true; + expect(dataItem.currency).to.equal('USD'); + }); + it('Should interpret native response', function () { + const native = { + body: [{ + mediaType: 'native', + native: { + clickUrl: 'test.com', + title: 'Test', + image: 'test.com', + impressionTrackers: ['test.com'], + }, + ttl: 120, + cpm: 0.4, + requestId: '23fhj33i987f', + creativeId: '2', + netRevenue: true, + currency: 'USD', + }] + }; + let nativeResponses = spec.interpretResponse(native); + expect(nativeResponses).to.be.an('array').that.is.not.empty; + + let dataItem = nativeResponses[0]; + expect(dataItem).to.have.keys('requestId', 'cpm', 'ttl', 'creativeId', 'netRevenue', 'currency', 'mediaType', 'native'); + expect(dataItem.native).to.have.keys('clickUrl', 'impressionTrackers', 'title', 'image') + expect(dataItem.requestId).to.equal('23fhj33i987f'); + expect(dataItem.cpm).to.equal(0.4); + expect(dataItem.native.clickUrl).to.equal('test.com'); + expect(dataItem.native.title).to.equal('Test'); + expect(dataItem.native.image).to.equal('test.com'); + expect(dataItem.native.impressionTrackers).to.be.an('array').that.is.not.empty; + expect(dataItem.native.impressionTrackers[0]).to.equal('test.com'); + expect(dataItem.ttl).to.equal(120); + expect(dataItem.creativeId).to.equal('2'); + expect(dataItem.netRevenue).to.be.true; + expect(dataItem.currency).to.equal('USD'); + }); + it('Should return an empty array if invalid banner response is passed', function () { + const invBanner = { + body: [{ + width: 300, + cpm: 0.4, + ad: 'Test', + requestId: '23fhj33i987f', + ttl: 120, + creativeId: '2', + netRevenue: true, + currency: 'USD', + dealId: '1' + }] + }; + + let serverResponses = spec.interpretResponse(invBanner); + expect(serverResponses).to.be.an('array').that.is.empty; + }); + it('Should return an empty array if invalid video response is passed', function () { + const invVideo = { + body: [{ + mediaType: 'video', + cpm: 0.5, + requestId: '23fhj33i987f', + ttl: 120, + creativeId: '2', + netRevenue: true, + currency: 'USD', + dealId: '1' + }] + }; + let serverResponses = spec.interpretResponse(invVideo); + expect(serverResponses).to.be.an('array').that.is.empty; + }); + it('Should return an empty array if invalid native response is passed', function () { + const invNative = { + body: [{ + mediaType: 'native', + clickUrl: 'test.com', + title: 'Test', + impressionTrackers: ['test.com'], + ttl: 120, + requestId: '23fhj33i987f', + creativeId: '2', + netRevenue: true, + currency: 'USD', + }] + }; + let serverResponses = spec.interpretResponse(invNative); + expect(serverResponses).to.be.an('array').that.is.empty; + }); + it('Should return an empty array if invalid response is passed', function () { + const invalid = { + body: [{ + ttl: 120, + creativeId: '2', + netRevenue: true, + currency: 'USD', + dealId: '1' + }] + }; + let serverResponses = spec.interpretResponse(invalid); + expect(serverResponses).to.be.an('array').that.is.empty; + }); + }); +}); diff --git a/test/spec/modules/hybridBidAdapter_spec.js b/test/spec/modules/hybridBidAdapter_spec.js index e5ad878c9b1..d1899ef3d81 100644 --- a/test/spec/modules/hybridBidAdapter_spec.js +++ b/test/spec/modules/hybridBidAdapter_spec.js @@ -25,16 +25,22 @@ describe('Hybrid.ai Adapter', function() { placeId: PLACE_ID, placement: 'video' } + const inImageMandatoryParams = { + placeId: PLACE_ID, + placement: 'inImage', + imageUrl: 'https://hybrid.ai/images/image.jpg' + } const validBidRequests = [ getSlotConfigs({ banner: {} }, bannerMandatoryParams), - getSlotConfigs({ video: {playerSize: [[640, 480]]} }, videoMandatoryParams) + getSlotConfigs({ video: {playerSize: [[640, 480]], context: 'outstream'} }, videoMandatoryParams), + getSlotConfigs({ banner: {sizes: [0, 0]} }, inImageMandatoryParams) ] describe('isBidRequestValid method', function() { describe('returns true', function() { describe('when banner slot config has all mandatory params', () => { - describe('and placement has the correct value', function() { + describe('and banner placement has the correct value', function() { const slotConfig = getSlotConfigs( - { banner: {} }, + {banner: {}}, { placeId: PLACE_ID, placement: 'banner' @@ -43,6 +49,22 @@ describe('Hybrid.ai Adapter', function() { const isBidRequestValid = spec.isBidRequestValid(slotConfig) expect(isBidRequestValid).to.equal(true) }) + describe('and In-Image placement has the correct value', function() { + const slotConfig = getSlotConfigs( + { + banner: { + sizes: [[0, 0]] + } + }, + { + placeId: PLACE_ID, + placement: 'inImage', + imageUrl: 'imageUrl' + } + ) + const isBidRequestValid = spec.isBidRequestValid(slotConfig) + expect(isBidRequestValid).to.equal(true) + }) describe('when video slot has all mandatory params.', function() { it('should return true, when video mediatype object are correct.', function() { const slotConfig = getSlotConfigs( @@ -84,6 +106,15 @@ describe('Hybrid.ai Adapter', function() { ) expect(isBidRequestValid).to.equal(false) }) + it('does not have the imageUrl.', function() { + const isBidRequestValid = spec.isBidRequestValid( + createSlotconfig({ + placeId: PLACE_ID, + placement: 'inImage' + }) + ) + expect(isBidRequestValid).to.equal(false) + }) it('does not have a the correct placement.', function() { const isBidRequestValid = spec.isBidRequestValid( createSlotconfig({ @@ -240,6 +271,36 @@ describe('Hybrid.ai Adapter', function() { expect(bids[0].netRevenue).to.equal(true) expect(typeof bids[0].ad).to.equal('string') }) + it('should return a In-Image bid', function() { + const request = spec.buildRequests([validBidRequests[2]], bidderRequest) + const serverResponse = { + body: { + bids: [ + { + bidId: '2df8c0733f284e', + price: 0.5, + currency: 'USD', + content: 'html', + inImage: { + actionUrls: {} + }, + width: 100, + height: 100, + ttl: 360 + } + ] + } + } + const bids = spec.interpretResponse(serverResponse, request) + expect(bids.length).to.equal(1) + expect(bids[0].requestId).to.equal('2df8c0733f284e') + expect(bids[0].cpm).to.equal(0.5) + expect(bids[0].width).to.equal(100) + expect(bids[0].height).to.equal(100) + expect(bids[0].currency).to.equal('USD') + expect(bids[0].netRevenue).to.equal(true) + expect(typeof bids[0].ad).to.equal('string') + }) }) describe('the bid is a video', function() { it('should return a video bid', function() { @@ -253,7 +314,8 @@ describe('Hybrid.ai Adapter', function() { currency: 'USD', content: 'html', width: 100, - height: 100 + height: 100, + transactionId: '31a58515-3634-4e90-9c96-f86196db1459' } ] } diff --git a/test/spec/modules/id5IdSystem_spec.js b/test/spec/modules/id5IdSystem_spec.js index 05ecec8dc36..afde1696766 100644 --- a/test/spec/modules/id5IdSystem_spec.js +++ b/test/spec/modules/id5IdSystem_spec.js @@ -1,9 +1,19 @@ +import { + id5IdSubmodule, + ID5_STORAGE_NAME, + getFromLocalStorage, + storeInLocalStorage, + expDaysStr, + nbCacheName, + getNbFromCache, + storeNbInCache +} from 'modules/id5IdSystem.js'; import { init, requestBidsHook, setSubmoduleRegistry, coreStorage } from 'modules/userId/index.js'; import { config } from 'src/config.js'; -import { id5IdSubmodule } from 'modules/id5IdSystem.js'; import { server } from 'test/mocks/xhr.js'; import events from 'src/events.js'; import CONSTANTS from 'src/constants.json'; +import * as utils from 'src/utils.js'; let expect = require('chai').expect; @@ -11,33 +21,31 @@ describe('ID5 ID System', function() { const ID5_MODULE_NAME = 'id5Id'; const ID5_EIDS_NAME = ID5_MODULE_NAME.toLowerCase(); const ID5_SOURCE = 'id5-sync.com'; - const ID5_PARTNER = 173; - const ID5_ENDPOINT = `https://id5-sync.com/g/v2/${ID5_PARTNER}.json`; - const ID5_COOKIE_NAME = 'id5idcookie'; - const ID5_NB_COOKIE_NAME = `id5id.1st_${ID5_PARTNER}_nb`; - const ID5_EXPIRED_COOKIE_DATE = 'Thu, 01 Jan 1970 00:00:01 GMT'; + const ID5_TEST_PARTNER_ID = 173; + const ID5_ENDPOINT = `https://id5-sync.com/g/v2/${ID5_TEST_PARTNER_ID}.json`; + const ID5_NB_STORAGE_NAME = nbCacheName(ID5_TEST_PARTNER_ID); const ID5_STORED_ID = 'storedid5id'; const ID5_STORED_SIGNATURE = '123456'; + const ID5_STORED_LINK_TYPE = 1; const ID5_STORED_OBJ = { 'universal_uid': ID5_STORED_ID, - 'signature': ID5_STORED_SIGNATURE + 'signature': ID5_STORED_SIGNATURE, + 'link_type': ID5_STORED_LINK_TYPE }; - const ID5_LEGACY_STORED_OBJ = { - 'ID5ID': ID5_STORED_ID - } const ID5_RESPONSE_ID = 'newid5id'; const ID5_RESPONSE_SIGNATURE = 'abcdef'; + const ID5_RESPONSE_LINK_TYPE = 2; const ID5_JSON_RESPONSE = { 'universal_uid': ID5_RESPONSE_ID, 'signature': ID5_RESPONSE_SIGNATURE, - 'link_type': 0 + 'link_type': ID5_RESPONSE_LINK_TYPE }; - function getId5FetchConfig(storageName = ID5_COOKIE_NAME, storageType = 'cookie') { + function getId5FetchConfig(storageName = ID5_STORAGE_NAME, storageType = 'html5') { return { name: ID5_MODULE_NAME, params: { - partner: ID5_PARTNER + partner: ID5_TEST_PARTNER_ID }, storage: { name: storageName, @@ -50,7 +58,9 @@ describe('ID5 ID System', function() { return { name: ID5_MODULE_NAME, value: { - id5id: value + id5id: { + uid: value + } } } } @@ -63,10 +73,10 @@ describe('ID5 ID System', function() { } } function getFetchCookieConfig() { - return getUserSyncConfig([getId5FetchConfig()]); + return getUserSyncConfig([getId5FetchConfig(ID5_STORAGE_NAME, 'cookie')]); } function getFetchLocalStorageConfig() { - return getUserSyncConfig([getId5FetchConfig(ID5_COOKIE_NAME, 'html5')]); + return getUserSyncConfig([getId5FetchConfig(ID5_STORAGE_NAME, 'html5')]); } function getValueConfig(value) { return getUserSyncConfig([getId5ValueConfig(value)]); @@ -80,6 +90,37 @@ describe('ID5 ID System', function() { }; } + describe('Check for valid publisher config', function() { + it('should fail with invalid config', function() { + // no config + expect(id5IdSubmodule.getId()).to.be.eq(undefined); + expect(id5IdSubmodule.getId({ })).to.be.eq(undefined); + + // valid params, invalid storage + expect(id5IdSubmodule.getId({ params: { partner: 123 } })).to.be.eq(undefined); + expect(id5IdSubmodule.getId({ params: { partner: 123 }, storage: {} })).to.be.eq(undefined); + expect(id5IdSubmodule.getId({ params: { partner: 123 }, storage: { name: '' } })).to.be.eq(undefined); + expect(id5IdSubmodule.getId({ params: { partner: 123 }, storage: { type: '' } })).to.be.eq(undefined); + + // valid storage, invalid params + expect(id5IdSubmodule.getId({ storage: { name: 'name', type: 'html5', }, })).to.be.eq(undefined); + expect(id5IdSubmodule.getId({ storage: { name: 'name', type: 'html5', }, params: { } })).to.be.eq(undefined); + expect(id5IdSubmodule.getId({ storage: { name: 'name', type: 'html5', }, params: { partner: 'abc' } })).to.be.eq(undefined); + }); + + it('should warn with non-recommended storage params', function() { + let logWarnStub = sinon.stub(utils, 'logWarn'); + + id5IdSubmodule.getId({ storage: { name: 'name', type: 'html5', }, params: { partner: 123 } }); + expect(logWarnStub.calledOnce).to.be.true; + logWarnStub.restore(); + + id5IdSubmodule.getId({ storage: { name: ID5_STORAGE_NAME, type: 'cookie', }, params: { partner: 123 } }); + expect(logWarnStub.calledOnce).to.be.true; + logWarnStub.restore(); + }); + }); + describe('Xhr Requests from getId()', function() { const responseHeader = { 'Content-Type': 'application/json' }; let callbackSpy = sinon.spy(); @@ -91,121 +132,109 @@ describe('ID5 ID System', function() { }); - it('should fail if no partner is provided in the config', function() { - expect(id5IdSubmodule.getId()).to.be.eq(undefined); - }); - - it('should call the ID5 server with 1puid field for legacy storedObj format', function () { - let submoduleCallback = id5IdSubmodule.getId(getId5FetchConfig().params, undefined, ID5_LEGACY_STORED_OBJ).callback; + it('should call the ID5 server and handle a valid response', function () { + let submoduleCallback = id5IdSubmodule.getId(getId5FetchConfig(), undefined, undefined).callback; submoduleCallback(callbackSpy); let request = server.requests[0]; let requestBody = JSON.parse(request.requestBody); expect(request.url).to.contain(ID5_ENDPOINT); expect(request.withCredentials).to.be.true; + expect(requestBody.partner).to.eq(ID5_TEST_PARTNER_ID); + expect(requestBody.o).to.eq('pbjs'); + expect(requestBody.pd).to.eq(''); expect(requestBody.s).to.eq(''); - expect(requestBody.partner).to.eq(ID5_PARTNER); - expect(requestBody['1puid']).to.eq(ID5_STORED_ID); + expect(requestBody.provider).to.eq(''); + expect(requestBody.v).to.eq('$prebid.version$'); + expect(requestBody.gdpr).to.exist; + expect(requestBody.gdpr_consent).to.exist + expect(requestBody.us_privacy).to.exist; request.respond(200, responseHeader, JSON.stringify(ID5_JSON_RESPONSE)); expect(callbackSpy.calledOnce).to.be.true; expect(callbackSpy.lastCall.lastArg).to.deep.equal(ID5_JSON_RESPONSE); }); - it('should call the ID5 server with signature field for new storedObj format', function () { - let submoduleCallback = id5IdSubmodule.getId(getId5FetchConfig().params, undefined, ID5_STORED_OBJ).callback; + it('should call the ID5 server with empty signature field when no stored object', function () { + let submoduleCallback = id5IdSubmodule.getId(getId5FetchConfig(), undefined, undefined).callback; + submoduleCallback(callbackSpy); + + let request = server.requests[0]; + let requestBody = JSON.parse(request.requestBody); + expect(requestBody.s).to.eq(''); + + request.respond(200, responseHeader, JSON.stringify(ID5_JSON_RESPONSE)); + }); + + it('should call the ID5 server with signature field from stored object', function () { + let submoduleCallback = id5IdSubmodule.getId(getId5FetchConfig(), undefined, ID5_STORED_OBJ).callback; submoduleCallback(callbackSpy); let request = server.requests[0]; let requestBody = JSON.parse(request.requestBody); - expect(request.url).to.contain(ID5_ENDPOINT); - expect(request.withCredentials).to.be.true; expect(requestBody.s).to.eq(ID5_STORED_SIGNATURE); - expect(requestBody.partner).to.eq(ID5_PARTNER); - expect(requestBody['1puid']).to.eq(''); request.respond(200, responseHeader, JSON.stringify(ID5_JSON_RESPONSE)); - expect(callbackSpy.calledOnce).to.be.true; - expect(callbackSpy.lastCall.lastArg).to.deep.equal(ID5_JSON_RESPONSE); }); it('should call the ID5 server with pd field when pd config is set', function () { const pubData = 'b50ca08271795a8e7e4012813f23d505193d75c0f2e2bb99baa63aa822f66ed3'; - let config = getId5FetchConfig().params; - config.pd = pubData; + let config = getId5FetchConfig(); + config.params.pd = pubData; let submoduleCallback = id5IdSubmodule.getId(config, undefined, ID5_STORED_OBJ).callback; submoduleCallback(callbackSpy); let request = server.requests[0]; let requestBody = JSON.parse(request.requestBody); - expect(request.url).to.contain(ID5_ENDPOINT); - expect(request.withCredentials).to.be.true; - expect(requestBody.s).to.eq(ID5_STORED_SIGNATURE); expect(requestBody.pd).to.eq(pubData); - expect(requestBody['1puid']).to.eq(''); request.respond(200, responseHeader, JSON.stringify(ID5_JSON_RESPONSE)); - expect(callbackSpy.calledOnce).to.be.true; - expect(callbackSpy.lastCall.lastArg).to.deep.equal(ID5_JSON_RESPONSE); }); it('should call the ID5 server with empty pd field when pd config is not set', function () { - let config = getId5FetchConfig().params; - config.pd = undefined; + let config = getId5FetchConfig(); + config.params.pd = undefined; let submoduleCallback = id5IdSubmodule.getId(config, undefined, ID5_STORED_OBJ).callback; submoduleCallback(callbackSpy); let request = server.requests[0]; let requestBody = JSON.parse(request.requestBody); - expect(request.url).to.contain(ID5_ENDPOINT); - expect(request.withCredentials).to.be.true; expect(requestBody.pd).to.eq(''); request.respond(200, responseHeader, JSON.stringify(ID5_JSON_RESPONSE)); - expect(callbackSpy.calledOnce).to.be.true; - expect(callbackSpy.lastCall.lastArg).to.deep.equal(ID5_JSON_RESPONSE); }); - it('should call the ID5 server with nb=1 when no stored value exists', function () { - coreStorage.setCookie(ID5_NB_COOKIE_NAME, '', ID5_EXPIRED_COOKIE_DATE); + it('should call the ID5 server with nb=1 when no stored value exists and reset after', function () { + coreStorage.removeDataFromLocalStorage(ID5_NB_STORAGE_NAME); - let submoduleCallback = id5IdSubmodule.getId(getId5FetchConfig().params, undefined, ID5_STORED_OBJ).callback; + let submoduleCallback = id5IdSubmodule.getId(getId5FetchConfig(), undefined, ID5_STORED_OBJ).callback; submoduleCallback(callbackSpy); let request = server.requests[0]; let requestBody = JSON.parse(request.requestBody); - expect(request.url).to.contain(ID5_ENDPOINT); - expect(request.withCredentials).to.be.true; expect(requestBody.nbPage).to.eq(1); request.respond(200, responseHeader, JSON.stringify(ID5_JSON_RESPONSE)); - expect(callbackSpy.calledOnce).to.be.true; - expect(callbackSpy.lastCall.lastArg).to.deep.equal(ID5_JSON_RESPONSE); - expect(coreStorage.getCookie(ID5_NB_COOKIE_NAME)).to.be.eq('0'); + expect(getNbFromCache(ID5_TEST_PARTNER_ID)).to.be.eq(0); }); - it('should call the ID5 server with incremented nb when stored value exists', function () { - let expStr = (new Date(Date.now() + 25000).toUTCString()); - coreStorage.setCookie(ID5_NB_COOKIE_NAME, '1', expStr); + it('should call the ID5 server with incremented nb when stored value exists and reset after', function () { + storeNbInCache(ID5_TEST_PARTNER_ID, 1); - let submoduleCallback = id5IdSubmodule.getId(getId5FetchConfig().params, undefined, ID5_STORED_OBJ).callback; + let submoduleCallback = id5IdSubmodule.getId(getId5FetchConfig(), undefined, ID5_STORED_OBJ).callback; submoduleCallback(callbackSpy); let request = server.requests[0]; let requestBody = JSON.parse(request.requestBody); - expect(request.url).to.contain(ID5_ENDPOINT); - expect(request.withCredentials).to.be.true; expect(requestBody.nbPage).to.eq(2); request.respond(200, responseHeader, JSON.stringify(ID5_JSON_RESPONSE)); - expect(callbackSpy.calledOnce).to.be.true; - expect(callbackSpy.lastCall.lastArg).to.deep.equal(ID5_JSON_RESPONSE); - expect(coreStorage.getCookie(ID5_NB_COOKIE_NAME)).to.be.eq('0'); + expect(getNbFromCache(ID5_TEST_PARTNER_ID)).to.be.eq(0); }); }); @@ -214,34 +243,36 @@ describe('ID5 ID System', function() { beforeEach(function() { sinon.stub(events, 'getEvents').returns([]); - coreStorage.setCookie(ID5_COOKIE_NAME, '', ID5_EXPIRED_COOKIE_DATE); - coreStorage.setCookie(`${ID5_COOKIE_NAME}_last`, '', ID5_EXPIRED_COOKIE_DATE); - coreStorage.setCookie(ID5_NB_COOKIE_NAME, '', ID5_EXPIRED_COOKIE_DATE); + coreStorage.removeDataFromLocalStorage(ID5_STORAGE_NAME); + coreStorage.removeDataFromLocalStorage(`${ID5_STORAGE_NAME}_last`); + coreStorage.removeDataFromLocalStorage(ID5_NB_STORAGE_NAME); adUnits = [getAdUnitMock()]; }); afterEach(function() { events.getEvents.restore(); - coreStorage.setCookie(ID5_COOKIE_NAME, '', ID5_EXPIRED_COOKIE_DATE); - coreStorage.setCookie(`${ID5_COOKIE_NAME}_last`, '', ID5_EXPIRED_COOKIE_DATE); - coreStorage.setCookie(ID5_NB_COOKIE_NAME, '', ID5_EXPIRED_COOKIE_DATE); + coreStorage.removeDataFromLocalStorage(ID5_STORAGE_NAME); + coreStorage.removeDataFromLocalStorage(`${ID5_STORAGE_NAME}_last`); + coreStorage.removeDataFromLocalStorage(ID5_NB_STORAGE_NAME); }); - it('should add stored ID from cookie to bids', function (done) { - let expStr = (new Date(Date.now() + 25000).toUTCString()); - coreStorage.setCookie(ID5_COOKIE_NAME, JSON.stringify(ID5_STORED_OBJ), expStr); + it('should add stored ID from cache to bids', function (done) { + storeInLocalStorage(ID5_STORAGE_NAME, JSON.stringify(ID5_STORED_OBJ), 1); setSubmoduleRegistry([id5IdSubmodule]); init(config); - config.setConfig(getFetchCookieConfig()); + config.setConfig(getFetchLocalStorageConfig()); requestBidsHook(function () { adUnits.forEach(unit => { unit.bids.forEach(bid => { expect(bid).to.have.deep.nested.property(`userId.${ID5_EIDS_NAME}`); - expect(bid.userId.id5id).to.equal(ID5_STORED_ID); + expect(bid.userId.id5id.uid).to.equal(ID5_STORED_ID); expect(bid.userIdAsEids[0]).to.deep.equal({ source: ID5_SOURCE, - uids: [{ id: ID5_STORED_ID, atype: 1 }] + uids: [{ id: ID5_STORED_ID, atype: 1 }], + ext: { + linkType: ID5_STORED_LINK_TYPE + } }); }); }); @@ -258,7 +289,7 @@ describe('ID5 ID System', function() { adUnits.forEach(unit => { unit.bids.forEach(bid => { expect(bid).to.have.deep.nested.property(`userId.${ID5_EIDS_NAME}`); - expect(bid.userId.id5id).to.equal(ID5_STORED_ID); + expect(bid.userId.id5id.uid).to.equal(ID5_STORED_ID); expect(bid.userIdAsEids[0]).to.deep.equal({ source: ID5_SOURCE, uids: [{ id: ID5_STORED_ID, atype: 1 }] @@ -269,43 +300,40 @@ describe('ID5 ID System', function() { }, { adUnits }); }); - it('should set nb=1 in cookie when no stored value exists', function () { - let expStr = (new Date(Date.now() + 25000).toUTCString()); - coreStorage.setCookie(ID5_COOKIE_NAME, JSON.stringify(ID5_STORED_OBJ), expStr); - coreStorage.setCookie(ID5_NB_COOKIE_NAME, '', ID5_EXPIRED_COOKIE_DATE); + it('should set nb=1 in cache when no stored nb value exists and cached ID', function () { + storeInLocalStorage(ID5_STORAGE_NAME, JSON.stringify(ID5_STORED_OBJ), 1); + coreStorage.removeDataFromLocalStorage(ID5_NB_STORAGE_NAME); setSubmoduleRegistry([id5IdSubmodule]); init(config); - config.setConfig(getFetchCookieConfig()); + config.setConfig(getFetchLocalStorageConfig()); let innerAdUnits; requestBidsHook((config) => { innerAdUnits = config.adUnits }, {adUnits}); - expect(coreStorage.getCookie(ID5_NB_COOKIE_NAME)).to.be.eq('1'); + expect(getNbFromCache(ID5_TEST_PARTNER_ID)).to.be.eq(1); }); - it('should increment nb in cookie when stored value exists', function () { - let expStr = (new Date(Date.now() + 25000).toUTCString()); - coreStorage.setCookie(ID5_COOKIE_NAME, JSON.stringify(ID5_STORED_OBJ), expStr); - coreStorage.setCookie(ID5_NB_COOKIE_NAME, '1', expStr); + it('should increment nb in cache when stored nb value exists and cached ID', function () { + storeInLocalStorage(ID5_STORAGE_NAME, JSON.stringify(ID5_STORED_OBJ), 1); + storeNbInCache(ID5_TEST_PARTNER_ID, 1); setSubmoduleRegistry([id5IdSubmodule]); init(config); - config.setConfig(getFetchCookieConfig()); + config.setConfig(getFetchLocalStorageConfig()); let innerAdUnits; requestBidsHook((config) => { innerAdUnits = config.adUnits }, {adUnits}); - expect(coreStorage.getCookie(ID5_NB_COOKIE_NAME)).to.be.eq('2'); + expect(getNbFromCache(ID5_TEST_PARTNER_ID)).to.be.eq(2); }); it('should call ID5 servers with signature and incremented nb post auction if refresh needed', function () { - let expStr = (new Date(Date.now() + 25000).toUTCString()); - coreStorage.setCookie(ID5_COOKIE_NAME, JSON.stringify(ID5_STORED_OBJ), expStr); - coreStorage.setCookie(`${ID5_COOKIE_NAME}_last`, (new Date(Date.now() - 50000).toUTCString()), expStr); - coreStorage.setCookie(ID5_NB_COOKIE_NAME, '1', expStr); + storeInLocalStorage(ID5_STORAGE_NAME, JSON.stringify(ID5_STORED_OBJ), 1); + storeInLocalStorage(`${ID5_STORAGE_NAME}_last`, expDaysStr(-1), 1); + storeNbInCache(ID5_TEST_PARTNER_ID, 1); - let id5Config = getFetchCookieConfig(); + let id5Config = getFetchLocalStorageConfig(); id5Config.userSync.userIds[0].storage.refreshInSeconds = 2; setSubmoduleRegistry([id5IdSubmodule]); @@ -315,7 +343,7 @@ describe('ID5 ID System', function() { let innerAdUnits; requestBidsHook((config) => { innerAdUnits = config.adUnits }, {adUnits}); - expect(coreStorage.getCookie(ID5_NB_COOKIE_NAME)).to.be.eq('2'); + expect(getNbFromCache(ID5_TEST_PARTNER_ID)).to.be.eq(2); expect(server.requests).to.be.empty; events.emit(CONSTANTS.EVENTS.AUCTION_END, {}); @@ -329,55 +357,161 @@ describe('ID5 ID System', function() { const responseHeader = { 'Content-Type': 'application/json' }; request.respond(200, responseHeader, JSON.stringify(ID5_JSON_RESPONSE)); - expect(coreStorage.getCookie(ID5_COOKIE_NAME)).to.be.eq(JSON.stringify(ID5_JSON_RESPONSE)); - expect(coreStorage.getCookie(ID5_NB_COOKIE_NAME)).to.be.eq('0'); + expect(decodeURIComponent(getFromLocalStorage(ID5_STORAGE_NAME))).to.be.eq(JSON.stringify(ID5_JSON_RESPONSE)); + expect(getNbFromCache(ID5_TEST_PARTNER_ID)).to.be.eq(0); }); + }); - it('should call ID5 servers with 1puid and nb=1 post auction if refresh needed for legacy stored object', function () { - let expStr = (new Date(Date.now() + 25000).toUTCString()); - coreStorage.setCookie(ID5_COOKIE_NAME, JSON.stringify(ID5_LEGACY_STORED_OBJ), expStr); - coreStorage.setCookie(`${ID5_COOKIE_NAME}_last`, (new Date(Date.now() - 50000).toUTCString()), expStr); - - let id5Config = getFetchCookieConfig(); - id5Config.userSync.userIds[0].storage.refreshInSeconds = 2; - - setSubmoduleRegistry([id5IdSubmodule]); - init(config); - config.setConfig(id5Config); + describe('Decode stored object', function() { + const expectedDecodedObject = { id5id: { uid: ID5_STORED_ID, ext: { linkType: ID5_STORED_LINK_TYPE } } }; - let innerAdUnits; - requestBidsHook((config) => { innerAdUnits = config.adUnits }, {adUnits}); + it('should properly decode from a stored object', function() { + expect(id5IdSubmodule.decode(ID5_STORED_OBJ, getId5FetchConfig())).to.deep.equal(expectedDecodedObject); + }); + it('should return undefined if passed a string', function() { + expect(id5IdSubmodule.decode('somestring', getId5FetchConfig())).to.eq(undefined); + }); + }); - expect(coreStorage.getCookie(ID5_NB_COOKIE_NAME)).to.be.eq('1'); + describe('A/B Testing', function() { + const expectedDecodedObjectWithIdAbOff = { id5id: { uid: ID5_STORED_ID, ext: { linkType: ID5_STORED_LINK_TYPE } } }; + const expectedDecodedObjectWithIdAbOn = { id5id: { uid: ID5_STORED_ID, ext: { linkType: ID5_STORED_LINK_TYPE, abTestingControlGroup: false } } }; + const expectedDecodedObjectWithoutIdAbOn = { id5id: { uid: 0, ext: { linkType: 0, abTestingControlGroup: true } } }; + let testConfig; - expect(server.requests).to.be.empty; - events.emit(CONSTANTS.EVENTS.AUCTION_END, {}); + beforeEach(function() { + testConfig = getId5FetchConfig(); + }); - let request = server.requests[0]; - let requestBody = JSON.parse(request.requestBody); - expect(request.url).to.contain(ID5_ENDPOINT); - expect(requestBody['1puid']).to.eq(ID5_STORED_ID); - expect(requestBody.nbPage).to.eq(1); + describe('Configuration Validation', function() { + let logErrorSpy; + let logInfoSpy; + + beforeEach(function() { + logErrorSpy = sinon.spy(utils, 'logError'); + logInfoSpy = sinon.spy(utils, 'logInfo'); + }); + afterEach(function() { + logErrorSpy.restore(); + logInfoSpy.restore(); + }); + + // A/B Testing ON, but invalid config + let testInvalidAbTestingConfigsWithError = [ + { enabled: true }, + { enabled: true, controlGroupPct: 2 }, + { enabled: true, controlGroupPct: -1 }, + { enabled: true, controlGroupPct: 'a' }, + { enabled: true, controlGroupPct: true } + ]; + testInvalidAbTestingConfigsWithError.forEach((testAbTestingConfig) => { + it('should error if config is invalid, and always return an ID', function () { + testConfig.params.abTesting = testAbTestingConfig; + let decoded = id5IdSubmodule.decode(ID5_STORED_OBJ, testConfig); + expect(decoded).to.deep.equal(expectedDecodedObjectWithIdAbOn); + sinon.assert.calledOnce(logErrorSpy); + }); + }); + + // A/B Testing OFF, with invalid config (ignore) + let testInvalidAbTestingConfigsWithoutError = [ + { enabled: false, controlGroupPct: -1 }, + { enabled: false, controlGroupPct: 2 }, + { enabled: false, controlGroupPct: 'a' }, + { enabled: false, controlGroupPct: true } + ]; + testInvalidAbTestingConfigsWithoutError.forEach((testAbTestingConfig) => { + it('should not error if config is invalid but A/B testing is off, and always return an ID', function () { + testConfig.params.abTesting = testAbTestingConfig; + let decoded = id5IdSubmodule.decode(ID5_STORED_OBJ, testConfig); + expect(decoded).to.deep.equal(expectedDecodedObjectWithIdAbOff); + sinon.assert.notCalled(logErrorSpy); + sinon.assert.notCalled(logInfoSpy); + }); + }); + + // A/B Testing ON, with valid config + let testValidConfigs = [ + { enabled: true, controlGroupPct: 0 }, + { enabled: true, controlGroupPct: 0.5 }, + { enabled: true, controlGroupPct: 1 } + ]; + testValidConfigs.forEach((testAbTestingConfig) => { + it('should not error if config is valid', function () { + testConfig.params.abTesting = testAbTestingConfig; + id5IdSubmodule.decode(ID5_STORED_OBJ, testConfig); + sinon.assert.notCalled(logErrorSpy); + sinon.assert.calledOnce(logInfoSpy); + }); + }); + }); - const responseHeader = { 'Content-Type': 'application/json' }; - request.respond(200, responseHeader, JSON.stringify(ID5_JSON_RESPONSE)); + describe('A/B Testing Config is not Set', function() { + let randStub; - expect(coreStorage.getCookie(ID5_COOKIE_NAME)).to.be.eq(JSON.stringify(ID5_JSON_RESPONSE)); - expect(coreStorage.getCookie(ID5_NB_COOKIE_NAME)).to.be.eq('0'); + beforeEach(function() { + randStub = sinon.stub(Math, 'random').callsFake(function() { + return 0; + }); + }); + afterEach(function () { + randStub.restore(); + }); + + it('should expose ID when A/B config is not set', function () { + let decoded = id5IdSubmodule.decode(ID5_STORED_OBJ, testConfig); + expect(decoded).to.deep.equal(expectedDecodedObjectWithIdAbOff); + }); + + it('should expose ID when A/B config is empty', function () { + testConfig.params.abTesting = { }; + + let decoded = id5IdSubmodule.decode(ID5_STORED_OBJ, testConfig); + expect(decoded).to.deep.equal(expectedDecodedObjectWithIdAbOff); + }); }); - }); - describe('Decode stored object', function() { - const decodedObject = { 'id5id': ID5_STORED_ID }; + describe('A/B Testing Config is Set', function() { + let randStub; - it('should properly decode from a stored object', function() { - expect(id5IdSubmodule.decode(ID5_STORED_OBJ)).to.deep.equal(decodedObject); - }); - it('should properly decode from a legacy stored object', function() { - expect(id5IdSubmodule.decode(ID5_LEGACY_STORED_OBJ)).to.deep.equal(decodedObject); - }); - it('should return undefined if passed a string', function() { - expect(id5IdSubmodule.decode('somestring')).to.eq(undefined); + beforeEach(function() { + randStub = sinon.stub(Math, 'random').callsFake(function() { + return 0.25; + }); + }); + afterEach(function () { + randStub.restore(); + }); + + it('should expose ID when A/B testing is off', function () { + testConfig.params.abTesting = { + enabled: false, + controlGroupPct: 0.5 + }; + + let decoded = id5IdSubmodule.decode(ID5_STORED_OBJ, testConfig); + expect(decoded).to.deep.equal(expectedDecodedObjectWithIdAbOff); + }); + + it('should expose ID when not in control group', function () { + testConfig.params.abTesting = { + enabled: true, + controlGroupPct: 0.1 + }; + + let decoded = id5IdSubmodule.decode(ID5_STORED_OBJ, testConfig); + expect(decoded).to.deep.equal(expectedDecodedObjectWithIdAbOn); + }); + + it('should not expose ID when in control group', function () { + testConfig.params.abTesting = { + enabled: true, + controlGroupPct: 0.5 + }; + + let decoded = id5IdSubmodule.decode(ID5_STORED_OBJ, testConfig); + expect(decoded).to.deep.equal(expectedDecodedObjectWithoutIdAbOn); + }); }); }); }); diff --git a/test/spec/modules/idLibrary_spec.js b/test/spec/modules/idLibrary_spec.js new file mode 100644 index 00000000000..da61850f29b --- /dev/null +++ b/test/spec/modules/idLibrary_spec.js @@ -0,0 +1,61 @@ +import * as utils from 'src/utils.js'; +import * as idlibrary from 'modules/idLibrary.js'; + +var expect = require('chai').expect; + +describe('currency', function () { + let fakeCurrencyFileServer; + let sandbox; + let clock; + + let fn = sinon.spy(); + + beforeEach(function () { + fakeCurrencyFileServer = sinon.fakeServer.create(); + sinon.stub(utils, 'logInfo'); + sinon.stub(utils, 'logError'); + }); + + afterEach(function () { + utils.logInfo.restore(); + utils.logError.restore(); + fakeCurrencyFileServer.restore(); + idlibrary.setConfig({}); + }); + + describe('setConfig', function () { + beforeEach(function() { + sandbox = sinon.sandbox.create(); + clock = sinon.useFakeTimers(1046952000000); // 2003-03-06T12:00:00Z + }); + + afterEach(function () { + sandbox.restore(); + clock.restore(); + }); + + it('results when no config available', function () { + idlibrary.setConfig({}); + sinon.assert.called(utils.logError); + }); + it('results with config available', function () { + idlibrary.setConfig({ 'url': 'URL' }); + sinon.assert.called(utils.logInfo); + }); + it('results with config default debounce ', function () { + let config = { 'url': 'URL' } + idlibrary.setConfig(config); + expect(config.debounce).to.be.equal(250); + }); + it('results with config default fullscan ', function () { + let config = { 'url': 'URL' } + idlibrary.setConfig(config); + expect(config.fullscan).to.be.equal(true); + }); + it('results with config fullscan ', function () { + let config = { 'url': 'URL', 'fullscan': false } + idlibrary.setConfig(config); + expect(config.fullscan).to.be.equal(false); + }); + }); +}); diff --git a/test/spec/modules/identityLinkIdSystem_spec.js b/test/spec/modules/identityLinkIdSystem_spec.js index 9f36ba92558..3c544fa8d08 100644 --- a/test/spec/modules/identityLinkIdSystem_spec.js +++ b/test/spec/modules/identityLinkIdSystem_spec.js @@ -1,9 +1,12 @@ import {identityLinkSubmodule} from 'modules/identityLinkIdSystem.js'; import * as utils from 'src/utils.js'; import {server} from 'test/mocks/xhr.js'; +import {getStorageManager} from '../../../src/storageManager.js'; + +export const storage = getStorageManager(); const pid = '14'; -const defaultConfigParams = {pid: pid}; +const defaultConfigParams = { params: {pid: pid} }; const responseHeader = {'Content-Type': 'application/json'} describe('IdentityLinkId tests', function () { @@ -11,6 +14,8 @@ describe('IdentityLinkId tests', function () { beforeEach(function () { logErrorStub = sinon.stub(utils, 'logError'); + // remove _lr_retry_request cookie before test + storage.setCookie('_lr_retry_request', 'true', 'Thu, 01 Jan 1970 00:00:01 GMT'); }); afterEach(function () { @@ -18,12 +23,12 @@ describe('IdentityLinkId tests', function () { }); it('should log an error if no configParams were passed when getId', function () { - identityLinkSubmodule.getId(); + identityLinkSubmodule.getId({ params: {} }); expect(logErrorStub.calledOnce).to.be.true; }); it('should log an error if pid configParam was not passed when getId', function () { - identityLinkSubmodule.getId({}); + identityLinkSubmodule.getId({ params: {} }); expect(logErrorStub.calledOnce).to.be.true; }); @@ -124,4 +129,29 @@ describe('IdentityLinkId tests', function () { ); expect(callBackSpy.calledOnce).to.be.true; }); + + it('should not call the LiveRamp envelope endpoint if cookie _lr_retry_request exist', function () { + let now = new Date(); + now.setTime(now.getTime() + 3000); + storage.setCookie('_lr_retry_request', 'true', now.toUTCString()); + let callBackSpy = sinon.spy(); + let submoduleCallback = identityLinkSubmodule.getId(defaultConfigParams).callback; + submoduleCallback(callBackSpy); + let request = server.requests[0]; + expect(request).to.be.eq(undefined); + }); + + it('should call the LiveRamp envelope endpoint if cookie _lr_retry_request does not exist', function () { + let callBackSpy = sinon.spy(); + let submoduleCallback = identityLinkSubmodule.getId(defaultConfigParams).callback; + submoduleCallback(callBackSpy); + let request = server.requests[0]; + expect(request.url).to.be.eq('https://api.rlcdn.com/api/identity/envelope?pid=14'); + request.respond( + 200, + responseHeader, + JSON.stringify({}) + ); + expect(callBackSpy.calledOnce).to.be.true; + }); }); diff --git a/test/spec/modules/idxIdSystem_spec.js b/test/spec/modules/idxIdSystem_spec.js new file mode 100644 index 00000000000..14cd9a88d13 --- /dev/null +++ b/test/spec/modules/idxIdSystem_spec.js @@ -0,0 +1,117 @@ +import { expect } from 'chai'; +import find from 'core-js-pure/features/array/find.js'; +import { config } from 'src/config.js'; +import { init, requestBidsHook, setSubmoduleRegistry } from 'modules/userId/index.js'; +import { storage, idxIdSubmodule } from 'modules/idxIdSystem.js'; + +const IDX_COOKIE_NAME = '_idx'; +const IDX_DUMMY_VALUE = 'idx value for testing'; +const IDX_COOKIE_STORED = '{ "idx": "' + IDX_DUMMY_VALUE + '" }'; +const ID_COOKIE_OBJECT = { id: IDX_DUMMY_VALUE }; +const IDX_COOKIE_OBJECT = { idx: IDX_DUMMY_VALUE }; + +function getConfigMock() { + return { + userSync: { + syncDelay: 0, + userIds: [{ + name: 'idx' + }] + } + } +} + +function getAdUnitMock(code = 'adUnit-code') { + return { + code, + mediaTypes: {banner: {}, native: {}}, + sizes: [ + [300, 200], + [300, 600] + ], + bids: [{ + bidder: 'sampleBidder', + params: { placementId: 'banner-only-bidder' } + }] + }; +} + +describe('IDx ID System', () => { + let getDataFromLocalStorageStub, localStorageIsEnabledStub; + let getCookieStub, cookiesAreEnabledStub; + + beforeEach(() => { + getDataFromLocalStorageStub = sinon.stub(storage, 'getDataFromLocalStorage'); + localStorageIsEnabledStub = sinon.stub(storage, 'localStorageIsEnabled'); + getCookieStub = sinon.stub(storage, 'getCookie'); + cookiesAreEnabledStub = sinon.stub(storage, 'cookiesAreEnabled'); + }); + + afterEach(() => { + getDataFromLocalStorageStub.restore(); + localStorageIsEnabledStub.restore(); + getCookieStub.restore(); + cookiesAreEnabledStub.restore(); + }); + + describe('IDx: test "getId" method', () => { + it('provides the stored IDx if a cookie exists', () => { + getCookieStub.withArgs(IDX_COOKIE_NAME).returns(IDX_COOKIE_STORED); + let idx = idxIdSubmodule.getId(); + expect(idx).to.deep.equal(ID_COOKIE_OBJECT); + }); + + it('provides the stored IDx if cookie is absent but present in local storage', () => { + getDataFromLocalStorageStub.withArgs(IDX_COOKIE_NAME).returns(IDX_COOKIE_STORED); + let idx = idxIdSubmodule.getId(); + expect(idx).to.deep.equal(ID_COOKIE_OBJECT); + }); + + it('returns undefined if both cookie and local storage are empty', () => { + let idx = idxIdSubmodule.getId(); + expect(idx).to.be.undefined; + }) + }); + + describe('IDx: test "decode" method', () => { + it('provides the IDx from a stored object', () => { + expect(idxIdSubmodule.decode(ID_COOKIE_OBJECT)).to.deep.equal(IDX_COOKIE_OBJECT); + }); + + it('provides the IDx from a stored string', () => { + expect(idxIdSubmodule.decode(IDX_DUMMY_VALUE)).to.deep.equal(IDX_COOKIE_OBJECT); + }); + }); + + describe('requestBids hook', () => { + let adUnits; + + beforeEach(() => { + adUnits = [getAdUnitMock()]; + setSubmoduleRegistry([idxIdSubmodule]); + init(config); + config.setConfig(getConfigMock()); + getCookieStub.withArgs(IDX_COOKIE_NAME).returns(IDX_COOKIE_STORED); + }); + + it('when a stored IDx exists it is added to bids', (done) => { + requestBidsHook(() => { + adUnits.forEach(unit => { + unit.bids.forEach(bid => { + expect(bid).to.have.deep.nested.property('userId.idx'); + expect(bid.userId.idx).to.equal(IDX_DUMMY_VALUE); + const idxIdAsEid = find(bid.userIdAsEids, e => e.source == 'idx.lat'); + expect(idxIdAsEid).to.deep.equal({ + source: 'idx.lat', + uids: [{ + id: IDX_DUMMY_VALUE, + atype: 1, + }] + }); + }); + }); + done(); + }, { adUnits }); + }); + }); +}); diff --git a/test/spec/modules/improvedigitalBidAdapter_spec.js b/test/spec/modules/improvedigitalBidAdapter_spec.js index 5a20944a6ed..89ec5aed8c3 100644 --- a/test/spec/modules/improvedigitalBidAdapter_spec.js +++ b/test/spec/modules/improvedigitalBidAdapter_spec.js @@ -154,6 +154,8 @@ describe('Improve Digital Adapter Tests', function () { expect(params.bid_request.version).to.equal(`${spec.version}-${idClient.CONSTANTS.CLIENT_VERSION}`); expect(params.bid_request.gdpr).to.not.exist; expect(params.bid_request.us_privacy).to.not.exist; + expect(params.bid_request.schain).to.not.exist; + expect(params.bid_request.user).to.not.exist; expect(params.bid_request.imp).to.deep.equal([ { id: '33e9500b21129f', @@ -345,6 +347,22 @@ describe('Improve Digital Adapter Tests', function () { expect(params.bid_request.schain).to.equal(schain); }); + it('should add eids', function () { + const userId = { id5id: { uid: '1111' } }; + const expectedUserObject = { ext: { eids: [{ + source: 'id5-sync.com', + uids: [{ + atype: 1, + id: '1111' + }] + }]}}; + const bidRequest = Object.assign({}, simpleBidRequest); + bidRequest.userId = userId; + const request = spec.buildRequests([bidRequest], bidderRequestReferrer)[0]; + const params = JSON.parse(decodeURIComponent(request.data.substring(PARAM_PREFIX.length))); + expect(params.bid_request.user).to.deep.equal(expectedUserObject); + }); + it('should return 2 requests', function () { const requests = spec.buildRequests([ simpleBidRequest, diff --git a/test/spec/modules/inmarBidAdapter_spec.js b/test/spec/modules/inmarBidAdapter_spec.js new file mode 100644 index 00000000000..86b7ab3a8af --- /dev/null +++ b/test/spec/modules/inmarBidAdapter_spec.js @@ -0,0 +1,251 @@ +// import or require modules necessary for the test, e.g.: +import {expect} from 'chai'; // may prefer 'assert' in place of 'expect' +import { + spec +} from 'modules/inmarBidAdapter.js'; +import {config} from 'src/config.js'; + +describe('Inmar adapter tests', function () { + var DEFAULT_PARAMS_NEW_SIZES = [{ + adUnitCode: 'test-div', + bidId: '2c7c8e9c900244', + mediaTypes: { + banner: { + sizes: [ + [300, 250], [300, 600], [728, 90], [970, 250]] + } + }, + bidder: 'inmar', + params: { + adnetId: 'ADb1f40rmi', + partnerId: 12345 + }, + auctionId: '0cb3144c-d084-4686-b0d6-f5dbe917c563', + bidRequestsCount: 1, + bidderRequestId: '1858b7382993ca', + transactionId: '29df2112-348b-4961-8863-1b33684d95e6', + user: {} + }]; + + var DEFAULT_PARAMS_VIDEO = [{ + adUnitCode: 'test-div', + bidId: '2c7c8e9c900244', + mediaTypes: { + video: { + context: 'instream', // or 'outstream' + playerSize: [640, 480], + mimes: ['video/mp4'] + } + }, + bidder: 'inmar', + params: { + adnetId: 'ADb1f40rmi', + partnerId: 12345 + }, + auctionId: '0cb3144c-d084-4686-b0d6-f5dbe917c563', + bidRequestsCount: 1, + bidderRequestId: '1858b7382993ca', + transactionId: '29df2112-348b-4961-8863-1b33684d95e6', + user: {} + }]; + + var DEFAULT_PARAMS_WO_OPTIONAL = [{ + adUnitCode: 'test-div', + bidId: '2c7c8e9c900244', + sizes: [ + [300, 250], + [300, 600], + [728, 90], + [970, 250] + ], + bidder: 'inmar', + params: { + adnetId: 'ADb1f40rmi', + partnerId: 12345, + }, + auctionId: '851adee7-d843-48f9-a7e9-9ff00573fcbf', + bidRequestsCount: 1, + bidderRequestId: '1858b7382993ca', + transactionId: '29df2112-348b-4961-8863-1b33684d95e6' + }]; + + var BID_RESPONSE = { + body: { + cpm: 1.50, + ad: '', + meta: { + mediaType: 'banner', + }, + width: 300, + height: 250, + creativeId: '189198063', + netRevenue: true, + currency: 'USD', + ttl: 300, + dealId: 'dealId' + + } + }; + + var BID_RESPONSE_VIDEO = { + body: { + cpm: 1.50, + meta: { + mediaType: 'video', + }, + width: 1, + height: 1, + creativeId: '189198063', + netRevenue: true, + currency: 'USD', + ttl: 300, + vastUrl: 'https://vast.com/vast.xml', + dealId: 'dealId' + } + }; + + it('Verify build request to prebid 3.0 display test', function() { + const request = spec.buildRequests(DEFAULT_PARAMS_NEW_SIZES, { + gdprConsent: { + consentString: 'BOZcQl_ObPFjWAeABAESCD-AAAAjx7_______9______9uz_Ov_v_f__33e8__9v_l_7_-___u_-33d4-_1vf99yfm1-7ftr3tp_87ues2_Xur__59__3z3_NohBgA', + gdprApplies: true + }, + refererInfo: { + referer: 'https://domain.com', + numIframes: 0 + } + }); + + expect(request).to.have.property('method').and.to.equal('POST'); + const requestContent = JSON.parse(request.data); + expect(requestContent.bidRequests[0].params).to.have.property('adnetId').and.to.equal('ADb1f40rmi'); + expect(requestContent.bidRequests[0].params).to.have.property('partnerId').and.to.equal(12345); + expect(requestContent.bidRequests[0]).to.have.property('auctionId').and.to.equal('0cb3144c-d084-4686-b0d6-f5dbe917c563'); + expect(requestContent.bidRequests[0]).to.have.property('bidId').and.to.equal('2c7c8e9c900244'); + expect(requestContent.bidRequests[0]).to.have.property('bidRequestsCount').and.to.equal(1); + expect(requestContent.bidRequests[0]).to.have.property('bidder').and.to.equal('inmar'); + expect(requestContent.bidRequests[0]).to.have.property('bidderRequestId').and.to.equal('1858b7382993ca'); + expect(requestContent.bidRequests[0]).to.have.property('adUnitCode').and.to.equal('test-div'); + expect(requestContent.refererInfo).to.have.property('referer').and.to.equal('https://domain.com'); + expect(requestContent.bidRequests[0].mediaTypes.banner).to.have.property('sizes'); + expect(requestContent.bidRequests[0].mediaTypes.banner.sizes[0]).to.have.ordered.members([300, 250]); + expect(requestContent.bidRequests[0].mediaTypes.banner.sizes[1]).to.have.ordered.members([300, 600]); + expect(requestContent.bidRequests[0].mediaTypes.banner.sizes[2]).to.have.ordered.members([728, 90]); + expect(requestContent.bidRequests[0].mediaTypes.banner.sizes[3]).to.have.ordered.members([970, 250]); + expect(requestContent.bidRequests[0]).to.have.property('transactionId').and.to.equal('29df2112-348b-4961-8863-1b33684d95e6'); + expect(requestContent.refererInfo).to.have.property('numIframes').and.to.equal(0); + }) + + it('Verify interprete response', function () { + const request = spec.buildRequests(DEFAULT_PARAMS_NEW_SIZES, { + gdprConsent: { + consentString: 'BOZcQl_ObPFjWAeABAESCD-AAAAjx7_______9______9uz_Ov_v_f__33e8__9v_l_7_-___u_-33d4-_1vf99yfm1-7ftr3tp_87ues2_Xur__59__3z3_NohBgA', + gdprApplies: true + }, + refererInfo: { + referer: 'https://domain.com', + numIframes: 0 + } + }); + + const bids = spec.interpretResponse(BID_RESPONSE, request); + expect(bids).to.have.lengthOf(1); + const bid = bids[0]; + expect(bid.cpm).to.equal(1.50); + expect(bid.ad).to.equal(''); + expect(bid.meta.mediaType).to.equal('banner'); + expect(bid.width).to.equal(300); + expect(bid.height).to.equal(250); + expect(bid.creativeId).to.equal('189198063'); + expect(bid.netRevenue).to.equal(true); + expect(bid.currency).to.equal('USD'); + expect(bid.ttl).to.equal(300); + expect(bid.dealId).to.equal('dealId'); + }); + + it('no banner media response', function () { + const request = spec.buildRequests(DEFAULT_PARAMS_NEW_SIZES, { + gdprConsent: { + consentString: 'BOZcQl_ObPFjWAeABAESCD-AAAAjx7_______9______9uz_Ov_v_f__33e8__9v_l_7_-___u_-33d4-_1vf99yfm1-7ftr3tp_87ues2_Xur__59__3z3_NohBgA', + gdprApplies: true + }, + refererInfo: { + referer: 'https://domain.com', + numIframes: 0 + } + }); + + const bids = spec.interpretResponse(BID_RESPONSE_VIDEO, request); + const bid = bids[0]; + expect(bid.vastUrl).to.equal('https://vast.com/vast.xml'); + }); + + it('Verifies bidder_code', function () { + expect(spec.code).to.equal('inmar'); + }); + + it('Verifies bidder aliases', function () { + expect(spec.aliases).to.have.lengthOf(1); + expect(spec.aliases[0]).to.equal('inm'); + }); + + it('Verifies if bid request is valid', function () { + expect(spec.isBidRequestValid(DEFAULT_PARAMS_NEW_SIZES[0])).to.equal(true); + expect(spec.isBidRequestValid(DEFAULT_PARAMS_WO_OPTIONAL[0])).to.equal(true); + expect(spec.isBidRequestValid({})).to.equal(false); + expect(spec.isBidRequestValid({ + params: {} + })).to.equal(false); + expect(spec.isBidRequestValid({ + params: { + adnetId: 'ADb1f40rmi' + } + })).to.equal(false); + expect(spec.isBidRequestValid({ + params: { + partnerId: 12345 + } + })).to.equal(false); + expect(spec.isBidRequestValid({ + params: { + adnetId: 'ADb1f40rmi', + partnerId: 12345 + } + })).to.equal(true); + }); + + it('Verifies user syncs image', function () { + var syncs = spec.getUserSyncs({ + iframeEnabled: false, + pixelEnabled: true + }, [BID_RESPONSE], { + consentString: 'BOZcQl_ObPFjWAeABAESCD-AAAAjx7_______9______9uz_Ov_v_f__33e8__9v_l_7_-___u_-33d4-_1vf99yfm1-7ftr3tp_87ues2_Xur__59__3z3_NohBgA', + referer: 'http://domain.com', + gdprApplies: true + }) + expect(syncs).to.have.lengthOf(1); + expect(syncs[0].type).to.equal('image'); + + syncs = spec.getUserSyncs({ + iframeEnabled: false, + pixelEnabled: true + }, [BID_RESPONSE], { + consentString: '', + referer: 'http://domain.com', + gdprApplies: true + }) + expect(syncs).to.have.lengthOf(1); + expect(syncs[0].type).to.equal('image'); + + syncs = spec.getUserSyncs({ + iframeEnabled: false, + pixelEnabled: true + }, [], { + consentString: null, + referer: 'http://domain.com', + gdprApplies: true + }) + expect(syncs).to.have.lengthOf(1); + expect(syncs[0].type).to.equal('image'); + }); +}); diff --git a/test/spec/modules/intentIqIdSystem_spec.js b/test/spec/modules/intentIqIdSystem_spec.js new file mode 100644 index 00000000000..4b45342cb6f --- /dev/null +++ b/test/spec/modules/intentIqIdSystem_spec.js @@ -0,0 +1,151 @@ +import { expect } from 'chai'; +import {intentIqIdSubmodule} from 'modules/intentIqIdSystem.js'; +import * as utils from 'src/utils.js'; +import {server} from 'test/mocks/xhr.js'; + +const partner = 10; +const pai = '11'; +const pcid = '12'; +const defaultConfigParams = { params: {partner: partner} }; +const paiConfigParams = { params: {partner: partner, pai: pai} }; +const pcidConfigParams = { params: {partner: partner, pcid: pcid} }; +const allConfigParams = { params: {partner: partner, pai: pai, pcid: pcid} }; +const responseHeader = {'Content-Type': 'application/json'} + +describe('IntentIQ tests', function () { + let logErrorStub; + + beforeEach(function () { + logErrorStub = sinon.stub(utils, 'logError'); + }); + + afterEach(function () { + logErrorStub.restore(); + }); + + it('should log an error if no configParams were passed when getId', function () { + let submodule = intentIqIdSubmodule.getId({ params: {} }); + expect(logErrorStub.calledOnce).to.be.true; + expect(submodule).to.be.undefined; + }); + + it('should log an error if partner configParam was not passed when getId', function () { + let submodule = intentIqIdSubmodule.getId({ params: {} }); + expect(logErrorStub.calledOnce).to.be.true; + expect(submodule).to.be.undefined; + }); + + it('should log an error if partner configParam was not a numeric value', function () { + let submodule = intentIqIdSubmodule.getId({ params: {partner: '10'} }); + expect(logErrorStub.calledOnce).to.be.true; + expect(submodule).to.be.undefined; + }); + + it('should call the IntentIQ endpoint with only partner', function () { + let callBackSpy = sinon.spy(); + let submoduleCallback = intentIqIdSubmodule.getId(defaultConfigParams).callback; + submoduleCallback(callBackSpy); + let request = server.requests[0]; + expect(request.url).to.be.eq('https://api.intentiq.com/profiles_engine/ProfilesEngineServlet?at=39&mi=10&dpi=10&pt=17&dpn=1'); + request.respond( + 200, + responseHeader, + JSON.stringify({}) + ); + expect(callBackSpy.calledOnce).to.be.true; + }); + + it('should ignore NA and invalid responses', function () { + let resp = JSON.stringify({'RESULT': 'NA'}); + expect(intentIqIdSubmodule.decode(resp)).to.equal(undefined); + expect(intentIqIdSubmodule.decode('NA')).to.equal(undefined); + expect(intentIqIdSubmodule.decode('')).to.equal(undefined); + expect(intentIqIdSubmodule.decode(undefined)).to.equal(undefined); + }); + + it('should call the IntentIQ endpoint with only partner, pai', function () { + let callBackSpy = sinon.spy(); + let submoduleCallback = intentIqIdSubmodule.getId(paiConfigParams).callback; + submoduleCallback(callBackSpy); + let request = server.requests[0]; + expect(request.url).to.be.eq('https://api.intentiq.com/profiles_engine/ProfilesEngineServlet?at=39&mi=10&dpi=10&pt=17&dpn=1&pai=11'); + request.respond( + 200, + responseHeader, + JSON.stringify({}) + ); + expect(callBackSpy.calledOnce).to.be.true; + }); + + it('should call the IntentIQ endpoint with only partner, pcid', function () { + let callBackSpy = sinon.spy(); + let submoduleCallback = intentIqIdSubmodule.getId(pcidConfigParams).callback; + submoduleCallback(callBackSpy); + let request = server.requests[0]; + expect(request.url).to.be.eq('https://api.intentiq.com/profiles_engine/ProfilesEngineServlet?at=39&mi=10&dpi=10&pt=17&dpn=1&pcid=12'); + request.respond( + 200, + responseHeader, + JSON.stringify({}) + ); + expect(callBackSpy.calledOnce).to.be.true; + }); + + it('should call the IntentIQ endpoint with partner, pcid, pai', function () { + let callBackSpy = sinon.spy(); + let submoduleCallback = intentIqIdSubmodule.getId(allConfigParams).callback; + submoduleCallback(callBackSpy); + let request = server.requests[0]; + expect(request.url).to.be.eq('https://api.intentiq.com/profiles_engine/ProfilesEngineServlet?at=39&mi=10&dpi=10&pt=17&dpn=1&pcid=12&pai=11'); + request.respond( + 200, + responseHeader, + JSON.stringify({}) + ); + expect(callBackSpy.calledOnce).to.be.true; + }); + + it('should not throw Uncaught TypeError when IntentIQ endpoint returns empty response', function () { + let callBackSpy = sinon.spy(); + let submoduleCallback = intentIqIdSubmodule.getId(defaultConfigParams).callback; + submoduleCallback(callBackSpy); + let request = server.requests[0]; + expect(request.url).to.be.eq('https://api.intentiq.com/profiles_engine/ProfilesEngineServlet?at=39&mi=10&dpi=10&pt=17&dpn=1'); + request.respond( + 204, + responseHeader, + '' + ); + expect(callBackSpy.calledOnce).to.be.true; + expect(request.response).to.equal(''); + expect(logErrorStub.calledOnce).to.not.be.true; + }); + + it('should log an error and continue to callback if ajax request errors', function () { + let callBackSpy = sinon.spy(); + let submoduleCallback = intentIqIdSubmodule.getId(defaultConfigParams).callback; + submoduleCallback(callBackSpy); + let request = server.requests[0]; + expect(request.url).to.be.eq('https://api.intentiq.com/profiles_engine/ProfilesEngineServlet?at=39&mi=10&dpi=10&pt=17&dpn=1'); + request.respond( + 503, + responseHeader, + 'Unavailable' + ); + expect(callBackSpy.calledOnce).to.be.true; + }); + + it('should log an error and continue to callback if ajax request errors', function () { + let callBackSpy = sinon.spy(); + let submoduleCallback = intentIqIdSubmodule.getId(defaultConfigParams).callback; + submoduleCallback(callBackSpy); + let request = server.requests[0]; + expect(request.url).to.be.eq('https://api.intentiq.com/profiles_engine/ProfilesEngineServlet?at=39&mi=10&dpi=10&pt=17&dpn=1'); + request.respond( + 503, + responseHeader, + 'Unavailable' + ); + expect(callBackSpy.calledOnce).to.be.true; + }); +}); diff --git a/test/spec/modules/ironsourceBidAdapter_spec.js b/test/spec/modules/ironsourceBidAdapter_spec.js index cfdc51e0235..93c3a6fb7b9 100644 --- a/test/spec/modules/ironsourceBidAdapter_spec.js +++ b/test/spec/modules/ironsourceBidAdapter_spec.js @@ -5,6 +5,7 @@ import { config } from 'src/config.js'; import { VIDEO } from '../../../src/mediaTypes.js'; const ENDPOINT = 'https://hb.yellowblue.io/hb'; +const TEST_ENDPOINT = 'https://hb.yellowblue.io/hb-test'; const TTL = 360; describe('ironsourceAdapter', function () { @@ -55,6 +56,21 @@ describe('ironsourceAdapter', function () { } ]; + const testModeBidRequests = [ + { + 'bidder': spec.code, + 'adUnitCode': 'adunit-code', + 'sizes': [[640, 480]], + 'params': { + 'isOrg': 'jdye8weeyirk00000001', + 'testMode': true + }, + 'bidId': '299ffc8cca0b87', + 'bidderRequestId': '1144f487e563f9', + 'auctionId': 'bfc420c3-8577-4568-9766-a8a935fb620d', + } + ]; + const bidderRequest = { bidderCode: 'ironsource', } @@ -67,6 +83,14 @@ describe('ironsourceAdapter', function () { } }); + it('sends bid request to test ENDPOINT via GET', function () { + const requests = spec.buildRequests(testModeBidRequests, bidderRequest); + for (const request of requests) { + expect(request.url).to.equal(TEST_ENDPOINT); + expect(request.method).to.equal('GET'); + } + }); + it('should send the correct bid Id', function () { const requests = spec.buildRequests(bidRequests, bidderRequest); for (const request of requests) { @@ -254,7 +278,7 @@ describe('ironsourceAdapter', function () { mediaType: VIDEO } ]; - const result = spec.interpretResponse({ body: [response] }); + const result = spec.interpretResponse({ body: response }); expect(Object.keys(result[0])).to.have.members(Object.keys(expectedResponse[0])); }); }) diff --git a/test/spec/modules/ixBidAdapter_spec.js b/test/spec/modules/ixBidAdapter_spec.js index 63b04077f4e..7ac4bd94f9d 100644 --- a/test/spec/modules/ixBidAdapter_spec.js +++ b/test/spec/modules/ixBidAdapter_spec.js @@ -18,7 +18,6 @@ describe('IndexexchangeAdapter', function () { 'sid': '00001', 'hp': 1 }, - { 'asi': 'indirectseller-2.com', 'sid': '00002', @@ -26,7 +25,74 @@ describe('IndexexchangeAdapter', function () { } ] }; - + var div_many_sizes = [ + [300, 250], + [600, 410], + [336, 280], + [400, 300], + [320, 50], + [360, 360], + [250, 250], + [320, 250], + [400, 250], + [387, 359], + [300, 50], + [372, 250], + [320, 320], + [412, 412], + [327, 272], + [312, 260], + [384, 320], + [335, 250], + [366, 305], + [374, 250], + [375, 375], + [272, 391], + [364, 303], + [414, 414], + [366, 375], + [272, 360], + [364, 373], + [366, 359], + [320, 100], + [360, 250], + [468, 60], + [480, 300], + [600, 400], + [600, 300], + [33, 28], + [40, 30], + [32, 5], + [36, 36], + [25, 25], + [320, 25], + [400, 25], + [387, 35], + [300, 5], + [372, 20], + [320, 32], + [412, 41], + [327, 27], + [312, 26], + [384, 32], + [335, 25], + [366, 30], + [374, 25], + [375, 37], + [272, 31], + [364, 303], + [414, 41], + [366, 35], + [272, 60], + [364, 73], + [366, 59], + [320, 10], + [360, 25], + [468, 6], + [480, 30], + [600, 40], + [600, 30] + ]; const DEFAULT_BANNER_VALID_BID = [ { bidder: 'ix', @@ -587,7 +653,6 @@ describe('IndexexchangeAdapter', function () { it('IX adapter reads LiveRamp IDL envelope from Prebid and adds it to Video', function () { const cloneValidBid = utils.deepClone(DEFAULT_VIDEO_VALID_BID); cloneValidBid[0].userId = utils.deepClone(DEFAULT_USERID_DATA); - const request = spec.buildRequests(cloneValidBid, DEFAULT_OPTION)[0]; const payload = JSON.parse(request.data.r); @@ -972,6 +1037,97 @@ describe('IndexexchangeAdapter', function () { expect(videoImp.video.h).to.equal(DEFAULT_VIDEO_VALID_BID[0].params.size[1]); }); + it('single request under 8k size limit for large ad unit', function () { + const options = {}; + const bid1 = utils.deepClone(DEFAULT_BANNER_VALID_BID[0]); + bid1.mediaTypes.banner.sizes = div_many_sizes; + const requests = spec.buildRequests([bid1], options); + + const reqSize = new Blob([`${requests[0].url}?${utils.parseQueryStringParameters(requests[0].data)}`]).size; + expect(requests).to.be.an('array'); + expect(requests).to.have.lengthOf(1); + expect(reqSize).to.be.lessThan(8000); + }); + + it('2 requests due to 2 ad units, one larger than url size', function () { + const bid1 = utils.deepClone(DEFAULT_BANNER_VALID_BID[0]); + bid1.mediaTypes.banner.sizes = div_many_sizes; + bid1.params.siteId = '124'; + bid1.adUnitCode = 'div-gpt-1' + bid1.transactionId = '152e36d1-1241-4242-t35e-y1dv34d12315'; + bid1.bidId = '2f6g5s5e'; + + const requests = spec.buildRequests([bid1, DEFAULT_BANNER_VALID_BID[0]], DEFAULT_OPTION); + expect(requests).to.be.an('array'); + expect(requests).to.have.lengthOf(2); + expect(requests[0].data.sn).to.be.equal(0); + expect(requests[1].data.sn).to.be.equal(1); + }); + + it('6 ad units should generate only 4 requests', function () { + const bid1 = utils.deepClone(DEFAULT_BANNER_VALID_BID[0]); + bid1.mediaTypes.banner.sizes = div_many_sizes; + bid1.params.siteId = '121'; + bid1.adUnitCode = 'div-gpt-1' + bid1.transactionId = 'tr1'; + bid1.bidId = '2f6g5s5e'; + + const bid2 = utils.deepClone(bid1); + bid2.transactionId = 'tr2'; + + const bid3 = utils.deepClone(bid1); + bid3.transactionId = 'tr3'; + + const bid4 = utils.deepClone(bid1); + bid4.transactionId = 'tr4'; + + const bid5 = utils.deepClone(bid1); + bid5.transactionId = 'tr5'; + + const bid6 = utils.deepClone(bid1); + bid6.transactionId = 'tr6'; + + const requests = spec.buildRequests([bid1, bid2, bid3, bid4, bid5, bid6], DEFAULT_OPTION); + + expect(requests).to.be.an('array'); + expect(requests).to.have.lengthOf(4); + + // check if seq number increases + for (var i = 0; i < requests.length; i++) { + const reqSize = new Blob([`${requests[i].url}?${utils.parseQueryStringParameters(requests[i].data)}`]).size; + expect(reqSize).to.be.lessThan(8000); + let payload = JSON.parse(requests[i].data.r); + if (requests.length > 1) { + expect(requests[i].data.sn).to.equal(i); + } + expect(payload.source.ext.schain).to.deep.equal(SAMPLE_SCHAIN); + } + }); + + it('multiple ad units in one request', function () { + const bid1 = utils.deepClone(DEFAULT_BANNER_VALID_BID[0]); + bid1.mediaTypes.banner.sizes = [[300, 250], [300, 600], [100, 200]]; + bid1.params.siteId = '121'; + bid1.adUnitCode = 'div-gpt-1' + bid1.transactionId = 'tr1'; + bid1.bidId = '2f6g5s5e'; + + const bid2 = utils.deepClone(bid1); + bid2.transactionId = 'tr2'; + bid2.mediaTypes.banner.sizes = [[220, 221], [222, 223], [300, 250]]; + const bid3 = utils.deepClone(bid1); + bid3.transactionId = 'tr3'; + bid3.mediaTypes.banner.sizes = [[330, 331], [332, 333], [300, 250]]; + + const requests = spec.buildRequests([bid1, bid2, bid3], DEFAULT_OPTION); + expect(requests).to.be.an('array'); + expect(requests).to.have.lengthOf(1); + + const impressions = JSON.parse(requests[0].data.r).imp; + expect(impressions).to.be.an('array'); + expect(impressions).to.have.lengthOf(9); + }); + it('request should contain the extra banner ad sizes that IX is not configured for using the first site id in the ad unit', function () { const bid = utils.deepClone(DEFAULT_BANNER_VALID_BID[0]); bid.sizes.push([336, 280], [970, 90]); @@ -1017,23 +1173,26 @@ describe('IndexexchangeAdapter', function () { const impressions = JSON.parse(request.data.r).imp; expect(impressions).to.be.an('array'); expect(impressions).to.have.lengthOf(4); - - expect(impressions[0].ext.siteID).to.equal(DEFAULT_BANNER_VALID_BID[0].params.siteId.toString()) - expect(impressions[1].ext.siteID).to.equal(bid.params.siteId) - expect(impressions[2].ext.siteID).to.equal(DEFAULT_BANNER_VALID_BID[0].params.siteId.toString()) - expect(impressions[3].ext.siteID).to.equal(bid.params.siteId) + expect(impressions[0].ext.siteID).to.equal(DEFAULT_BANNER_VALID_BID[0].params.siteId.toString()); + expect(impressions[1].ext.siteID).to.equal(bid.params.siteId); + expect(impressions[2].ext.siteID).to.equal(DEFAULT_BANNER_VALID_BID[0].params.siteId.toString()); + expect(impressions[3].ext.siteID).to.equal(bid.params.siteId); expect(impressions[0].banner.w).to.equal(DEFAULT_BANNER_VALID_BID[0].params.size[0]); expect(impressions[0].banner.h).to.equal(DEFAULT_BANNER_VALID_BID[0].params.size[1]); + expect(impressions[1].banner.w).to.equal(bid.params.size[0]); expect(impressions[1].banner.h).to.equal(bid.params.size[1]); + expect(impressions[2].banner.w).to.equal(DEFAULT_BANNER_VALID_BID[0].mediaTypes.banner.sizes[1][0]); expect(impressions[2].banner.h).to.equal(DEFAULT_BANNER_VALID_BID[0].mediaTypes.banner.sizes[1][1]); + expect(impressions[3].banner.w).to.equal(bid.mediaTypes.banner.sizes[1][0]); expect(impressions[3].banner.h).to.equal(bid.mediaTypes.banner.sizes[1][1]); expect(impressions[0].ext.sid).to.equal(`${DEFAULT_BANNER_VALID_BID[0].params.size[0].toString()}x${DEFAULT_BANNER_VALID_BID[0].params.size[1].toString()}`); expect(impressions[1].ext.sid).to.equal(`${bid.params.size[0].toString()}x${bid.params.size[1].toString()}`); + expect(impressions[2].ext.sid).to.equal(`${DEFAULT_BANNER_VALID_BID[0].mediaTypes.banner.sizes[1][0].toString()}x${DEFAULT_BANNER_VALID_BID[0].mediaTypes.banner.sizes[1][1].toString()}`); expect(impressions[3].ext.sid).to.equal(`${bid.mediaTypes.banner.sizes[1][0].toString()}x${bid.mediaTypes.banner.sizes[1][1].toString()}`); }); @@ -1045,6 +1204,28 @@ describe('IndexexchangeAdapter', function () { expect(impressions).to.be.an('array'); expect(impressions).to.have.lengthOf(1); }); + + describe('detect missing sizes', function () { + beforeEach(function () { + config.setConfig({ + ix: { + detectMissingSizes: false + } + }); + }) + + it('request should not contain missing sizes if detectMissingSizes = false', function () { + const bid1 = utils.deepClone(DEFAULT_BANNER_VALID_BID[0]); + bid1.mediaTypes.banner.sizes = div_many_sizes; + + const requests = spec.buildRequests([bid1, DEFAULT_BANNER_VALID_BID[0]], DEFAULT_OPTION); + + const impressions = JSON.parse(requests[0].data.r).imp; + + expect(impressions).to.be.an('array'); + expect(impressions).to.have.lengthOf(2); + }); + }); }); describe('buildRequestVideo', function () { diff --git a/test/spec/modules/justpremiumBidAdapter_spec.js b/test/spec/modules/justpremiumBidAdapter_spec.js index c162b785aed..7cc154254ff 100644 --- a/test/spec/modules/justpremiumBidAdapter_spec.js +++ b/test/spec/modules/justpremiumBidAdapter_spec.js @@ -21,7 +21,9 @@ describe('justpremium adapter', function () { }, userId: { tdid: '1111111', - id5id: '2222222', + id5id: { + uid: '2222222' + }, digitrustid: { data: { id: '3333333' @@ -84,7 +86,7 @@ describe('justpremium adapter', function () { expect(jpxRequest.version.jp_adapter).to.equal('1.7') expect(jpxRequest.pubcid).to.equal('0000000') expect(jpxRequest.uids.tdid).to.equal('1111111') - expect(jpxRequest.uids.id5id).to.equal('2222222') + expect(jpxRequest.uids.id5id.uid).to.equal('2222222') expect(jpxRequest.uids.digitrustid.data.id).to.equal('3333333') expect(jpxRequest.us_privacy).to.equal('1YYN') }) diff --git a/test/spec/modules/jwplayerRtdProvider_spec.js b/test/spec/modules/jwplayerRtdProvider_spec.js index b5bacdc3694..48b432b6bb4 100644 --- a/test/spec/modules/jwplayerRtdProvider_spec.js +++ b/test/spec/modules/jwplayerRtdProvider_spec.js @@ -1,4 +1,5 @@ -import { fetchTargetingForMediaId, getTargetingForBid, +import { fetchTargetingForMediaId, getVatFromCache, extractPublisherParams, + formatTargetingResponse, getVatFromPlayer, enrichAdUnits, addTargetingToBid, fetchTargetingInformation, jwplayerSubmodule } from 'modules/jwplayerRtdProvider.js'; import { server } from 'test/mocks/xhr.js'; @@ -39,11 +40,7 @@ describe('jwplayerRtdProvider', function() { }) ); - const targetingInfo = getTargetingForBid({ - jwTargeting: { - mediaID: testIdForSuccess - } - }); + const targetingInfo = getVatFromCache(testIdForSuccess); const validTargeting = { segments: validSegments, @@ -62,21 +59,13 @@ describe('jwplayerRtdProvider', function() { it('should not write to cache when response is malformed', function() { request.respond('{]'); - const targetingInfo = getTargetingForBid({ - jwTargeting: { - mediaID: testIdForFailure - } - }); + const targetingInfo = getVatFromCache(testIdForFailure); expect(targetingInfo).to.be.null; }); it('should not write to cache when playlist is absent', function() { request.respond({}); - const targetingInfo = getTargetingForBid({ - jwTargeting: { - mediaID: testIdForFailure - } - }); + const targetingInfo = getVatFromCache(testIdForFailure); expect(targetingInfo).to.be.null; }); @@ -92,27 +81,46 @@ describe('jwplayerRtdProvider', function() { ] }) ); - const targetingInfo = getTargetingForBid({ - jwTargeting: { - mediaID: testIdForFailure - } - }); + const targetingInfo = getVatFromCache(testIdForFailure); expect(targetingInfo).to.be.null; }); it('should not write to cache when request errors', function() { request.error(); - const targetingInfo = getTargetingForBid({ - jwTargeting: { - mediaID: testIdForFailure - } - }); + const targetingInfo = getVatFromCache(testIdForFailure); expect(targetingInfo).to.be.null; }); }); }); - describe('Get targeting for bid', function() { + describe('Format targeting response', function () { + it('should exclude segment key when absent', function () { + const targeting = formatTargetingResponse({ mediaID: 'test' }); + expect(targeting).to.not.have.property('segments'); + }); + + it('should exclude content block when mediaId is absent', function () { + const targeting = formatTargetingResponse({ segments: ['test'] }); + expect(targeting).to.not.have.property('content'); + }); + + it('should return proper format', function () { + const segments = ['123']; + const mediaID = 'test'; + const expectedContentId = 'jw_' + mediaID; + const expectedContent = { + id: expectedContentId + }; + const targeting = formatTargetingResponse({ + segments, + mediaID + }); + expect(targeting).to.have.deep.property('segments', segments); + expect(targeting).to.have.deep.property('content', expectedContent); + }); + }); + + describe('Get VAT from player', function () { const mediaIdWithSegment = 'media_ID_1'; const mediaIdNoSegment = 'media_ID_2'; const mediaIdForCurrentItem = 'media_ID_current'; @@ -121,18 +129,8 @@ describe('jwplayerRtdProvider', function() { const validPlayerID = 'player_test_ID_valid'; const invalidPlayerID = 'player_test_ID_invalid'; - it('returns null when targeting block is missing', function () { - const targeting = getTargetingForBid({}); - expect(targeting).to.be.null; - }); - it('returns null when jwplayer.js is absent from page', function () { - const targeting = getTargetingForBid({ - jwTargeting: { - playerID: invalidPlayerID, - mediaID: mediaIdNotCached - } - }); + const targeting = getVatFromPlayer(invalidPlayerID, mediaIdNotCached); expect(targeting).to.be.null; }); @@ -184,81 +182,358 @@ describe('jwplayerRtdProvider', function() { }); it('returns null when player ID does not match player on page', function () { - const targeting = getTargetingForBid({ - jwTargeting: { - playerID: invalidPlayerID, - mediaID: mediaIdNotCached - } - }); + const targeting = getVatFromPlayer(invalidPlayerID, mediaIdNotCached); expect(targeting).to.be.null; }); it('returns segments when media ID matches a playlist item with segments', function () { - const targeting = getTargetingForBid({ - jwTargeting: { - playerID: validPlayerID, - mediaID: mediaIdWithSegment - } - }); + const targeting = getVatFromPlayer(validPlayerID, mediaIdWithSegment); expect(targeting).to.deep.equal(targetingForMediaWithSegment); }); - it('caches segments media ID matches a playist item with segments', function () { - getTargetingForBid({ - jwTargeting: { - playerID: validPlayerID, - mediaID: mediaIdWithSegment - } - }); - - window.jwplayer = null; - const targeting2 = getTargetingForBid({ - jwTargeting: { - playerID: invalidPlayerID, - mediaID: mediaIdWithSegment - } - }); - expect(targeting2).to.deep.equal(targetingForMediaWithSegment); + it('caches segments when media ID matches a playist item with segments', function () { + getVatFromPlayer(validPlayerID, mediaIdWithSegment); + const vat = getVatFromCache(mediaIdWithSegment); + expect(vat.segments).to.deep.equal(validSegments); }); it('returns segments of current item when media ID is missing', function () { - const targeting = getTargetingForBid({ - jwTargeting: { - playerID: validPlayerID - } - }); + const targeting = getVatFromPlayer(validPlayerID); expect(targeting).to.deep.equal(targetingForCurrentItem); }); it('caches segments from the current item', function () { - getTargetingForBid({ - jwTargeting: { - playerID: validPlayerID - } - }); + getVatFromPlayer(validPlayerID); window.jwplayer = null; - const targeting2 = getTargetingForBid({ - jwTargeting: { - playerID: invalidPlayerID, - mediaID: mediaIdForCurrentItem - } - }); - expect(targeting2).to.deep.equal(targetingForCurrentItem); + const targeting = getVatFromCache(mediaIdForCurrentItem); + expect(targeting).to.deep.equal(targetingForCurrentItem); }); it('returns undefined segments when segments are absent', function () { - const targeting = getTargetingForBid({ - jwTargeting: { - playerID: validPlayerID, - mediaID: mediaIdNoSegment - } - }); + const targeting = getVatFromPlayer(validPlayerID, mediaIdNoSegment); expect(targeting).to.deep.equal({ mediaID: mediaIdNoSegment, segments: undefined }); }); + + describe('Get Bid Request Data', function () { + it('executes immediately while request is active if player has item', function () { + const bidRequestSpy = sinon.spy(); + const fakeServer = sinon.createFakeServer(); + fakeServer.respondImmediately = false; + fakeServer.autoRespond = false; + + fetchTargetingForMediaId(mediaIdWithSegment); + + const bid = {}; + const adUnit = { + fpd: { + context: { + data: { + jwTargeting: { + mediaID: mediaIdWithSegment, + playerID: validPlayerID + } + } + } + }, + bids: [ + bid + ] + }; + const expectedContentId = 'jw_' + mediaIdWithSegment; + const expectedTargeting = { + segments: validSegments, + content: { + id: expectedContentId + } + }; + jwplayerSubmodule.getBidRequestData({ adUnits: [adUnit] }, bidRequestSpy); + expect(bidRequestSpy.calledOnce).to.be.true; + expect(bid.rtd.jwplayer).to.have.deep.property('targeting', expectedTargeting); + fakeServer.respond(); + expect(bidRequestSpy.calledOnce).to.be.true; + }); + }); + }); + }); + + describe('Enrich ad units', function () { + const contentIdForSuccess = 'jw_' + testIdForSuccess; + const expectedTargetingForSuccess = { + segments: validSegments, + content: { + id: contentIdForSuccess + } + }; + let bidRequestSpy; + let fakeServer; + let clock; + + beforeEach(function () { + bidRequestSpy = sinon.spy(); + + fakeServer = sinon.createFakeServer(); + fakeServer.respondImmediately = false; + fakeServer.autoRespond = false; + + clock = sinon.useFakeTimers(); + }); + + afterEach(function () { + clock.restore(); + fakeServer.respond(); + }); + + it('adds targeting when pending request succeeds', function () { + fetchTargetingForMediaId(testIdForSuccess); + const bids = [ + { + id: 'bid1' + }, + { + id: 'bid2' + } + ]; + const adUnit = { + fpd: { + context: { + data: { + jwTargeting: { + mediaID: testIdForSuccess + } + } + } + }, + bids + }; + + enrichAdUnits([adUnit]); + const bid1 = bids[0]; + const bid2 = bids[1]; + expect(bid1).to.not.have.property('rtd'); + expect(bid2).to.not.have.property('rtd'); + + const request = fakeServer.requests[0]; + request.respond( + 200, + responseHeader, + JSON.stringify({ + playlist: [ + { + file: 'test.mp4', + jwpseg: validSegments + } + ] + }) + ); + + expect(bid1.rtd.jwplayer).to.have.deep.property('targeting', expectedTargetingForSuccess); + expect(bid2.rtd.jwplayer).to.have.deep.property('targeting', expectedTargetingForSuccess); + }); + + it('immediately adds cached targeting', function () { + fetchTargetingForMediaId(testIdForSuccess); + const bids = [ + { + id: 'bid1' + }, + { + id: 'bid2' + } + ]; + const adUnit = { + fpd: { + context: { + data: { + jwTargeting: { + mediaID: testIdForSuccess + } + } + } + }, + bids + }; + const request = fakeServer.requests[0]; + request.respond( + 200, + responseHeader, + JSON.stringify({ + playlist: [ + { + file: 'test.mp4', + jwpseg: validSegments + } + ] + }) + ); + + enrichAdUnits([adUnit]); + const bid1 = bids[0]; + const bid2 = bids[1]; + expect(bid1.rtd.jwplayer).to.have.deep.property('targeting', expectedTargetingForSuccess); + expect(bid2.rtd.jwplayer).to.have.deep.property('targeting', expectedTargetingForSuccess); + }); + + it('adds content block when segments are absent and no request is pending', function () { + const expectedTargetingForFailure = { + content: { + id: 'jw_' + testIdForFailure + } + }; + const bids = [ + { + id: 'bid1' + }, + { + id: 'bid2' + } + ]; + const adUnit = { + fpd: { + context: { + data: { + jwTargeting: { + mediaID: testIdForFailure + } + } + } + }, + bids + }; + + enrichAdUnits([adUnit]); + const bid1 = bids[0]; + const bid2 = bids[1]; + expect(bid1.rtd.jwplayer).to.have.deep.property('targeting', expectedTargetingForFailure); + expect(bid2.rtd.jwplayer).to.have.deep.property('targeting', expectedTargetingForFailure); + }); + }); + + describe(' Extract Publisher Params', function () { + it('should default to config', function () { + const config = { mediaID: 'test' }; + + const adUnit1 = { fpd: { context: {} } }; + const targeting1 = extractPublisherParams(adUnit1, config); + expect(targeting1).to.deep.equal(config); + + const adUnit2 = { fpd: { context: { data: { jwTargeting: {} } } } }; + const targeting2 = extractPublisherParams(adUnit2, config); + expect(targeting2).to.deep.equal(config); + + const targeting3 = extractPublisherParams(null, config); + expect(targeting3).to.deep.equal(config); + }); + + it('should prioritize adUnit properties ', function () { + const expectedMediaID = 'test_media_id'; + const expectedPlayerID = 'test_player_id'; + const config = { playerID: 'bad_id', mediaID: 'bad_id' }; + + const adUnit = { fpd: { context: { data: { jwTargeting: { mediaID: expectedMediaID, playerID: expectedPlayerID } } } } }; + const targeting = extractPublisherParams(adUnit, config); + expect(targeting).to.have.property('mediaID', expectedMediaID); + expect(targeting).to.have.property('playerID', expectedPlayerID); + }); + + it('should use config properties as fallbacks', function () { + const expectedMediaID = 'test_media_id'; + const expectedPlayerID = 'test_player_id'; + const config = { playerID: expectedPlayerID, mediaID: 'bad_id' }; + + const adUnit = { fpd: { context: { data: { jwTargeting: { mediaID: expectedMediaID } } } } }; + const targeting = extractPublisherParams(adUnit, config); + expect(targeting).to.have.property('mediaID', expectedMediaID); + expect(targeting).to.have.property('playerID', expectedPlayerID); + }); + + it('should return empty object when Publisher Params are absent', function () { + const targeting = extractPublisherParams(null, null); + expect(targeting).to.deep.equal({}); + }) + }); + + describe('Add Targeting to Bid', function () { + const targeting = {foo: 'bar'}; + + it('creates realTimeData when absent from Bid', function () { + const targeting = {foo: 'bar'}; + const bid = {}; + addTargetingToBid(bid, targeting); + expect(bid).to.have.property('rtd'); + expect(bid).to.have.nested.property('rtd.jwplayer.targeting', targeting); + }); + + it('adds to existing realTimeData', function () { + const otherRtd = { + targeting: { + seg: 'rtd seg' + } + }; + + const bid = { + rtd: { + otherRtd + } + }; + + addTargetingToBid(bid, targeting); + expect(bid).to.have.property('rtd'); + const rtd = bid.rtd; + expect(rtd).to.have.property('jwplayer'); + expect(rtd).to.have.nested.property('jwplayer.targeting', targeting); + + expect(rtd).to.have.deep.property('otherRtd', otherRtd); + }); + + it('adds to existing realTimeData.jwplayer', function () { + const otherInfo = { seg: 'rtd seg' }; + const bid = { + rtd: { + jwplayer: { + otherInfo + } + } + }; + addTargetingToBid(bid, targeting); + + expect(bid).to.have.property('rtd'); + const rtd = bid.rtd; + expect(rtd).to.have.property('jwplayer'); + expect(rtd).to.have.nested.property('jwplayer.otherInfo', otherInfo); + expect(rtd).to.have.nested.property('jwplayer.targeting', targeting); + }); + + it('overrides existing jwplayer.targeting', function () { + const otherInfo = { seg: 'rtd seg' }; + const bid = { + rtd: { + jwplayer: { + targeting: { + otherInfo + } + } + } + }; + addTargetingToBid(bid, targeting); + + expect(bid).to.have.property('rtd'); + const rtd = bid.rtd; + expect(rtd).to.have.property('jwplayer'); + expect(rtd).to.have.nested.property('jwplayer.targeting', targeting); + }); + + it('creates jwplayer when absent from realTimeData', function () { + const bid = { rtd: {} }; + addTargetingToBid(bid, targeting); + + expect(bid).to.have.property('rtd'); + const rtd = bid.rtd; + expect(rtd).to.have.property('jwplayer'); + expect(rtd).to.have.nested.property('jwplayer.targeting', targeting); }); }); @@ -267,13 +542,47 @@ describe('jwplayerRtdProvider', function() { expect(jwplayerSubmodule.init()).to.equal(true); }); - describe('getData', function () { + describe('Get Bid Request Data', function () { const validMediaIDs = ['media_ID_1', 'media_ID_2', 'media_ID_3']; let bidRequestSpy; let fakeServer; let clock; + let bidReqConfig; beforeEach(function () { + bidReqConfig = { + adUnits: [ + { + fpd: { + context: { + data: { + jwTargeting: { + mediaID: validMediaIDs[0] + } + } + } + }, + bids: [ + {}, {} + ] + }, + { + fpd: { + context: { + data: { + jwTargeting: { + mediaID: validMediaIDs[1] + } + } + } + }, + bids: [ + {}, {} + ] + } + ] + }; + bidRequestSpy = sinon.spy(); fakeServer = sinon.createFakeServer(); @@ -288,19 +597,24 @@ describe('jwplayerRtdProvider', function() { fakeServer.respond(); }); + it('executes callback immediately when ad units are missing', function () { + jwplayerSubmodule.getBidRequestData({ adUnits: [] }, bidRequestSpy); + expect(bidRequestSpy.calledOnce).to.be.true; + }); + it('executes callback immediately when no requests are pending', function () { fetchTargetingInformation({ mediaIDs: [] }); - jwplayerSubmodule.getData([], bidRequestSpy); + jwplayerSubmodule.getBidRequestData(bidReqConfig, bidRequestSpy); expect(bidRequestSpy.calledOnce).to.be.true; }); - it('executes callback after requests complete', function() { + it('executes callback only after requests in adUnit complete', function() { fetchTargetingInformation({ mediaIDs: validMediaIDs }); - jwplayerSubmodule.getData([], bidRequestSpy); + jwplayerSubmodule.getBidRequestData(bidReqConfig, bidRequestSpy); expect(bidRequestSpy.notCalled).to.be.true; const req1 = fakeServer.requests[0]; @@ -311,88 +625,102 @@ describe('jwplayerRtdProvider', function() { expect(bidRequestSpy.notCalled).to.be.true; req2.respond(); - expect(bidRequestSpy.notCalled).to.be.true; - - req3.respond(); - expect(bidRequestSpy.calledOnce).to.be.true; - }); - - it('executes callback after timeout', function () { - fetchTargetingInformation({ - mediaIDs: validMediaIDs - }); - jwplayerSubmodule.getData([], bidRequestSpy); - expect(bidRequestSpy.notCalled).to.be.true; - clock.tick(150); expect(bidRequestSpy.calledOnce).to.be.true; - }); - it('executes callback only once if requests succeed after timeout', function () { - fetchTargetingInformation({ - mediaIDs: validMediaIDs - }); - jwplayerSubmodule.getData([], bidRequestSpy); - expect(bidRequestSpy.notCalled).to.be.true; - clock.tick(150); - expect(bidRequestSpy.calledOnce).to.be.true; - - fakeServer.respond(); + req3.respond(); expect(bidRequestSpy.calledOnce).to.be.true; }); - it('returns data in proper structure', function () { - const adUnitCode = 'test_ad_unit'; + it('sets targeting data in proper structure', function () { + const bid = {}; const adUnitWithMediaId = { - code: adUnitCode, - jwTargeting: { - mediaID: testIdForSuccess - } + fpd: { + context: { + data: { + jwTargeting: { + mediaID: testIdForSuccess + } + } + } + }, + bids: [ + bid + ] }; const adUnitEmpty = { code: 'test_ad_unit_empty' }; - const expectedData = {}; const expectedContentId = 'jw_' + testIdForSuccess; - expectedData[adUnitCode] = { - jwTargeting: { - segments: validSegments, - content: { - id: expectedContentId - } + const expectedTargeting = { + segments: validSegments, + content: { + id: expectedContentId } }; - jwplayerSubmodule.getData([adUnitWithMediaId, adUnitEmpty], bidRequestSpy); - expect(bidRequestSpy.calledOnceWithExactly(expectedData)).to.be.true; + jwplayerSubmodule.getBidRequestData({ adUnits: [adUnitWithMediaId, adUnitEmpty] }, bidRequestSpy); + expect(bidRequestSpy.calledOnce).to.be.true; + expect(bid.rtd.jwplayer).to.have.deep.property('targeting', expectedTargeting); }); - it('returns an empty object when media id is invalid', function () { + it('excludes segments when absent', function () { const adUnitCode = 'test_ad_unit'; - const adUnitWithMediaId = { - code: adUnitCode, - jwTargeting: { - mediaID: testIdForFailure - } + const bid = {}; + const adUnit = { + fpd: { + context: { + data: { + jwTargeting: { + mediaID: testIdForFailure + } + } + } + }, + bids: [ bid ] }; - const adUnitEmpty = { - code: 'test_ad_unit_empty' + const expectedContentId = 'jw_' + adUnit.fpd.context.data.jwTargeting.mediaID; + const expectedTargeting = { + content: { + id: expectedContentId + } }; - jwplayerSubmodule.getData([adUnitWithMediaId, adUnitEmpty], bidRequestSpy); - expect(bidRequestSpy.calledOnceWithExactly({})).to.be.true; + jwplayerSubmodule.getBidRequestData({ adUnits: [ adUnit ] }, bidRequestSpy); + expect(bidRequestSpy.calledOnce).to.be.true; + expect(bid.rtd.jwplayer.targeting).to.not.have.property('segments'); + expect(bid.rtd.jwplayer.targeting).to.not.have.property('segments'); + expect(bid.rtd.jwplayer).to.have.deep.property('targeting', expectedTargeting); }); - it('returns an empty object when jwTargeting block is absent', function () { + it('does not modify bid when jwTargeting block is absent', function () { const adUnitCode = 'test_ad_unit'; + const bid1 = {}; + const bid2 = {}; + const bid3 = {}; const adUnitWithMediaId = { code: adUnitCode, - mediaID: testIdForSuccess + mediaID: testIdForSuccess, + bids: [ bid1 ] }; const adUnitEmpty = { - code: 'test_ad_unit_empty' + code: 'test_ad_unit_empty', + bids: [ bid2 ] }; - jwplayerSubmodule.getData([adUnitWithMediaId, adUnitEmpty], bidRequestSpy); - expect(bidRequestSpy.calledOnceWithExactly({})).to.be.true; + const adUnitEmptyfpd = { + code: 'test_ad_unit_empty_fpd', + fpd: { + context: { + id: 'sthg' + } + }, + bids: [ bid3 ] + }; + + jwplayerSubmodule.getBidRequestData({ adUnits: [adUnitWithMediaId, adUnitEmpty, adUnitEmptyfpd] }, bidRequestSpy); + expect(bidRequestSpy.calledOnce).to.be.true; + expect(bid1).to.not.have.property('rtd'); + expect(bid2).to.not.have.property('rtd'); + expect(bid3).to.not.have.property('rtd'); }); }); }); diff --git a/test/spec/modules/krushmediaBidAdapter_spec.js b/test/spec/modules/krushmediaBidAdapter_spec.js new file mode 100644 index 00000000000..3af9ed64c43 --- /dev/null +++ b/test/spec/modules/krushmediaBidAdapter_spec.js @@ -0,0 +1,329 @@ +import {expect} from 'chai'; +import {spec} from '../../../modules/krushmediaBidAdapter.js'; +import { BANNER, VIDEO, NATIVE } from '../../../src/mediaTypes.js'; + +describe('KrushmediabBidAdapter', function () { + const bid = { + bidId: '23fhj33i987f', + bidder: 'krushmedia', + mediaTypes: { + [BANNER]: { + sizes: [[300, 250]] + } + }, + params: { + key: 783, + traffic: BANNER + } + }; + + const bidderRequest = { + refererInfo: { + referer: 'test.com' + } + }; + + describe('isBidRequestValid', function () { + it('Should return true if there are bidId, params and key parameters present', function () { + expect(spec.isBidRequestValid(bid)).to.be.true; + }); + it('Should return false if at least one of parameters is not present', function () { + delete bid.params.key; + expect(spec.isBidRequestValid(bid)).to.be.false; + }); + }); + + describe('buildRequests', function () { + let serverRequest = spec.buildRequests([bid], bidderRequest); + it('Creates a ServerRequest object with method, URL and data', function () { + expect(serverRequest).to.exist; + expect(serverRequest.method).to.exist; + expect(serverRequest.url).to.exist; + expect(serverRequest.data).to.exist; + }); + it('Returns POST method', function () { + expect(serverRequest.method).to.equal('POST'); + }); + it('Returns valid URL', function () { + expect(serverRequest.url).to.equal('https://ads4.krushmedia.com/?c=rtb&m=hb'); + }); + it('Returns valid data if array of bids is valid', function () { + let data = serverRequest.data; + expect(data).to.be.an('object'); + expect(data).to.have.all.keys('deviceWidth', 'deviceHeight', 'language', 'secure', 'host', 'page', 'placements'); + expect(data.deviceWidth).to.be.a('number'); + expect(data.deviceHeight).to.be.a('number'); + expect(data.language).to.be.a('string'); + expect(data.secure).to.be.within(0, 1); + expect(data.host).to.be.a('string'); + expect(data.page).to.be.a('string'); + expect(data.gdpr).to.not.exist; + expect(data.ccpa).to.not.exist; + let placement = data['placements'][0]; + expect(placement).to.have.keys('key', 'bidId', 'traffic', 'sizes', 'schain'); + expect(placement.key).to.equal(783); + expect(placement.bidId).to.equal('23fhj33i987f'); + expect(placement.traffic).to.equal(BANNER); + expect(placement.schain).to.be.an('object'); + expect(placement.sizes).to.be.an('array'); + }); + + it('Returns valid data for mediatype video', function () { + const playerSize = [300, 300]; + bid.mediaTypes = {}; + bid.params.traffic = VIDEO; + bid.mediaTypes[VIDEO] = { + playerSize + }; + serverRequest = spec.buildRequests([bid], bidderRequest); + let data = serverRequest.data; + expect(data).to.be.an('object'); + let placement = data['placements'][0]; + expect(placement).to.be.an('object'); + expect(placement).to.have.keys('key', 'bidId', 'traffic', 'wPlayer', 'hPlayer', 'schain'); + expect(placement.traffic).to.equal(VIDEO); + expect(placement.wPlayer).to.equal(playerSize[0]); + expect(placement.hPlayer).to.equal(playerSize[1]); + }); + + it('Returns valid data for mediatype native', function () { + const native = { + title: { + required: true + }, + body: { + required: true + }, + icon: { + required: true, + size: [64, 64] + } + }; + + bid.mediaTypes = {}; + bid.params.traffic = NATIVE; + bid.mediaTypes[NATIVE] = native; + serverRequest = spec.buildRequests([bid], bidderRequest); + let data = serverRequest.data; + expect(data).to.be.an('object'); + let placement = data['placements'][0]; + expect(placement).to.be.an('object'); + expect(placement).to.have.keys('key', 'bidId', 'traffic', 'native', 'schain'); + expect(placement.traffic).to.equal(NATIVE); + expect(placement.native).to.equal(native); + }); + + it('Returns data with gdprConsent and without uspConsent', function () { + bidderRequest.gdprConsent = 'test'; + serverRequest = spec.buildRequests([bid], bidderRequest); + let data = serverRequest.data; + expect(data.gdpr).to.exist; + expect(data.gdpr).to.be.a('string'); + expect(data.gdpr).to.equal(bidderRequest.gdprConsent); + expect(data.ccpa).to.not.exist; + delete bidderRequest.gdprConsent; + }); + + it('Returns data with uspConsent and without gdprConsent', function () { + bidderRequest.uspConsent = 'test'; + serverRequest = spec.buildRequests([bid], bidderRequest); + let data = serverRequest.data; + expect(data.ccpa).to.exist; + expect(data.ccpa).to.be.a('string'); + expect(data.ccpa).to.equal(bidderRequest.uspConsent); + expect(data.gdpr).to.not.exist; + }); + + it('Returns empty data if no valid requests are passed', function () { + serverRequest = spec.buildRequests([]); + let data = serverRequest.data; + expect(data.placements).to.be.an('array').that.is.empty; + }); + }); + describe('interpretResponse', function () { + it('Should interpret banner response', function () { + const banner = { + body: [{ + mediaType: 'banner', + width: 300, + height: 250, + cpm: 0.4, + ad: 'Test', + requestId: '23fhj33i987f', + ttl: 120, + creativeId: '2', + netRevenue: true, + currency: 'USD', + dealId: '1' + }] + }; + let bannerResponses = spec.interpretResponse(banner); + expect(bannerResponses).to.be.an('array').that.is.not.empty; + let dataItem = bannerResponses[0]; + expect(dataItem).to.have.all.keys('requestId', 'cpm', 'width', 'height', 'ad', 'ttl', 'creativeId', + 'netRevenue', 'currency', 'dealId', 'mediaType'); + expect(dataItem.requestId).to.equal('23fhj33i987f'); + expect(dataItem.cpm).to.equal(0.4); + expect(dataItem.width).to.equal(300); + expect(dataItem.height).to.equal(250); + expect(dataItem.ad).to.equal('Test'); + expect(dataItem.ttl).to.equal(120); + expect(dataItem.creativeId).to.equal('2'); + expect(dataItem.netRevenue).to.be.true; + expect(dataItem.currency).to.equal('USD'); + }); + it('Should interpret video response', function () { + const video = { + body: [{ + vastUrl: 'test.com', + mediaType: 'video', + cpm: 0.5, + requestId: '23fhj33i987f', + ttl: 120, + creativeId: '2', + netRevenue: true, + currency: 'USD', + dealId: '1' + }] + }; + let videoResponses = spec.interpretResponse(video); + expect(videoResponses).to.be.an('array').that.is.not.empty; + + let dataItem = videoResponses[0]; + expect(dataItem).to.have.all.keys('requestId', 'cpm', 'vastUrl', 'ttl', 'creativeId', + 'netRevenue', 'currency', 'dealId', 'mediaType'); + expect(dataItem.requestId).to.equal('23fhj33i987f'); + expect(dataItem.cpm).to.equal(0.5); + expect(dataItem.vastUrl).to.equal('test.com'); + expect(dataItem.ttl).to.equal(120); + expect(dataItem.creativeId).to.equal('2'); + expect(dataItem.netRevenue).to.be.true; + expect(dataItem.currency).to.equal('USD'); + }); + it('Should interpret native response', function () { + const native = { + body: [{ + mediaType: 'native', + native: { + clickUrl: 'test.com', + title: 'Test', + image: 'test.com', + impressionTrackers: ['test.com'], + }, + ttl: 120, + cpm: 0.4, + requestId: '23fhj33i987f', + creativeId: '2', + netRevenue: true, + currency: 'USD', + }] + }; + let nativeResponses = spec.interpretResponse(native); + expect(nativeResponses).to.be.an('array').that.is.not.empty; + + let dataItem = nativeResponses[0]; + expect(dataItem).to.have.keys('requestId', 'cpm', 'ttl', 'creativeId', 'netRevenue', 'currency', 'mediaType', 'native'); + expect(dataItem.native).to.have.keys('clickUrl', 'impressionTrackers', 'title', 'image') + expect(dataItem.requestId).to.equal('23fhj33i987f'); + expect(dataItem.cpm).to.equal(0.4); + expect(dataItem.native.clickUrl).to.equal('test.com'); + expect(dataItem.native.title).to.equal('Test'); + expect(dataItem.native.image).to.equal('test.com'); + expect(dataItem.native.impressionTrackers).to.be.an('array').that.is.not.empty; + expect(dataItem.native.impressionTrackers[0]).to.equal('test.com'); + expect(dataItem.ttl).to.equal(120); + expect(dataItem.creativeId).to.equal('2'); + expect(dataItem.netRevenue).to.be.true; + expect(dataItem.currency).to.equal('USD'); + }); + it('Should return an empty array if invalid banner response is passed', function () { + const invBanner = { + body: [{ + width: 300, + cpm: 0.4, + ad: 'Test', + requestId: '23fhj33i987f', + ttl: 120, + creativeId: '2', + netRevenue: true, + currency: 'USD', + dealId: '1' + }] + }; + + let serverResponses = spec.interpretResponse(invBanner); + expect(serverResponses).to.be.an('array').that.is.empty; + }); + it('Should return an empty array if invalid video response is passed', function () { + const invVideo = { + body: [{ + mediaType: 'video', + cpm: 0.5, + requestId: '23fhj33i987f', + ttl: 120, + creativeId: '2', + netRevenue: true, + currency: 'USD', + dealId: '1' + }] + }; + let serverResponses = spec.interpretResponse(invVideo); + expect(serverResponses).to.be.an('array').that.is.empty; + }); + it('Should return an empty array if invalid native response is passed', function () { + const invNative = { + body: [{ + mediaType: 'native', + clickUrl: 'test.com', + title: 'Test', + impressionTrackers: ['test.com'], + ttl: 120, + requestId: '23fhj33i987f', + creativeId: '2', + netRevenue: true, + currency: 'USD', + }] + }; + let serverResponses = spec.interpretResponse(invNative); + expect(serverResponses).to.be.an('array').that.is.empty; + }); + it('Should return an empty array if invalid response is passed', function () { + const invalid = { + body: [{ + ttl: 120, + creativeId: '2', + netRevenue: true, + currency: 'USD', + dealId: '1' + }] + }; + let serverResponses = spec.interpretResponse(invalid); + expect(serverResponses).to.be.an('array').that.is.empty; + }); + }); + describe('getUserSyncs', function() { + it('Should return array of objects with proper sync config , include GDPR', function() { + const syncData = spec.getUserSyncs({}, {}, { + consentString: 'ALL', + gdprApplies: true, + }, {}); + expect(syncData).to.be.an('array').which.is.not.empty; + expect(syncData[0]).to.be.an('object') + expect(syncData[0].type).to.be.a('string') + expect(syncData[0].type).to.equal('iframe') + expect(syncData[0].url).to.be.a('string') + expect(syncData[0].url).to.equal('https://cs.krushmedia.com/html?src=pbjs&gdpr=1&gdpr_consent=ALL') + }); + it('Should return array of objects with proper sync config , include CCPA', function() { + const syncData = spec.getUserSyncs({}, {}, {}, { + consentString: '1NNN' + }); + expect(syncData).to.be.an('array').which.is.not.empty; + expect(syncData[0]).to.be.an('object') + expect(syncData[0].type).to.be.a('string') + expect(syncData[0].type).to.equal('iframe') + expect(syncData[0].url).to.be.a('string') + expect(syncData[0].url).to.equal('https://cs.krushmedia.com/html?src=pbjs&ccpa_consent=1NNN') + }); + }); +}); diff --git a/test/spec/modules/kubientBidAdapter_spec.js b/test/spec/modules/kubientBidAdapter_spec.js new file mode 100644 index 00000000000..1df4370b2ba --- /dev/null +++ b/test/spec/modules/kubientBidAdapter_spec.js @@ -0,0 +1,259 @@ +import { expect, assert } from 'chai'; +import { spec } from 'modules/kubientBidAdapter.js'; + +describe('KubientAdapter', function () { + let bid = { + bidId: '2dd581a2b6281d', + bidder: 'kubient', + bidderRequestId: '145e1d6a7837c9', + params: { + zoneid: '5678', + floor: 0.05, + }, + auctionId: '74f78609-a92d-4cf1-869f-1b244bbfb5d2', + mediaTypes: { + banner: { + sizes: [[300, 250]] + } + }, + transactionId: '3bb2f6da-87a6-4029-aeb0-bfe951372e62', + schain: { + ver: '1.0', + complete: 1, + nodes: [ + { + asi: 'example.com', + sid: '0', + hp: 1, + rid: 'bidrequestid', + domain: 'example.com' + } + ] + } + }; + let consentString = 'BOJ8RZsOJ8RZsABAB8AAAAAZ+A=='; + let uspConsentData = '1YCC'; + let bidderRequest = { + bidderCode: 'kubient', + auctionId: 'fffffff-ffff-ffff-ffff-ffffffffffff', + bidderRequestId: 'ffffffffffffff', + start: 1472239426002, + auctionStart: 1472239426000, + timeout: 5000, + refererInfo: { + referer: 'http://www.example.com', + reachedTop: true, + }, + gdprConsent: { + consentString: consentString, + gdprApplies: true + }, + uspConsent: uspConsentData, + bids: [bid] + }; + describe('buildRequests', function () { + let serverRequests = spec.buildRequests([bid], bidderRequest); + it('Creates a ServerRequest object with method, URL and data', function () { + expect(serverRequests).to.be.an('array'); + }); + for (let i = 0; i < serverRequests.length; i++) { + let serverRequest = serverRequests[i]; + it('Creates a ServerRequest object with method, URL and data', function () { + expect(serverRequest.method).to.be.a('string'); + expect(serverRequest.url).to.be.a('string'); + expect(serverRequest.data).to.be.a('string'); + }); + it('Returns POST method', function () { + expect(serverRequest.method).to.equal('POST'); + }); + it('Returns valid URL', function () { + expect(serverRequest.url).to.equal('https://kssp.kbntx.ch/pbjs'); + }); + it('Returns valid data if array of bids is valid', function () { + let data = JSON.parse(serverRequest.data); + expect(data).to.be.an('object'); + expect(data).to.have.all.keys('v', 'requestId', 'adSlots', 'gdpr', 'referer', 'tmax', 'consent', 'consentGiven', 'uspConsent'); + expect(data.v).to.exist.and.to.be.a('string'); + expect(data.requestId).to.exist.and.to.be.a('string'); + expect(data.referer).to.be.a('string'); + expect(data.tmax).to.exist.and.to.be.a('number'); + expect(data.gdpr).to.exist.and.to.be.within(0, 1); + expect(data.consent).to.equal(consentString); + expect(data.uspConsent).to.exist.and.to.equal(uspConsentData); + for (let j = 0; j < data['adSlots'].length; j++) { + let adSlot = data['adSlots'][i]; + expect(adSlot).to.have.all.keys('bidId', 'zoneId', 'floor', 'sizes', 'schain', 'mediaTypes'); + expect(adSlot.bidId).to.be.a('string'); + expect(adSlot.zoneId).to.be.a('string'); + expect(adSlot.floor).to.be.a('number'); + expect(adSlot.sizes).to.be.an('array'); + expect(adSlot.schain).to.be.an('object'); + expect(adSlot.mediaTypes).to.be.an('object'); + } + }); + } + }); + + describe('isBidRequestValid', function () { + it('Should return true when required params are found', function () { + expect(spec.isBidRequestValid(bid)).to.be.true; + }); + it('Should return false when required params are not found', function () { + expect(spec.isBidRequestValid(bid)).to.be.true; + }); + it('Should return false when params are not found', function () { + delete bid.params; + expect(spec.isBidRequestValid(bid)).to.be.false; + }); + }); + + describe('interpretResponse', function () { + it('Should interpret response', function () { + const serverResponse = { + body: + { + seatbid: [ + { + bid: [ + { + bidId: '000', + price: 1.5, + adm: '
test
', + creativeId: 'creativeId', + w: 300, + h: 250, + cur: 'USD', + netRevenue: false, + ttl: 360 + } + ] + } + ] + } + }; + let bannerResponses = spec.interpretResponse(serverResponse); + expect(bannerResponses).to.be.an('array').that.is.not.empty; + let dataItem = bannerResponses[0]; + expect(dataItem).to.have.all.keys('requestId', 'cpm', 'ad', 'creativeId', 'width', 'height', 'currency', 'netRevenue', 'ttl'); + expect(dataItem.requestId).to.exist.and.to.be.a('string').and.to.equal(serverResponse.body.seatbid[0].bid[0].bidId); + expect(dataItem.cpm).to.exist.and.to.be.a('number').and.to.equal(serverResponse.body.seatbid[0].bid[0].price); + expect(dataItem.ad).to.exist.and.to.be.a('string').and.to.have.string(serverResponse.body.seatbid[0].bid[0].adm); + expect(dataItem.creativeId).to.exist.and.to.be.a('string').and.to.equal(serverResponse.body.seatbid[0].bid[0].creativeId); + expect(dataItem.width).to.exist.and.to.be.a('number').and.to.equal(serverResponse.body.seatbid[0].bid[0].w); + expect(dataItem.height).to.exist.and.to.be.a('number').and.to.equal(serverResponse.body.seatbid[0].bid[0].h); + expect(dataItem.currency).to.exist.and.to.be.a('string').and.to.equal(serverResponse.body.seatbid[0].bid[0].cur); + expect(dataItem.netRevenue).to.exist.and.to.be.a('boolean').and.to.equal(serverResponse.body.seatbid[0].bid[0].netRevenue); + expect(dataItem.ttl).to.exist.and.to.be.a('number').and.to.equal(serverResponse.body.seatbid[0].bid[0].ttl); + }); + + it('Should return no ad when not given a server response', function () { + const ads = spec.interpretResponse(null); + expect(ads).to.be.an('array').and.to.have.length(0); + }); + }); + + describe('getUserSyncs', function () { + it('should register the sync iframe without gdpr', function () { + let syncOptions = { + iframeEnabled: true + }; + let serverResponses = null; + let gdprConsent = { + consentString: consentString + }; + let uspConsent = null; + let syncs = spec.getUserSyncs(syncOptions, serverResponses, gdprConsent, uspConsent); + expect(syncs).to.be.an('array').and.to.have.length(1); + expect(syncs[0].type).to.equal('iframe'); + expect(syncs[0].url).to.equal('https://kdmp.kbntx.ch/init.html?consent_str=' + consentString + '&consent_given=0'); + }); + it('should register the sync iframe with gdpr', function () { + let syncOptions = { + iframeEnabled: true + }; + let serverResponses = null; + let gdprConsent = { + gdprApplies: true, + consentString: consentString + }; + let uspConsent = null; + let syncs = spec.getUserSyncs(syncOptions, serverResponses, gdprConsent, uspConsent); + expect(syncs).to.be.an('array').and.to.have.length(1); + expect(syncs[0].type).to.equal('iframe'); + expect(syncs[0].url).to.equal('https://kdmp.kbntx.ch/init.html?consent_str=' + consentString + '&gdpr=1&consent_given=0'); + }); + it('should register the sync iframe with gdpr vendor', function () { + let syncOptions = { + iframeEnabled: true + }; + let serverResponses = null; + let gdprConsent = { + gdprApplies: true, + consentString: consentString, + apiVersion: 1, + vendorData: { + vendorConsents: { + 794: 1 + } + } + }; + let uspConsent = null; + let syncs = spec.getUserSyncs(syncOptions, serverResponses, gdprConsent, uspConsent); + expect(syncs).to.be.an('array').and.to.have.length(1); + expect(syncs[0].type).to.equal('iframe'); + expect(syncs[0].url).to.equal('https://kdmp.kbntx.ch/init.html?consent_str=' + consentString + '&gdpr=1&consent_given=1'); + }); + it('should register the sync image without gdpr', function () { + let syncOptions = { + pixelEnabled: true + }; + let serverResponses = null; + let gdprConsent = { + consentString: consentString + }; + let uspConsent = null; + let syncs = spec.getUserSyncs(syncOptions, serverResponses, gdprConsent, uspConsent); + expect(syncs).to.be.an('array').and.to.have.length(1); + expect(syncs[0].type).to.equal('image'); + expect(syncs[0].url).to.equal('https://kdmp.kbntx.ch/init.png?consent_str=' + consentString + '&consent_given=0'); + }); + it('should register the sync image with gdpr', function () { + let syncOptions = { + pixelEnabled: true + }; + let serverResponses = null; + let gdprConsent = { + gdprApplies: true, + consentString: consentString + }; + let uspConsent = null; + let syncs = spec.getUserSyncs(syncOptions, serverResponses, gdprConsent, uspConsent); + expect(syncs).to.be.an('array').and.to.have.length(1); + expect(syncs[0].type).to.equal('image'); + expect(syncs[0].url).to.equal('https://kdmp.kbntx.ch/init.png?consent_str=' + consentString + '&gdpr=1&consent_given=0'); + }); + it('should register the sync image with gdpr vendor', function () { + let syncOptions = { + pixelEnabled: true + }; + let serverResponses = null; + let gdprConsent = { + gdprApplies: true, + consentString: consentString, + apiVersion: 2, + vendorData: { + vendor: { + consents: { + 794: 1 + } + } + } + }; + let uspConsent = null; + let syncs = spec.getUserSyncs(syncOptions, serverResponses, gdprConsent, uspConsent); + expect(syncs).to.be.an('array').and.to.have.length(1); + expect(syncs[0].type).to.equal('image'); + expect(syncs[0].url).to.equal('https://kdmp.kbntx.ch/init.png?consent_str=' + consentString + '&gdpr=1&consent_given=1'); + }); + }) +}); diff --git a/test/spec/modules/lemmaBidAdapter_spec.js b/test/spec/modules/lemmaBidAdapter_spec.js index a236ac17d71..a00c25d126c 100644 --- a/test/spec/modules/lemmaBidAdapter_spec.js +++ b/test/spec/modules/lemmaBidAdapter_spec.js @@ -331,5 +331,39 @@ describe('lemmaBidAdapter', function() { }); }); }); + describe('getUserSyncs', function() { + const syncurl_iframe = 'https://sync.lemmatechnologies.com/js/usersync.html?pid=1001'; + let sandbox; + beforeEach(function() { + sandbox = sinon.sandbox.create(); + }); + afterEach(function() { + sandbox.restore(); + }); + + it('execute as per config', function() { + expect(spec.getUserSyncs({ iframeEnabled: true }, {}, undefined, undefined)).to.deep.equal([{ + type: 'iframe', url: syncurl_iframe + }]); + }); + + it('CCPA/USP', function() { + expect(spec.getUserSyncs({ iframeEnabled: true }, {}, undefined, '1NYN')).to.deep.equal([{ + type: 'iframe', url: `${syncurl_iframe}&us_privacy=1NYN` + }]); + }); + + it('GDPR', function() { + expect(spec.getUserSyncs({ iframeEnabled: true }, {}, { gdprApplies: true, consentString: 'foo' }, undefined)).to.deep.equal([{ + type: 'iframe', url: `${syncurl_iframe}&gdpr=1&gdpr_consent=foo` + }]); + expect(spec.getUserSyncs({ iframeEnabled: true }, {}, { gdprApplies: false, consentString: 'foo' }, undefined)).to.deep.equal([{ + type: 'iframe', url: `${syncurl_iframe}&gdpr=0&gdpr_consent=foo` + }]); + expect(spec.getUserSyncs({ iframeEnabled: true }, {}, { gdprApplies: true, consentString: undefined }, undefined)).to.deep.equal([{ + type: 'iframe', url: `${syncurl_iframe}&gdpr=1&gdpr_consent=` + }]); + }); + }); }); }); diff --git a/test/spec/modules/liveIntentIdSystem_spec.js b/test/spec/modules/liveIntentIdSystem_spec.js index b19d38d5859..aae60cbcd19 100644 --- a/test/spec/modules/liveIntentIdSystem_spec.js +++ b/test/spec/modules/liveIntentIdSystem_spec.js @@ -1,46 +1,50 @@ -import {liveIntentIdSubmodule, reset as resetLiveIntentIdSubmodule, storage} from 'modules/liveIntentIdSystem.js'; +import { liveIntentIdSubmodule, reset as resetLiveIntentIdSubmodule, storage } from 'modules/liveIntentIdSystem.js'; import * as utils from 'src/utils.js'; -import {uspDataHandler} from '../../../src/adapterManager.js'; -import {server} from 'test/mocks/xhr.js'; +import { gdprDataHandler, uspDataHandler } from '../../../src/adapterManager.js'; +import { server } from 'test/mocks/xhr.js'; const PUBLISHER_ID = '89899'; -const defaultConfigParams = {publisherId: PUBLISHER_ID}; +const defaultConfigParams = { params: {publisherId: PUBLISHER_ID} }; const responseHeader = {'Content-Type': 'application/json'} -describe('LiveIntentId', function () { - let pixel = {}; +describe('LiveIntentId', function() { let logErrorStub; - let consentDataStub; + let uspConsentDataStub; + let gdprConsentDataStub; let getCookieStub; let getDataFromLocalStorageStub; let imgStub; - beforeEach(function () { - imgStub = sinon.stub(window, 'Image').returns(pixel); + beforeEach(function() { + imgStub = sinon.stub(utils, 'triggerPixel'); getCookieStub = sinon.stub(storage, 'getCookie'); getDataFromLocalStorageStub = sinon.stub(storage, 'getDataFromLocalStorage'); logErrorStub = sinon.stub(utils, 'logError'); - consentDataStub = sinon.stub(uspDataHandler, 'getConsentData'); + uspConsentDataStub = sinon.stub(uspDataHandler, 'getConsentData'); + gdprConsentDataStub = sinon.stub(gdprDataHandler, 'getConsentData'); }); - afterEach(function () { - pixel = {}; + afterEach(function() { imgStub.restore(); getCookieStub.restore(); getDataFromLocalStorageStub.restore(); logErrorStub.restore(); - consentDataStub.restore(); + uspConsentDataStub.restore(); + gdprConsentDataStub.restore(); resetLiveIntentIdSubmodule(); }); - it('should initialize LiveConnect with a us privacy string when getId, and include it in all requests', function () { - consentDataStub.returns('1YNY'); + it('should initialize LiveConnect with a privacy string when getId, and include it in the resolution request', function() { + uspConsentDataStub.returns('1YNY'); + gdprConsentDataStub.returns({ + gdprApplies: true, + consentString: 'consentDataString' + }) let callBackSpy = sinon.spy(); let submoduleCallback = liveIntentIdSubmodule.getId(defaultConfigParams).callback; - expect(pixel.src).to.match(/.*us_privacy=1YNY/); submoduleCallback(callBackSpy); - let request = server.requests[0]; - expect(request.url).to.match(/.*us_privacy=1YNY/); + let request = server.requests[1]; + expect(request.url).to.match(/.*us_privacy=1YNY.*&gdpr=1&gdpr_consent=consentDataString.*/); request.respond( 200, responseHeader, @@ -49,14 +53,27 @@ describe('LiveIntentId', function () { expect(callBackSpy.calledOnce).to.be.true; }); - it('should fire an event when getId', function () { + it('should fire an event when getId', function() { + uspConsentDataStub.returns('1YNY'); + gdprConsentDataStub.returns({ + gdprApplies: true, + consentString: 'consentDataString' + }) liveIntentIdSubmodule.getId(defaultConfigParams); - expect(pixel.src).to.match(/https:\/\/rp.liadm.com\/p\?wpn=prebid.*/) + expect(server.requests[0].url).to.match(/https:\/\/rp.liadm.com\/j\?wpn=prebid.*us_privacy=1YNY.*&gdpr=1&gdpr_consent=consentDataString.*/); }); - it('should initialize LiveConnect with the config params when decode and emit an event', function () { - liveIntentIdSubmodule.decode({}, { + it('should fire an event when getId and a hash is provided', function() { + liveIntentIdSubmodule.getId({ params: { ...defaultConfigParams, + emailHash: '58131bc547fb87af94cebdaf3102321f' + }}); + expect(server.requests[0].url).to.match(/https:\/\/rp.liadm.com\/j\?.*e=58131bc547fb87af94cebdaf3102321f.+/) + }); + + it('should initialize LiveConnect with the config params when decode and emit an event', function () { + liveIntentIdSubmodule.decode({}, { params: { + ...defaultConfigParams.params, ...{ url: 'https://dummy.liveintent.com', liCollectConfig: { @@ -64,40 +81,52 @@ describe('LiveIntentId', function () { collectorUrl: 'https://collector.liveintent.com' } } - }); - expect(pixel.src).to.match(/https:\/\/collector.liveintent.com\/p\?aid=a-0001&wpn=prebid.*/) + }}); + expect(server.requests[0].url).to.match(/https:\/\/collector.liveintent.com\/j\?aid=a-0001&wpn=prebid.*/); }); - it('should initialize LiveConnect and emit an event with a us privacy string when decode', function () { - consentDataStub.returns('1YNY'); + it('should initialize LiveConnect and emit an event with a privacy string when decode', function() { + uspConsentDataStub.returns('1YNY'); + gdprConsentDataStub.returns({ + gdprApplies: false, + consentString: 'consentDataString' + }) liveIntentIdSubmodule.decode({}, defaultConfigParams); - expect(pixel.src).to.match(/.*us_privacy=1YNY/); + expect(server.requests[0].url).to.match(/.*us_privacy=1YNY.*&gdpr=0&gdpr_consent=consentDataString.*/); + }); + + it('should fire an event when decode and a hash is provided', function() { + liveIntentIdSubmodule.decode({}, { params: { + ...defaultConfigParams.params, + emailHash: '58131bc547fb87af94cebdaf3102321f' + }}); + expect(server.requests[0].url).to.match(/https:\/\/rp.liadm.com\/j\?.*e=58131bc547fb87af94cebdaf3102321f.+/); }); - it('should not return a decoded identifier when the unifiedId is not present in the value', function () { - const result = liveIntentIdSubmodule.decode({additionalData: 'data'}); + it('should not return a decoded identifier when the unifiedId is not present in the value', function() { + const result = liveIntentIdSubmodule.decode({ additionalData: 'data' }); expect(result).to.be.undefined; }); - it('should fire an event when decode', function () { + it('should fire an event when decode', function() { liveIntentIdSubmodule.decode({}, defaultConfigParams); - expect(pixel.src).to.be.not.null + expect(server.requests[0].url).to.be.not.null }); - it('should initialize LiveConnect and send data only once', function () { + it('should initialize LiveConnect and send data only once', function() { liveIntentIdSubmodule.getId(defaultConfigParams); liveIntentIdSubmodule.decode({}, defaultConfigParams); liveIntentIdSubmodule.getId(defaultConfigParams); liveIntentIdSubmodule.decode({}, defaultConfigParams); - expect(imgStub.calledOnce).to.be.true; + expect(server.requests.length).to.be.eq(1); }); - it('should call the Custom URL of the LiveIntent Identity Exchange endpoint', function () { + it('should call the Custom URL of the LiveIntent Identity Exchange endpoint', function() { getCookieStub.returns(null); let callBackSpy = sinon.spy(); - let submoduleCallback = liveIntentIdSubmodule.getId({...defaultConfigParams, ...{'url': 'https://dummy.liveintent.com/idex'}}).callback; + let submoduleCallback = liveIntentIdSubmodule.getId({ params: {...defaultConfigParams.params, ...{'url': 'https://dummy.liveintent.com/idex'}} }).callback; submoduleCallback(callBackSpy); - let request = server.requests[0]; + let request = server.requests[1]; expect(request.url).to.be.eq('https://dummy.liveintent.com/idex/prebid/89899'); request.respond( 200, @@ -107,18 +136,18 @@ describe('LiveIntentId', function () { expect(callBackSpy.calledOnce).to.be.true; }); - it('should call the default url of the LiveIntent Identity Exchange endpoint, with a partner', function () { + it('should call the default url of the LiveIntent Identity Exchange endpoint, with a partner', function() { getCookieStub.returns(null); let callBackSpy = sinon.spy(); - let submoduleCallback = liveIntentIdSubmodule.getId({ - ...defaultConfigParams, + let submoduleCallback = liveIntentIdSubmodule.getId({ params: { + ...defaultConfigParams.params, ...{ 'url': 'https://dummy.liveintent.com/idex', 'partner': 'rubicon' } - }).callback; + } }).callback; submoduleCallback(callBackSpy); - let request = server.requests[0]; + let request = server.requests[1]; expect(request.url).to.be.eq('https://dummy.liveintent.com/idex/rubicon/89899'); request.respond( 200, @@ -128,12 +157,12 @@ describe('LiveIntentId', function () { expect(callBackSpy.calledOnce).to.be.true; }); - it('should call the LiveIntent Identity Exchange endpoint, with no additional query params', function () { + it('should call the LiveIntent Identity Exchange endpoint, with no additional query params', function() { getCookieStub.returns(null); let callBackSpy = sinon.spy(); let submoduleCallback = liveIntentIdSubmodule.getId(defaultConfigParams).callback; submoduleCallback(callBackSpy); - let request = server.requests[0]; + let request = server.requests[1]; expect(request.url).to.be.eq('https://idx.liadm.com/idex/prebid/89899'); request.respond( 200, @@ -143,12 +172,12 @@ describe('LiveIntentId', function () { expect(callBackSpy.calledOnce).to.be.true; }); - it('should log an error and continue to callback if ajax request errors', function () { + it('should log an error and continue to callback if ajax request errors', function() { getCookieStub.returns(null); let callBackSpy = sinon.spy(); let submoduleCallback = liveIntentIdSubmodule.getId(defaultConfigParams).callback; submoduleCallback(callBackSpy); - let request = server.requests[0]; + let request = server.requests[1]; expect(request.url).to.be.eq('https://idx.liadm.com/idex/prebid/89899'); request.respond( 503, @@ -159,13 +188,13 @@ describe('LiveIntentId', function () { expect(callBackSpy.calledOnce).to.be.true; }); - it('should include the LiveConnect identifier when calling the LiveIntent Identity Exchange endpoint', function () { + it('should include the LiveConnect identifier when calling the LiveIntent Identity Exchange endpoint', function() { const oldCookie = 'a-xxxx--123e4567-e89b-12d3-a456-426655440000' getDataFromLocalStorageStub.withArgs('_li_duid').returns(oldCookie); let callBackSpy = sinon.spy(); let submoduleCallback = liveIntentIdSubmodule.getId(defaultConfigParams).callback; submoduleCallback(callBackSpy); - let request = server.requests[0]; + let request = server.requests[1]; expect(request.url).to.be.eq(`https://idx.liadm.com/idex/prebid/89899?duid=${oldCookie}`); request.respond( 200, @@ -175,20 +204,20 @@ describe('LiveIntentId', function () { expect(callBackSpy.calledOnce).to.be.true; }); - it('should include the LiveConnect identifier and additional Identifiers to resolve', function () { + it('should include the LiveConnect identifier and additional Identifiers to resolve', function() { const oldCookie = 'a-xxxx--123e4567-e89b-12d3-a456-426655440000' getDataFromLocalStorageStub.withArgs('_li_duid').returns(oldCookie); getDataFromLocalStorageStub.withArgs('_thirdPC').returns('third-pc'); - const configParams = { - ...defaultConfigParams, + const configParams = { params: { + ...defaultConfigParams.params, ...{ 'identifiersToResolve': ['_thirdPC'] } - }; + }}; let callBackSpy = sinon.spy(); let submoduleCallback = liveIntentIdSubmodule.getId(configParams).callback; submoduleCallback(callBackSpy); - let request = server.requests[0]; + let request = server.requests[1]; expect(request.url).to.be.eq(`https://idx.liadm.com/idex/prebid/89899?duid=${oldCookie}&_thirdPC=third-pc`); request.respond( 200, @@ -198,19 +227,19 @@ describe('LiveIntentId', function () { expect(callBackSpy.calledOnce).to.be.true; }); - it('should include an additional identifier value to resolve even if it is an object', function () { + it('should include an additional identifier value to resolve even if it is an object', function() { getCookieStub.returns(null); getDataFromLocalStorageStub.withArgs('_thirdPC').returns({'key': 'value'}); - const configParams = { - ...defaultConfigParams, + const configParams = { params: { + ...defaultConfigParams.params, ...{ 'identifiersToResolve': ['_thirdPC'] } - }; + }}; let callBackSpy = sinon.spy(); let submoduleCallback = liveIntentIdSubmodule.getId(configParams).callback; submoduleCallback(callBackSpy); - let request = server.requests[0]; + let request = server.requests[1]; expect(request.url).to.be.eq('https://idx.liadm.com/idex/prebid/89899?_thirdPC=%7B%22key%22%3A%22value%22%7D'); request.respond( 200, @@ -219,4 +248,10 @@ describe('LiveIntentId', function () { ); expect(callBackSpy.calledOnce).to.be.true; }); + + it('should send an error when the cookie jar throws an unexpected error', function() { + getCookieStub.throws('CookieError', 'A message'); + liveIntentIdSubmodule.getId(defaultConfigParams); + expect(imgStub.getCall(0).args[0]).to.match(/.*ae=.+/); + }); }); diff --git a/test/spec/modules/livewrappedAnalyticsAdapter_spec.js b/test/spec/modules/livewrappedAnalyticsAdapter_spec.js index c723f589fa0..ba9430e0b95 100644 --- a/test/spec/modules/livewrappedAnalyticsAdapter_spec.js +++ b/test/spec/modules/livewrappedAnalyticsAdapter_spec.js @@ -121,6 +121,7 @@ const MOCK = { const ANALYTICS_MESSAGE = { publisherId: 'CC411485-42BC-4F92-8389-42C503EE38D7', gdpr: [{}], + auctionIds: ['25c6d7f5-699a-4bfc-87c9-996f915341fa'], bidAdUnits: [ { adUnit: 'panorama_d_1', @@ -136,19 +137,22 @@ const ANALYTICS_MESSAGE = { adUnit: 'panorama_d_1', bidder: 'livewrapped', timeStamp: 1519149562216, - gdpr: 0 + gdpr: 0, + auctionId: 0 }, { adUnit: 'box_d_1', bidder: 'livewrapped', timeStamp: 1519149562216, - gdpr: 0 + gdpr: 0, + auctionId: 0 }, { adUnit: 'box_d_2', bidder: 'livewrapped', timeStamp: 1519149562216, - gdpr: 0 + gdpr: 0, + auctionId: 0 } ], responses: [ @@ -161,7 +165,9 @@ const ANALYTICS_MESSAGE = { cpm: 1.1, ttr: 200, IsBid: true, - mediaType: 1 + mediaType: 1, + gdpr: 0, + auctionId: 0 }, { timeStamp: 1519149562216, @@ -172,14 +178,18 @@ const ANALYTICS_MESSAGE = { cpm: 2.2, ttr: 300, IsBid: true, - mediaType: 1 + mediaType: 1, + gdpr: 0, + auctionId: 0 }, { timeStamp: 1519149562216, adUnit: 'box_d_2', bidder: 'livewrapped', ttr: 200, - IsBid: false + IsBid: false, + gdpr: 0, + auctionId: 0 } ], timeouts: [], @@ -191,7 +201,9 @@ const ANALYTICS_MESSAGE = { width: 980, height: 240, cpm: 1.1, - mediaType: 1 + mediaType: 1, + gdpr: 0, + auctionId: 0 }, { timeStamp: 1519149562216, @@ -200,7 +212,9 @@ const ANALYTICS_MESSAGE = { width: 300, height: 250, cpm: 2.2, - mediaType: 1 + mediaType: 1, + gdpr: 0, + auctionId: 0 } ] }; @@ -351,7 +365,9 @@ describe('Livewrapped analytics adapter', function () { } }, ); - events.emit(BID_TIMEOUT, MOCK.BID_TIMEOUT); + + events.emit(BID_RESPONSE, MOCK.BID_RESPONSE[0]); + events.emit(BID_WON, MOCK.BID_WON[0]); events.emit(AUCTION_END, MOCK.AUCTION_END); clock.tick(BID_WON_TIMEOUT + 1000); @@ -366,6 +382,104 @@ describe('Livewrapped analytics adapter', function () { expect(message.requests.length).to.equal(2); expect(message.requests[0].gdpr).to.equal(0); expect(message.requests[1].gdpr).to.equal(0); + + expect(message.responses.length).to.equal(1); + expect(message.responses[0].gdpr).to.equal(0); + + expect(message.wins.length).to.equal(1); + expect(message.wins[0].gdpr).to.equal(0); + }); + + it('should forward floor data', function () { + events.emit(AUCTION_INIT, MOCK.AUCTION_INIT); + events.emit(BID_REQUESTED, { + 'bidder': 'livewrapped', + 'auctionId': '25c6d7f5-699a-4bfc-87c9-996f915341fa', + 'bidderRequestId': '1be65d7958826a', + 'bids': [ + { + 'bidder': 'livewrapped', + 'adUnitCode': 'panorama_d_1', + 'bidId': '2ecff0db240757', + 'floorData': { + 'floorValue': 1.1, + 'floorCurrency': 'SEK' + } + } + ], + 'start': 1519149562216 + }); + + events.emit(BID_RESPONSE, MOCK.BID_RESPONSE[0]); + events.emit(BID_WON, MOCK.BID_WON[0]); + events.emit(AUCTION_END, MOCK.AUCTION_END); + + clock.tick(BID_WON_TIMEOUT + 1000); + + expect(server.requests.length).to.equal(1); + let request = server.requests[0]; + let message = JSON.parse(request.requestBody); + + expect(message.gdpr.length).to.equal(1); + + expect(message.responses.length).to.equal(1); + expect(message.responses[0].floor).to.equal(1.1); + expect(message.responses[0].floorCur).to.equal('SEK'); + + expect(message.wins.length).to.equal(1); + expect(message.wins[0].floor).to.equal(1.1); + expect(message.wins[0].floorCur).to.equal('SEK'); + }); + + it('should forward Livewrapped floor data', function () { + events.emit(AUCTION_INIT, MOCK.AUCTION_INIT); + events.emit(BID_REQUESTED, { + 'bidder': 'livewrapped', + 'auctionId': '25c6d7f5-699a-4bfc-87c9-996f915341fa', + 'bidderRequestId': '1be65d7958826a', + 'bids': [ + { + 'bidder': 'livewrapped', + 'adUnitCode': 'panorama_d_1', + 'bidId': '2ecff0db240757', + 'lwflr': { + 'flr': 1.1 + } + }, + { + 'bidder': 'livewrapped', + 'adUnitCode': 'box_d_1', + 'bidId': '3ecff0db240757', + 'lwflr': { + 'flr': 1.1, + 'bflrs': {'livewrapped': 2.2} + } + } + ], + 'start': 1519149562216 + }); + + events.emit(BID_RESPONSE, MOCK.BID_RESPONSE[0]); + events.emit(BID_RESPONSE, MOCK.BID_RESPONSE[1]); + events.emit(BID_WON, MOCK.BID_WON[0]); + events.emit(BID_WON, MOCK.BID_WON[1]); + events.emit(AUCTION_END, MOCK.AUCTION_END); + + clock.tick(BID_WON_TIMEOUT + 1000); + + expect(server.requests.length).to.equal(1); + let request = server.requests[0]; + let message = JSON.parse(request.requestBody); + + expect(message.gdpr.length).to.equal(1); + + expect(message.responses.length).to.equal(2); + expect(message.responses[0].floor).to.equal(1.1); + expect(message.responses[1].floor).to.equal(2.2); + + expect(message.wins.length).to.equal(2); + expect(message.wins[0].floor).to.equal(1.1); + expect(message.wins[1].floor).to.equal(2.2); }); }); diff --git a/test/spec/modules/livewrappedBidAdapter_spec.js b/test/spec/modules/livewrappedBidAdapter_spec.js index 053e5102cbf..7983e8fbb0b 100644 --- a/test/spec/modules/livewrappedBidAdapter_spec.js +++ b/test/spec/modules/livewrappedBidAdapter_spec.js @@ -787,7 +787,7 @@ describe('Livewrapped adapter tests', function () { let testbidRequest = clone(bidderRequest); delete testbidRequest.bids[0].params.userId; testbidRequest.bids[0].userId = {}; - testbidRequest.bids[0].userId.id5id = 'id5-user-id'; + testbidRequest.bids[0].userId.id5id = { uid: 'id5-user-id' }; let result = spec.buildRequests(testbidRequest.bids, testbidRequest); let data = JSON.parse(result.data); @@ -811,7 +811,7 @@ describe('Livewrapped adapter tests', function () { let data = JSON.parse(result.data); expect(data.rtbData.user.ext.eids).to.deep.equal([{ - 'source': 'pubcommon', + 'source': 'pubcid.org', 'uids': [{ 'id': 'publisher-common-id', 'atype': 1 @@ -819,6 +819,25 @@ describe('Livewrapped adapter tests', function () { }]); }); + it('should make use of criteoId if available', function() { + sandbox.stub(utils, 'isSafariBrowser').callsFake(() => false); + sandbox.stub(storage, 'cookiesAreEnabled').callsFake(() => true); + let testbidRequest = clone(bidderRequest); + delete testbidRequest.bids[0].params.userId; + testbidRequest.bids[0].userId = {}; + testbidRequest.bids[0].userId.criteoId = 'criteo-id'; + let result = spec.buildRequests(testbidRequest.bids, testbidRequest); + let data = JSON.parse(result.data); + + expect(data.rtbData.user.ext.eids).to.deep.equal([{ + 'source': 'criteo.com', + 'uids': [{ + 'id': 'criteo-id', + 'atype': 1 + }] + }]); + }); + it('should send schain object if available', function() { sandbox.stub(utils, 'isSafariBrowser').callsFake(() => false); sandbox.stub(storage, 'cookiesAreEnabled').callsFake(() => true); diff --git a/test/spec/modules/lkqdBidAdapter_spec.js b/test/spec/modules/lkqdBidAdapter_spec.js index f10d936a28b..1cd33d9ec59 100644 --- a/test/spec/modules/lkqdBidAdapter_spec.js +++ b/test/spec/modules/lkqdBidAdapter_spec.js @@ -1,6 +1,7 @@ import { spec } from 'modules/lkqdBidAdapter.js'; import { newBidder } from 'src/adapters/bidderFactory.js'; -const { expect } = require('chai'); +import { config } from 'src/config.js'; +import { expect } from 'chai'; describe('LKQD Bid Adapter Test', () => { const adapter = newBidder(spec); @@ -47,7 +48,9 @@ describe('LKQD Bid Adapter Test', () => { 'bidder': 'lkqd', 'params': { 'siteId': '662921', - 'placementId': '263' + 'placementId': '263', + 'c1': 'newWindow', + 'c20': 'lkqdCustom' }, 'adUnitCode': 'lkqd', 'sizes': [[300, 250], [640, 480]], @@ -75,6 +78,10 @@ describe('LKQD Bid Adapter Test', () => { ]; it('should populate available parameters', () => { + sinon.stub(config, 'getConfig') + .withArgs('coppa') + .returns(true); + const requests = spec.buildRequests(bidRequests); expect(requests.length).to.equal(2); const r1 = requests[0].data; @@ -82,14 +89,26 @@ describe('LKQD Bid Adapter Test', () => { expect(r1).to.have.string('&sid=662921&'); expect(r1).to.have.string('&width=300&'); expect(r1).to.have.string('&height=250&'); + expect(r1).to.have.string('&coppa=1&'); + expect(r1).to.have.string('&c1=newWindow&'); + expect(r1).to.have.string('&c20=lkqdCustom'); const r2 = requests[1].data; expect(r2).to.have.string('pid=263&'); expect(r2).to.have.string('&sid=662921&'); expect(r2).to.have.string('&width=640&'); expect(r2).to.have.string('&height=480&'); + expect(r2).to.have.string('&coppa=1&'); + expect(r2).to.have.string('&c1=newWindow&'); + expect(r2).to.have.string('&c20=lkqdCustom'); + + config.getConfig.restore(); }); it('should not populate unspecified parameters', () => { + sinon.stub(config, 'getConfig') + .withArgs('coppa') + .returns(false); + const requests = spec.buildRequests(bidRequests); expect(requests.length).to.equal(2); const r1 = requests[0].data; @@ -99,6 +118,8 @@ describe('LKQD Bid Adapter Test', () => { expect(r1).to.not.have.string('&contentlength='); expect(r1).to.not.have.string('&contenturl='); expect(r1).to.not.have.string('&schain='); + expect(r1).to.not.have.string('&c10='); + expect(r1).to.not.have.string('coppa'); const r2 = requests[1].data; expect(r2).to.not.have.string('&dnt='); expect(r2).to.not.have.string('&contentid='); @@ -106,6 +127,10 @@ describe('LKQD Bid Adapter Test', () => { expect(r2).to.not.have.string('&contentlength='); expect(r2).to.not.have.string('&contenturl='); expect(r2).to.not.have.string('&schain='); + expect(r2).to.not.have.string('&c39='); + expect(r2).to.not.have.string('coppa'); + + config.getConfig.restore(); }); it('should handle single size request', () => { diff --git a/test/spec/modules/lunamediahbBidAdapter_spec.js b/test/spec/modules/lunamediahbBidAdapter_spec.js new file mode 100644 index 00000000000..e9f88935ed5 --- /dev/null +++ b/test/spec/modules/lunamediahbBidAdapter_spec.js @@ -0,0 +1,304 @@ +import {expect} from 'chai'; +import {spec} from '../../../modules/lunamediahbBidAdapter.js'; +import { BANNER, VIDEO, NATIVE } from '../../../src/mediaTypes.js'; + +describe('LunamediaHBBidAdapter', function () { + const bid = { + bidId: '23fhj33i987f', + bidder: 'lunamediahb', + mediaTypes: { + [BANNER]: { + sizes: [[300, 250]] + } + }, + params: { + placementId: 783, + traffic: BANNER + } + }; + + const bidderRequest = { + refererInfo: { + referer: 'test.com' + } + }; + + describe('isBidRequestValid', function () { + it('Should return true if there are bidId, params and key parameters present', function () { + expect(spec.isBidRequestValid(bid)).to.be.true; + }); + it('Should return false if at least one of parameters is not present', function () { + delete bid.params.placementId; + expect(spec.isBidRequestValid(bid)).to.be.false; + }); + }); + + describe('buildRequests', function () { + let serverRequest = spec.buildRequests([bid], bidderRequest); + it('Creates a ServerRequest object with method, URL and data', function () { + expect(serverRequest).to.exist; + expect(serverRequest.method).to.exist; + expect(serverRequest.url).to.exist; + expect(serverRequest.data).to.exist; + }); + it('Returns POST method', function () { + expect(serverRequest.method).to.equal('POST'); + }); + it('Returns valid URL', function () { + expect(serverRequest.url).to.equal('https://balancer.lmgssp.com/?c=o&m=multi'); + }); + it('Returns valid data if array of bids is valid', function () { + let data = serverRequest.data; + expect(data).to.be.an('object'); + expect(data).to.have.all.keys('deviceWidth', 'deviceHeight', 'language', 'secure', 'host', 'page', 'placements'); + expect(data.deviceWidth).to.be.a('number'); + expect(data.deviceHeight).to.be.a('number'); + expect(data.language).to.be.a('string'); + expect(data.secure).to.be.within(0, 1); + expect(data.host).to.be.a('string'); + expect(data.page).to.be.a('string'); + expect(data.gdpr).to.not.exist; + expect(data.ccpa).to.not.exist; + let placement = data['placements'][0]; + expect(placement).to.have.keys('placementId', 'bidId', 'traffic', 'sizes', 'schain'); + expect(placement.placementId).to.equal(783); + expect(placement.bidId).to.equal('23fhj33i987f'); + expect(placement.traffic).to.equal(BANNER); + expect(placement.schain).to.be.an('object'); + expect(placement.sizes).to.be.an('array'); + }); + + it('Returns valid data for mediatype video', function () { + const playerSize = [300, 300]; + bid.mediaTypes = {}; + bid.params.traffic = VIDEO; + bid.mediaTypes[VIDEO] = { + playerSize + }; + serverRequest = spec.buildRequests([bid], bidderRequest); + let data = serverRequest.data; + expect(data).to.be.an('object'); + let placement = data['placements'][0]; + expect(placement).to.be.an('object'); + expect(placement).to.have.keys('placementId', 'bidId', 'traffic', 'wPlayer', 'hPlayer', 'schain'); + expect(placement.traffic).to.equal(VIDEO); + expect(placement.wPlayer).to.equal(playerSize[0]); + expect(placement.hPlayer).to.equal(playerSize[1]); + }); + + it('Returns valid data for mediatype native', function () { + const native = { + title: { + required: true + }, + body: { + required: true + }, + icon: { + required: true, + size: [64, 64] + } + }; + + bid.mediaTypes = {}; + bid.params.traffic = NATIVE; + bid.mediaTypes[NATIVE] = native; + serverRequest = spec.buildRequests([bid], bidderRequest); + let data = serverRequest.data; + expect(data).to.be.an('object'); + let placement = data['placements'][0]; + expect(placement).to.be.an('object'); + expect(placement).to.have.keys('placementId', 'bidId', 'traffic', 'native', 'schain'); + expect(placement.traffic).to.equal(NATIVE); + expect(placement.native).to.equal(native); + }); + + it('Returns data with gdprConsent and without uspConsent', function () { + bidderRequest.gdprConsent = 'test'; + serverRequest = spec.buildRequests([bid], bidderRequest); + let data = serverRequest.data; + expect(data.gdpr).to.exist; + expect(data.gdpr).to.be.a('string'); + expect(data.gdpr).to.equal(bidderRequest.gdprConsent); + expect(data.ccpa).to.not.exist; + delete bidderRequest.gdprConsent; + }); + + it('Returns data with uspConsent and without gdprConsent', function () { + bidderRequest.uspConsent = 'test'; + serverRequest = spec.buildRequests([bid], bidderRequest); + let data = serverRequest.data; + expect(data.ccpa).to.exist; + expect(data.ccpa).to.be.a('string'); + expect(data.ccpa).to.equal(bidderRequest.uspConsent); + expect(data.gdpr).to.not.exist; + }); + + it('Returns empty data if no valid requests are passed', function () { + serverRequest = spec.buildRequests([]); + let data = serverRequest.data; + expect(data.placements).to.be.an('array').that.is.empty; + }); + }); + describe('interpretResponse', function () { + it('Should interpret banner response', function () { + const banner = { + body: [{ + mediaType: 'banner', + width: 300, + height: 250, + cpm: 0.4, + ad: 'Test', + requestId: '23fhj33i987f', + ttl: 120, + creativeId: '2', + netRevenue: true, + currency: 'USD', + dealId: '1' + }] + }; + let bannerResponses = spec.interpretResponse(banner); + expect(bannerResponses).to.be.an('array').that.is.not.empty; + let dataItem = bannerResponses[0]; + expect(dataItem).to.have.all.keys('requestId', 'cpm', 'width', 'height', 'ad', 'ttl', 'creativeId', + 'netRevenue', 'currency', 'dealId', 'mediaType'); + expect(dataItem.requestId).to.equal('23fhj33i987f'); + expect(dataItem.cpm).to.equal(0.4); + expect(dataItem.width).to.equal(300); + expect(dataItem.height).to.equal(250); + expect(dataItem.ad).to.equal('Test'); + expect(dataItem.ttl).to.equal(120); + expect(dataItem.creativeId).to.equal('2'); + expect(dataItem.netRevenue).to.be.true; + expect(dataItem.currency).to.equal('USD'); + }); + it('Should interpret video response', function () { + const video = { + body: [{ + vastUrl: 'test.com', + mediaType: 'video', + cpm: 0.5, + requestId: '23fhj33i987f', + ttl: 120, + creativeId: '2', + netRevenue: true, + currency: 'USD', + dealId: '1' + }] + }; + let videoResponses = spec.interpretResponse(video); + expect(videoResponses).to.be.an('array').that.is.not.empty; + + let dataItem = videoResponses[0]; + expect(dataItem).to.have.all.keys('requestId', 'cpm', 'vastUrl', 'ttl', 'creativeId', + 'netRevenue', 'currency', 'dealId', 'mediaType'); + expect(dataItem.requestId).to.equal('23fhj33i987f'); + expect(dataItem.cpm).to.equal(0.5); + expect(dataItem.vastUrl).to.equal('test.com'); + expect(dataItem.ttl).to.equal(120); + expect(dataItem.creativeId).to.equal('2'); + expect(dataItem.netRevenue).to.be.true; + expect(dataItem.currency).to.equal('USD'); + }); + it('Should interpret native response', function () { + const native = { + body: [{ + mediaType: 'native', + native: { + clickUrl: 'test.com', + title: 'Test', + image: 'test.com', + impressionTrackers: ['test.com'], + }, + ttl: 120, + cpm: 0.4, + requestId: '23fhj33i987f', + creativeId: '2', + netRevenue: true, + currency: 'USD', + }] + }; + let nativeResponses = spec.interpretResponse(native); + expect(nativeResponses).to.be.an('array').that.is.not.empty; + + let dataItem = nativeResponses[0]; + expect(dataItem).to.have.keys('requestId', 'cpm', 'ttl', 'creativeId', 'netRevenue', 'currency', 'mediaType', 'native'); + expect(dataItem.native).to.have.keys('clickUrl', 'impressionTrackers', 'title', 'image') + expect(dataItem.requestId).to.equal('23fhj33i987f'); + expect(dataItem.cpm).to.equal(0.4); + expect(dataItem.native.clickUrl).to.equal('test.com'); + expect(dataItem.native.title).to.equal('Test'); + expect(dataItem.native.image).to.equal('test.com'); + expect(dataItem.native.impressionTrackers).to.be.an('array').that.is.not.empty; + expect(dataItem.native.impressionTrackers[0]).to.equal('test.com'); + expect(dataItem.ttl).to.equal(120); + expect(dataItem.creativeId).to.equal('2'); + expect(dataItem.netRevenue).to.be.true; + expect(dataItem.currency).to.equal('USD'); + }); + it('Should return an empty array if invalid banner response is passed', function () { + const invBanner = { + body: [{ + width: 300, + cpm: 0.4, + ad: 'Test', + requestId: '23fhj33i987f', + ttl: 120, + creativeId: '2', + netRevenue: true, + currency: 'USD', + dealId: '1' + }] + }; + + let serverResponses = spec.interpretResponse(invBanner); + expect(serverResponses).to.be.an('array').that.is.empty; + }); + it('Should return an empty array if invalid video response is passed', function () { + const invVideo = { + body: [{ + mediaType: 'video', + cpm: 0.5, + requestId: '23fhj33i987f', + ttl: 120, + creativeId: '2', + netRevenue: true, + currency: 'USD', + dealId: '1' + }] + }; + let serverResponses = spec.interpretResponse(invVideo); + expect(serverResponses).to.be.an('array').that.is.empty; + }); + it('Should return an empty array if invalid native response is passed', function () { + const invNative = { + body: [{ + mediaType: 'native', + clickUrl: 'test.com', + title: 'Test', + impressionTrackers: ['test.com'], + ttl: 120, + requestId: '23fhj33i987f', + creativeId: '2', + netRevenue: true, + currency: 'USD', + }] + }; + let serverResponses = spec.interpretResponse(invNative); + expect(serverResponses).to.be.an('array').that.is.empty; + }); + it('Should return an empty array if invalid response is passed', function () { + const invalid = { + body: [{ + ttl: 120, + creativeId: '2', + netRevenue: true, + currency: 'USD', + dealId: '1' + }] + }; + let serverResponses = spec.interpretResponse(invalid); + expect(serverResponses).to.be.an('array').that.is.empty; + }); + }); +}); diff --git a/test/spec/modules/malltvBidAdapter_spec.js b/test/spec/modules/malltvBidAdapter_spec.js index 10161b319c5..e1e9ad867e7 100644 --- a/test/spec/modules/malltvBidAdapter_spec.js +++ b/test/spec/modules/malltvBidAdapter_spec.js @@ -80,7 +80,11 @@ describe('malltvAdapterTest', () => { it('bidRequest sizes', () => { const requests = spec.buildRequests(bidRequests); - expect(requests[0].data.sizes).to.equal('300x250'); + requests.forEach(function (requestItem) { + expect(requestItem.data.placements).to.exist; + expect(requestItem.data.placements.length).to.equal(1); + expect(requestItem.data.placements[0].sizes).to.equal('300x250'); + }); }); }); @@ -128,7 +132,9 @@ describe('malltvAdapterTest', () => { 'netRevenue', 'ttl', 'referrer', - 'ad' + 'ad', + 'vastUrl', + 'mediaType' ]; let resultKeys = Object.keys(result[0]); diff --git a/test/spec/modules/mediaforceBidAdapter_spec.js b/test/spec/modules/mediaforceBidAdapter_spec.js index ee478acbc83..0b3271da770 100644 --- a/test/spec/modules/mediaforceBidAdapter_spec.js +++ b/test/spec/modules/mediaforceBidAdapter_spec.js @@ -100,6 +100,37 @@ describe('mediaforce bid adapter', function () { transactionId: 'd45dd707-a418-42ec-b8a7-b70a6c6fab0b', }; + const multiBid = [ + { + publisher_id: 'pub123', + placement_id: '202', + }, + { + publisher_id: 'pub123', + placement_id: '203', + }, + { + publisher_id: 'pub124', + placement_id: '202', + }, + { + publisher_id: 'pub123', + placement_id: '203', + transactionId: '8df76688-1618-417a-87b1-60ad046841c9' + } + ].map(({publisher_id, placement_id, transactionId}) => { + return { + bidder: 'mediaforce', + params: {publisher_id, placement_id}, + mediaTypes: { + banner: { + sizes: [[300, 250], [600, 400]] + } + }, + transactionId: transactionId || 'd45dd707-a418-42ec-b8a7-b70a6c6fab0b' + } + }); + const refererInfo = { referer: 'https://www.prebid.org', reachedTop: true, @@ -111,7 +142,9 @@ describe('mediaforce bid adapter', function () { const requestUrl = `${baseUrl}/header_bid`; const dnt = utils.getDNT() ? 1 : 0; - const secure = 1; + const secure = window.location.protocol === 'https' ? 1 : 0; + const pageUrl = window.location.href; + const timeout = 1500; it('should return undefined if no validBidRequests passed', function () { assert.equal(spec.buildRequests([]), undefined); @@ -127,18 +160,29 @@ describe('mediaforce bid adapter', function () { bid.params.bidfloor = 0.5; let bidRequests = [bid]; - let bidderRequest = {bids: bidRequests, refererInfo: refererInfo}; + let bidderRequest = { + bids: bidRequests, + refererInfo: refererInfo, + timeout: timeout, + auctionId: '210a474e-88f0-4646-837f-4253b7cf14fb' + }; let [request] = spec.buildRequests(bidRequests, bidderRequest); let data = JSON.parse(request.data); assert.deepEqual(data, { - id: bid.transactionId, + id: data.id, + tmax: timeout, + ext: { + mediaforce: { + hb_key: bidderRequest.auctionId + } + }, site: { id: bid.params.publisher_id, publisher: {id: bid.params.publisher_id}, ref: encodeURIComponent(refererInfo.referer), - page: encodeURIComponent(refererInfo.referer), + page: pageUrl, }, device: { ua: navigator.userAgent, @@ -150,6 +194,11 @@ describe('mediaforce bid adapter', function () { tagid: bid.params.placement_id, secure: secure, bidfloor: bid.params.bidfloor, + ext: { + mediaforce: { + transactionId: bid.transactionId + } + }, banner: {w: 300, h: 250}, native: { ver: '1.2', @@ -170,7 +219,7 @@ describe('mediaforce bid adapter', function () { assert.deepEqual(request, { method: 'POST', url: requestUrl, - data: '{"id":"d45dd707-a418-42ec-b8a7-b70a6c6fab0b","site":{"page":"https%3A%2F%2Fwww.prebid.org","ref":"https%3A%2F%2Fwww.prebid.org","id":"pub123","publisher":{"id":"pub123"}},"device":{"ua":"' + navigator.userAgent + '","js":1,"dnt":' + dnt + ',"language":"' + language + '"},"imp":[{"tagid":"202","secure":1,"bidfloor":0.5,"banner":{"w":300,"h":250},"native":{"ver":"1.2","request":{"assets":[{"required":1,"id":1,"title":{"len":800}},{"required":1,"id":3,"img":{"type":3,"w":300,"h":250}},{"required":1,"id":5,"data":{"type":1}}],"context":1,"plcmttype":1,"ver":"1.2"}}}]}', + data: '{"id":"' + data.id + '","site":{"page":"' + pageUrl + '","ref":"https%3A%2F%2Fwww.prebid.org","id":"pub123","publisher":{"id":"pub123"}},"device":{"ua":"' + navigator.userAgent + '","js":1,"dnt":' + dnt + ',"language":"' + language + '"},"ext":{"mediaforce":{"hb_key":"210a474e-88f0-4646-837f-4253b7cf14fb"}},"tmax":1500,"imp":[{"tagid":"202","secure":' + secure + ',"bidfloor":0.5,"ext":{"mediaforce":{"transactionId":"d45dd707-a418-42ec-b8a7-b70a6c6fab0b"}},"banner":{"w":300,"h":250},"native":{"ver":"1.2","request":{"assets":[{"required":1,"id":1,"title":{"len":800}},{"required":1,"id":3,"img":{"type":3,"w":300,"h":250}},{"required":1,"id":5,"data":{"type":1}}],"context":1,"plcmttype":1,"ver":"1.2"}}}]}', }); }); @@ -186,6 +235,116 @@ describe('mediaforce bid adapter', function () { let data = JSON.parse(request.data); assert.deepEqual(data.imp[0].banner, {w: 300, h: 600, format: [{w: 300, h: 250}]}); }); + + it('should return proper requests for multiple imps', function () { + let bidderRequest = { + bids: multiBid, + refererInfo: refererInfo, + timeout: timeout, + auctionId: '210a474e-88f0-4646-837f-4253b7cf14fb' + }; + + let requests = spec.buildRequests(multiBid, bidderRequest); + assert.equal(requests.length, 2); + requests.forEach((req) => { + req.data = JSON.parse(req.data); + }); + + assert.deepEqual(requests, [ + { + method: 'POST', + url: requestUrl, + data: { + id: requests[0].data.id, + tmax: timeout, + ext: { + mediaforce: { + hb_key: bidderRequest.auctionId + } + }, + site: { + id: 'pub123', + publisher: {id: 'pub123'}, + ref: encodeURIComponent(refererInfo.referer), + page: pageUrl, + }, + device: { + ua: navigator.userAgent, + dnt: dnt, + js: 1, + language: language, + }, + imp: [{ + tagid: '202', + secure: secure, + bidfloor: 0, + ext: { + mediaforce: { + transactionId: 'd45dd707-a418-42ec-b8a7-b70a6c6fab0b' + } + }, + banner: {w: 300, h: 250, format: [{w: 600, h: 400}]}, + }, { + tagid: '203', + secure: secure, + bidfloor: 0, + ext: { + mediaforce: { + transactionId: 'd45dd707-a418-42ec-b8a7-b70a6c6fab0b' + } + }, + banner: {w: 300, h: 250, format: [{w: 600, h: 400}]}, + }, { + tagid: '203', + secure: secure, + bidfloor: 0, + ext: { + mediaforce: { + transactionId: '8df76688-1618-417a-87b1-60ad046841c9' + } + }, + banner: {w: 300, h: 250, format: [{w: 600, h: 400}]}, + }] + } + }, + { + method: 'POST', + url: requestUrl, + data: { + id: requests[1].data.id, + tmax: timeout, + ext: { + mediaforce: { + hb_key: bidderRequest.auctionId + } + }, + site: { + id: 'pub124', + publisher: {id: 'pub124'}, + ref: encodeURIComponent(refererInfo.referer), + page: pageUrl, + }, + device: { + ua: navigator.userAgent, + dnt: dnt, + js: 1, + language: language, + }, + imp: [{ + tagid: '202', + secure: secure, + bidfloor: 0, + ext: { + mediaforce: { + transactionId: 'd45dd707-a418-42ec-b8a7-b70a6c6fab0b' + } + }, + banner: {w: 300, h: 250, format: [{w: 600, h: 400}]}, + }] + } + } + ]); + }); }); describe('interpretResponse() banner', function () { diff --git a/test/spec/modules/medianetAnalyticsAdapter_spec.js b/test/spec/modules/medianetAnalyticsAdapter_spec.js index 97b45cef00c..41a6338225e 100644 --- a/test/spec/modules/medianetAnalyticsAdapter_spec.js +++ b/test/spec/modules/medianetAnalyticsAdapter_spec.js @@ -9,20 +9,23 @@ const { } = CONSTANTS; const MOCK = { - AUCTION_INIT: {'auctionId': '8e0d5245-deb3-406c-96ca-9b609e077ff7', 'timestamp': 1584563605739}, - AUCTION_INIT_WITH_FLOOR: {'auctionId': '8e0d5245-deb3-406c-96ca-9b609e077ff7', 'timestamp': 1584563605739, 'bidderRequests': [{'bids': [{ 'floorData': {'enforcements': {'enforceJS': true}} }]}]}, + Ad_Units: [{'code': 'div-gpt-ad-1460505748561-0', 'mediaTypes': {'banner': {'sizes': [[300, 250]]}}, 'bids': [], 'ext': {'prop1': 'value1'}}], + MULTI_FORMAT_TWIN_AD_UNITS: [{'code': 'div-gpt-ad-1460505748561-0', 'mediaTypes': {'banner': {'sizes': [[300, 250]]}, 'native': {'image': {'required': true, 'sizes': [150, 50]}}}, 'bids': [], 'ext': {'prop1': 'value1'}}, {'code': 'div-gpt-ad-1460505748561-0', 'mediaTypes': {'video': {'playerSize': [640, 480], 'context': 'instream'}}, 'bids': [], 'ext': {'prop1': 'value1'}}], + AUCTION_INIT: {'auctionId': '8e0d5245-deb3-406c-96ca-9b609e077ff7', 'timestamp': 1584563605739, 'timeout': 6000}, + AUCTION_INIT_WITH_FLOOR: {'auctionId': '8e0d5245-deb3-406c-96ca-9b609e077ff7', 'timestamp': 1584563605739, 'timeout': 6000, 'bidderRequests': [{'bids': [{ 'floorData': {'enforcements': {'enforceJS': true}} }]}]}, BID_REQUESTED: {'bidderCode': 'medianet', 'auctionId': '8e0d5245-deb3-406c-96ca-9b609e077ff7', 'bids': [{'bidder': 'medianet', 'params': {'cid': 'TEST_CID', 'crid': '451466393'}, 'mediaTypes': {'banner': {'sizes': [[300, 250]], 'ext': ['asdads']}}, 'adUnitCode': 'div-gpt-ad-1460505748561-0', 'sizes': [[300, 250]], 'bidId': '28248b0e6aece2', 'auctionId': '8e0d5245-deb3-406c-96ca-9b609e077ff7', 'src': 'client'}], 'auctionStart': 1584563605739, 'timeout': 6000, 'uspConsent': '1YY', 'start': 1584563605743}, - MULTI_FORMAT_BID_REQUESTED: {'bidderCode': 'medianet', 'auctionId': '8e0d5245-deb3-406c-96ca-9b609e077ff7', 'bids': [{'bidder': 'medianet', 'params': {'cid': 'TEST_CID', 'crid': '451466393'}, 'mediaTypes': {'banner': {'sizes': [[300, 250]]}, 'video': {'playerSize': [640, 480], 'context': 'instream'}, 'native': {'image': {'required': true, 'sizes': [150, 50]}, 'title': {'required': true, 'len': 80}}, 'ext': ['asdads']}, 'adUnitCode': 'div-gpt-ad-1460505748561-0', 'sizes': [[300, 250]], 'bidId': '28248b0e6aece2', 'auctionId': '8e0d5245-deb3-406c-96ca-9b609e077ff7', 'src': 'client'}], 'auctionStart': 1584563605739, 'timeout': 6000, 'uspConsent': '1YY', 'start': 1584563605743}, + MULTI_FORMAT_BID_REQUESTED: {'bidderCode': 'medianet', 'auctionId': '8e0d5245-deb3-406c-96ca-9b609e077ff7', 'bids': [{'bidder': 'medianet', 'params': {'cid': 'TEST_CID', 'crid': '451466393'}, 'mediaTypes': {'banner': {'sizes': [[300, 250]]}, 'video': {'playerSize': [640, 480], 'context': 'instream'}, 'native': {'image': {'required': true, 'sizes': [150, 50]}, 'title': {'required': true, 'len': 80}}}, 'adUnitCode': 'div-gpt-ad-1460505748561-0', 'sizes': [[300, 250]], 'bidId': '28248b0e6aece2', 'auctionId': '8e0d5245-deb3-406c-96ca-9b609e077ff7', 'src': 'client'}], 'auctionStart': 1584563605739, 'timeout': 6000, 'uspConsent': '1YY', 'start': 1584563605743}, BID_RESPONSE: {'bidderCode': 'medianet', 'width': 300, 'height': 250, 'adId': '3e6e4bce5c8fb3', 'requestId': '28248b0e6aece2', 'mediaType': 'banner', 'source': 'client', 'ext': {'pvid': 123, 'crid': '321'}, 'no_bid': false, 'cpm': 2.299, 'ad': 'AD_CODE', 'ttl': 180, 'creativeId': 'Test1', 'netRevenue': true, 'currency': 'USD', 'dfp_id': 'div-gpt-ad-1460505748561-0', 'originalCpm': 1.1495, 'originalCurrency': 'USD', 'floorData': {'floorValue': 1.10, 'floorRule': 'banner'}, 'auctionId': '8e0d5245-deb3-406c-96ca-9b609e077ff7', 'responseTimestamp': 1584563606009, 'requestTimestamp': 1584563605743, 'bidder': 'medianet', 'adUnitCode': 'div-gpt-ad-1460505748561-0', 'timeToRespond': 266, 'pbLg': '2.00', 'pbMg': '2.20', 'pbHg': '2.29', 'pbAg': '2.25', 'pbDg': '2.29', 'pbCg': '2.00', 'size': '300x250', 'adserverTargeting': {'hb_bidder': 'medianet', 'hb_adid': '3e6e4bce5c8fb3', 'hb_pb': '2.00', 'hb_size': '300x250', 'hb_source': 'client', 'hb_format': 'banner', 'prebid_test': 1}, 'status': 'rendered', 'params': [{'cid': 'test123', 'crid': '451466393'}]}, AUCTION_END: {'auctionId': '8e0d5245-deb3-406c-96ca-9b609e077ff7', 'auctionEnd': 1584563605739}, SET_TARGETING: {'div-gpt-ad-1460505748561-0': {'prebid_test': '1', 'hb_format': 'banner', 'hb_source': 'client', 'hb_size': '300x250', 'hb_pb': '2.00', 'hb_adid': '3e6e4bce5c8fb3', 'hb_bidder': 'medianet', 'hb_format_medianet': 'banner', 'hb_source_medianet': 'client', 'hb_size_medianet': '300x250', 'hb_pb_medianet': '2.00', 'hb_adid_medianet': '3e6e4bce5c8fb3', 'hb_bidder_medianet': 'medianet'}}, + NO_BID_SET_TARGETING: {'div-gpt-ad-1460505748561-0': {}}, BID_WON: {'bidderCode': 'medianet', 'width': 300, 'height': 250, 'statusMessage': 'Bid available', 'adId': '3e6e4bce5c8fb3', 'requestId': '28248b0e6aece2', 'mediaType': 'banner', 'source': 'client', 'no_bid': false, 'cpm': 2.299, 'ad': 'AD_CODE', 'ttl': 180, 'creativeId': 'Test1', 'netRevenue': true, 'currency': 'USD', 'dfp_id': 'div-gpt-ad-1460505748561-0', 'originalCpm': 1.1495, 'originalCurrency': 'USD', 'auctionId': '8e0d5245-deb3-406c-96ca-9b609e077ff7', 'responseTimestamp': 1584563606009, 'requestTimestamp': 1584563605743, 'bidder': 'medianet', 'adUnitCode': 'div-gpt-ad-1460505748561-0', 'timeToRespond': 266, 'pbLg': '2.00', 'pbMg': '2.20', 'pbHg': '2.29', 'pbAg': '2.25', 'pbDg': '2.29', 'pbCg': '2.00', 'size': '300x250', 'adserverTargeting': {'hb_bidder': 'medianet', 'hb_adid': '3e6e4bce5c8fb3', 'hb_pb': '2.00', 'hb_size': '300x250', 'hb_source': 'client', 'hb_format': 'banner', 'prebid_test': 1}, 'status': 'rendered', 'params': [{'cid': 'test123', 'crid': '451466393'}]}, NO_BID: {'bidder': 'medianet', 'params': {'cid': 'test123', 'crid': '451466393', 'site': {}}, 'mediaTypes': {'banner': {'sizes': [[300, 250]], 'ext': ['asdads']}}, 'adUnitCode': 'div-gpt-ad-1460505748561-0', 'transactionId': '303fa0c6-682f-4aea-8e4a-dc68f0d5c7d5', 'sizes': [[300, 250], [300, 600]], 'bidId': '28248b0e6aece2', 'bidderRequestId': '13fccf3809fe43', 'auctionId': '8e0d5245-deb3-406c-96ca-9b609e077ff7', 'src': 'client'}, BID_TIMEOUT: [{'bidId': '28248b0e6aece2', 'bidder': 'medianet', 'adUnitCode': 'div-gpt-ad-1460505748561-0', 'auctionId': '8e0d5245-deb3-406c-96ca-9b609e077ff7', 'params': [{'cid': 'test123', 'crid': '451466393', 'site': {}}, {'cid': '8CUX0H51P', 'crid': '451466393', 'site': {}}], 'timeout': 6}] } function performAuctionWithFloorConfig() { - events.emit(AUCTION_INIT, MOCK.AUCTION_INIT_WITH_FLOOR); + events.emit(AUCTION_INIT, {...MOCK.AUCTION_INIT_WITH_FLOOR, adUnits: MOCK.Ad_Units}); events.emit(BID_REQUESTED, MOCK.BID_REQUESTED); events.emit(BID_RESPONSE, MOCK.BID_RESPONSE); events.emit(AUCTION_END, MOCK.AUCTION_END); @@ -31,7 +34,7 @@ function performAuctionWithFloorConfig() { } function performStandardAuctionWithWinner() { - events.emit(AUCTION_INIT, MOCK.AUCTION_INIT); + events.emit(AUCTION_INIT, {...MOCK.AUCTION_INIT, adUnits: MOCK.Ad_Units}); events.emit(BID_REQUESTED, MOCK.BID_REQUESTED); events.emit(BID_RESPONSE, MOCK.BID_RESPONSE); events.emit(AUCTION_END, MOCK.AUCTION_END); @@ -40,27 +43,27 @@ function performStandardAuctionWithWinner() { } function performMultiFormatAuctionWithNoBid() { - events.emit(AUCTION_INIT, MOCK.AUCTION_INIT); + events.emit(AUCTION_INIT, {...MOCK.AUCTION_INIT, adUnits: MOCK.MULTI_FORMAT_TWIN_AD_UNITS}); events.emit(BID_REQUESTED, MOCK.MULTI_FORMAT_BID_REQUESTED); - events.emit(BID_RESPONSE, MOCK.BID_RESPONSE); + events.emit(NO_BID, MOCK.NO_BID); events.emit(AUCTION_END, MOCK.AUCTION_END); - events.emit(SET_TARGETING, MOCK.SET_TARGETING); + events.emit(SET_TARGETING, MOCK.NO_BID_SET_TARGETING); } function performStandardAuctionWithNoBid() { - events.emit(AUCTION_INIT, MOCK.AUCTION_INIT); + events.emit(AUCTION_INIT, {...MOCK.AUCTION_INIT, adUnits: MOCK.Ad_Units}); events.emit(BID_REQUESTED, MOCK.BID_REQUESTED); events.emit(NO_BID, MOCK.NO_BID); events.emit(AUCTION_END, MOCK.AUCTION_END); - events.emit(SET_TARGETING, MOCK.SET_TARGETING); + events.emit(SET_TARGETING, MOCK.NO_BID_SET_TARGETING); } function performStandardAuctionWithTimeout() { - events.emit(AUCTION_INIT, MOCK.AUCTION_INIT); + events.emit(AUCTION_INIT, {...MOCK.AUCTION_INIT, adUnits: MOCK.Ad_Units}); events.emit(BID_REQUESTED, MOCK.BID_REQUESTED); events.emit(BID_TIMEOUT, MOCK.BID_TIMEOUT); events.emit(AUCTION_END, MOCK.AUCTION_END); - events.emit(SET_TARGETING, MOCK.SET_TARGETING); + events.emit(SET_TARGETING, MOCK.NO_BID_SET_TARGETING); } function getQueryData(url) { @@ -123,8 +126,10 @@ describe('Media.net Analytics Adapter', function() { cid: 'test123' } }); + medianetAnalytics.clearlogsQueue(); }); afterEach(function () { + medianetAnalytics.clearlogsQueue(); medianetAnalytics.disableAnalytics(); }); @@ -139,10 +144,9 @@ describe('Media.net Analytics Adapter', function() { performMultiFormatAuctionWithNoBid(); const noBidLog = medianetAnalytics.getlogsQueue().map((log) => getQueryData(log))[0]; medianetAnalytics.clearlogsQueue(); - - expect(noBidLog.szs).to.equal(encodeURIComponent('300x250|1x1|640x480')); + expect(noBidLog.mtype).to.have.ordered.members([encodeURIComponent('banner|native|video'), encodeURIComponent('banner|video|native')]); + expect(noBidLog.szs).to.have.ordered.members([encodeURIComponent('300x250|1x1|640x480'), encodeURIComponent('300x250|1x1|640x480')]); expect(noBidLog.vplcmtt).to.equal('instream'); - expect(noBidLog.sz2).to.equal(encodeURIComponent('300x250')); }); it('should have winner log in standard auction', function() { @@ -167,6 +171,7 @@ describe('Media.net Analytics Adapter', function() { src: 'client', size: '300x250', mtype: 'banner', + gdpr: '0', cid: 'test123', lper: '1', ogbdp: '1.1495', @@ -205,7 +210,7 @@ describe('Media.net Analytics Adapter', function() { expect(noBidLog.status).to.have.ordered.members(['1', '2']); expect(noBidLog.src).to.have.ordered.members(['client', 'client']); expect(noBidLog.curr).to.have.ordered.members(['', '']); - expect(noBidLog.mtype).to.have.ordered.members(['', '']); + expect(noBidLog.mtype).to.have.ordered.members(['banner', 'banner']); expect(noBidLog.ogbdp).to.have.ordered.members(['', '']); expect(noBidLog.mpvid).to.have.ordered.members(['', '']); expect(noBidLog.crid).to.have.ordered.members(['', '451466393']); @@ -223,7 +228,7 @@ describe('Media.net Analytics Adapter', function() { expect(timeoutLog.status).to.have.ordered.members(['1', '3']); expect(timeoutLog.src).to.have.ordered.members(['client', 'client']); expect(timeoutLog.curr).to.have.ordered.members(['', '']); - expect(timeoutLog.mtype).to.have.ordered.members(['', '']); + expect(timeoutLog.mtype).to.have.ordered.members(['banner', 'banner']); expect(timeoutLog.ogbdp).to.have.ordered.members(['', '']); expect(timeoutLog.mpvid).to.have.ordered.members(['', '']); expect(timeoutLog.crid).to.have.ordered.members(['', '451466393']); diff --git a/test/spec/modules/medianetBidAdapter_spec.js b/test/spec/modules/medianetBidAdapter_spec.js index 649929056fa..1eeb167601e 100644 --- a/test/spec/modules/medianetBidAdapter_spec.js +++ b/test/spec/modules/medianetBidAdapter_spec.js @@ -1,7 +1,9 @@ import {expect} from 'chai'; import {spec} from 'modules/medianetBidAdapter.js'; +import { makeSlot } from '../integration/faker/googletag.js'; import { config } from 'src/config.js'; +$$PREBID_GLOBAL$$.version = $$PREBID_GLOBAL$$.version || 'version'; let VALID_BID_REQUEST = [{ 'bidder': 'medianet', 'params': { @@ -950,6 +952,42 @@ let VALID_BID_REQUEST = [{ } } }, + SERVER_VIDEO_OUTSTREAM_RESPONSE_VALID_BID = { + body: { + 'id': 'd90ca32f-3877-424a-b2f2-6a68988df57a', + 'bidList': [{ + 'no_bid': false, + 'requestId': '27210feac00e96', + 'cpm': 12.00, + 'width': 640, + 'height': 480, + 'ttl': 180, + 'creativeId': '370637746', + 'netRevenue': true, + 'vastXml': '', + 'currency': 'USD', + 'dfp_id': 'video1', + 'mediaType': 'video', + 'vto': 5000, + 'mavtr': 10, + 'avp': true, + 'ap': true, + 'pl': true, + 'mt': true, + 'jslt': 3000, + 'context': 'outstream' + }], + 'ext': { + 'csUrl': [{ + 'type': 'image', + 'url': 'http://cs.media.net/cksync.php' + }, { + 'type': 'iframe', + 'url': 'http://contextual.media.net/checksync.php?&vsSync=1' + }] + } + } + }, SERVER_VALID_BIDS = [{ 'no_bid': false, 'requestId': '27210feac00e96', @@ -1306,6 +1344,30 @@ describe('Media.net bid adapter', function () { expect(data.imp[1].ext).to.not.have.ownPropertyDescriptor('viewability'); expect(data.imp[1].ext.visibility).to.equal(0); }); + it('slot visibility should be calculable even in case of adUnitPath', function () { + const code = '/19968336/header-bid-tag-0'; + const divId = 'div-gpt-ad-1460505748561-0'; + window.googletag.pubads().setSlots([makeSlot({ code, divId })]); + + let boundingRect = { + top: 1010, + left: 1010, + bottom: 1050, + right: 1050 + }; + documentStub.withArgs(divId).returns({ + getBoundingClientRect: () => boundingRect + }); + documentStub.withArgs('div-gpt-ad-1460505748561-123').returns({ + getBoundingClientRect: () => boundingRect + }); + + const bidRequest = [{...VALID_BID_REQUEST[0], adUnitCode: code}] + const bidReq = spec.buildRequests(bidRequest, VALID_AUCTIONDATA); + const data = JSON.parse(bidReq.data); + expect(data.imp[0].ext.visibility).to.equal(2); + expect(data.imp[0].ext.viewability).to.equal(0); + }); }); describe('getUserSyncs', function () { @@ -1379,4 +1441,9 @@ describe('Media.net bid adapter', function () { expect(response).to.deep.equal(undefined); }); }); + + it('context should be outstream', function () { + let bids = spec.interpretResponse(SERVER_VIDEO_OUTSTREAM_RESPONSE_VALID_BID, []); + expect(bids[0].context).to.equal('outstream'); + }); }); diff --git a/test/spec/modules/mediasquareBidAdapter_spec.js b/test/spec/modules/mediasquareBidAdapter_spec.js index 351fbb40228..3c18cfe0be7 100644 --- a/test/spec/modules/mediasquareBidAdapter_spec.js +++ b/test/spec/modules/mediasquareBidAdapter_spec.js @@ -24,6 +24,43 @@ describe('MediaSquare bid adapter tests', function () { code: 'publishername_atf_desktop_rg_pave' }, }]; + var VIDEO_PARAMS = [{ + adUnitCode: 'banner-div', + bidId: 'aaaa1234', + auctionId: 'bbbb1234', + transactionId: 'cccc1234', + mediaTypes: { + video: { + context: 'instream', + playerSize: [640, 480], + mimes: ['video/mp4'], + } + }, + bidder: 'mediasquare', + params: { + owner: 'test', + code: 'publishername_atf_desktop_rg_pave' + }, + }]; + var NATIVE_PARAMS = [{ + adUnitCode: 'banner-div', + bidId: 'aaaa1234', + auctionId: 'bbbb1234', + transactionId: 'cccc1234', + mediaTypes: { + native: { + title: { + required: true, + len: 80 + }, + } + }, + bidder: 'mediasquare', + params: { + owner: 'test', + code: 'publishername_atf_desktop_rg_pave' + }, + }]; var BID_RESPONSE = {'body': { 'responses': [{ @@ -53,7 +90,7 @@ describe('MediaSquare bid adapter tests', function () { canonicalUrl: 'https://www.prebid.org/the/link/to/the/page' }, uspConsent: '111222333', - userId: {'id5id': '1111'}, + userId: { 'id5id': { uid: '1111' } }, schain: { 'ver': '1.0', 'complete': 1, @@ -128,4 +165,23 @@ describe('MediaSquare bid adapter tests', function () { expect(syncs[0]).to.have.property('type').and.to.equal('image'); expect(syncs[0]).to.have.property('url').and.to.equal('http://www.cookie.sync.org/'); }); + it('Verifies native in bid response', function () { + const request = spec.buildRequests(NATIVE_PARAMS, DEFAULT_OPTIONS); + BID_RESPONSE.body.responses[0].native = {'title': 'native title'}; + const response = spec.interpretResponse(BID_RESPONSE, request); + expect(response).to.have.lengthOf(1); + const bid = response[0]; + expect(bid).to.have.property('native'); + delete BID_RESPONSE.body.responses[0].native; + }); + it('Verifies video in bid response', function () { + const request = spec.buildRequests(VIDEO_PARAMS, DEFAULT_OPTIONS); + BID_RESPONSE.body.responses[0].video = {'xml': 'my vast XML', 'url': 'my vast url'}; + const response = spec.interpretResponse(BID_RESPONSE, request); + expect(response).to.have.lengthOf(1); + const bid = response[0]; + expect(bid).to.have.property('vastXml'); + expect(bid).to.have.property('vastUrl'); + delete BID_RESPONSE.body.responses[0].video; + }); }); diff --git a/test/spec/modules/mobfoxpbBidAdapter_spec.js b/test/spec/modules/mobfoxpbBidAdapter_spec.js new file mode 100644 index 00000000000..a02d580ab88 --- /dev/null +++ b/test/spec/modules/mobfoxpbBidAdapter_spec.js @@ -0,0 +1,304 @@ +import {expect} from 'chai'; +import {spec} from '../../../modules/mobfoxpbBidAdapter.js'; +import { BANNER, VIDEO, NATIVE } from '../../../src/mediaTypes.js'; + +describe('MobfoxHBBidAdapter', function () { + const bid = { + bidId: '23fhj33i987f', + bidder: 'mobfoxpb', + mediaTypes: { + [BANNER]: { + sizes: [[300, 250]] + } + }, + params: { + placementId: 783, + traffic: BANNER + } + }; + + const bidderRequest = { + refererInfo: { + referer: 'test.com' + } + }; + + describe('isBidRequestValid', function () { + it('Should return true if there are bidId, params and key parameters present', function () { + expect(spec.isBidRequestValid(bid)).to.be.true; + }); + it('Should return false if at least one of parameters is not present', function () { + delete bid.params.placementId; + expect(spec.isBidRequestValid(bid)).to.be.false; + }); + }); + + describe('buildRequests', function () { + let serverRequest = spec.buildRequests([bid], bidderRequest); + it('Creates a ServerRequest object with method, URL and data', function () { + expect(serverRequest).to.exist; + expect(serverRequest.method).to.exist; + expect(serverRequest.url).to.exist; + expect(serverRequest.data).to.exist; + }); + it('Returns POST method', function () { + expect(serverRequest.method).to.equal('POST'); + }); + it('Returns valid URL', function () { + expect(serverRequest.url).to.equal('https://bes.mobfox.com/?c=o&m=multi'); + }); + it('Returns valid data if array of bids is valid', function () { + let data = serverRequest.data; + expect(data).to.be.an('object'); + expect(data).to.have.all.keys('deviceWidth', 'deviceHeight', 'language', 'secure', 'host', 'page', 'placements'); + expect(data.deviceWidth).to.be.a('number'); + expect(data.deviceHeight).to.be.a('number'); + expect(data.language).to.be.a('string'); + expect(data.secure).to.be.within(0, 1); + expect(data.host).to.be.a('string'); + expect(data.page).to.be.a('string'); + expect(data.gdpr).to.not.exist; + expect(data.ccpa).to.not.exist; + let placement = data['placements'][0]; + expect(placement).to.have.keys('placementId', 'bidId', 'traffic', 'sizes', 'schain'); + expect(placement.placementId).to.equal(783); + expect(placement.bidId).to.equal('23fhj33i987f'); + expect(placement.traffic).to.equal(BANNER); + expect(placement.schain).to.be.an('object'); + expect(placement.sizes).to.be.an('array'); + }); + + it('Returns valid data for mediatype video', function () { + const playerSize = [300, 300]; + bid.mediaTypes = {}; + bid.params.traffic = VIDEO; + bid.mediaTypes[VIDEO] = { + playerSize + }; + serverRequest = spec.buildRequests([bid], bidderRequest); + let data = serverRequest.data; + expect(data).to.be.an('object'); + let placement = data['placements'][0]; + expect(placement).to.be.an('object'); + expect(placement).to.have.keys('placementId', 'bidId', 'traffic', 'wPlayer', 'hPlayer', 'schain'); + expect(placement.traffic).to.equal(VIDEO); + expect(placement.wPlayer).to.equal(playerSize[0]); + expect(placement.hPlayer).to.equal(playerSize[1]); + }); + + it('Returns valid data for mediatype native', function () { + const native = { + title: { + required: true + }, + body: { + required: true + }, + icon: { + required: true, + size: [64, 64] + } + }; + + bid.mediaTypes = {}; + bid.params.traffic = NATIVE; + bid.mediaTypes[NATIVE] = native; + serverRequest = spec.buildRequests([bid], bidderRequest); + let data = serverRequest.data; + expect(data).to.be.an('object'); + let placement = data['placements'][0]; + expect(placement).to.be.an('object'); + expect(placement).to.have.keys('placementId', 'bidId', 'traffic', 'native', 'schain'); + expect(placement.traffic).to.equal(NATIVE); + expect(placement.native).to.equal(native); + }); + + it('Returns data with gdprConsent and without uspConsent', function () { + bidderRequest.gdprConsent = 'test'; + serverRequest = spec.buildRequests([bid], bidderRequest); + let data = serverRequest.data; + expect(data.gdpr).to.exist; + expect(data.gdpr).to.be.a('string'); + expect(data.gdpr).to.equal(bidderRequest.gdprConsent); + expect(data.ccpa).to.not.exist; + delete bidderRequest.gdprConsent; + }); + + it('Returns data with uspConsent and without gdprConsent', function () { + bidderRequest.uspConsent = 'test'; + serverRequest = spec.buildRequests([bid], bidderRequest); + let data = serverRequest.data; + expect(data.ccpa).to.exist; + expect(data.ccpa).to.be.a('string'); + expect(data.ccpa).to.equal(bidderRequest.uspConsent); + expect(data.gdpr).to.not.exist; + }); + + it('Returns empty data if no valid requests are passed', function () { + serverRequest = spec.buildRequests([]); + let data = serverRequest.data; + expect(data.placements).to.be.an('array').that.is.empty; + }); + }); + describe('interpretResponse', function () { + it('Should interpret banner response', function () { + const banner = { + body: [{ + mediaType: 'banner', + width: 300, + height: 250, + cpm: 0.4, + ad: 'Test', + requestId: '23fhj33i987f', + ttl: 120, + creativeId: '2', + netRevenue: true, + currency: 'USD', + dealId: '1' + }] + }; + let bannerResponses = spec.interpretResponse(banner); + expect(bannerResponses).to.be.an('array').that.is.not.empty; + let dataItem = bannerResponses[0]; + expect(dataItem).to.have.all.keys('requestId', 'cpm', 'width', 'height', 'ad', 'ttl', 'creativeId', + 'netRevenue', 'currency', 'dealId', 'mediaType'); + expect(dataItem.requestId).to.equal('23fhj33i987f'); + expect(dataItem.cpm).to.equal(0.4); + expect(dataItem.width).to.equal(300); + expect(dataItem.height).to.equal(250); + expect(dataItem.ad).to.equal('Test'); + expect(dataItem.ttl).to.equal(120); + expect(dataItem.creativeId).to.equal('2'); + expect(dataItem.netRevenue).to.be.true; + expect(dataItem.currency).to.equal('USD'); + }); + it('Should interpret video response', function () { + const video = { + body: [{ + vastUrl: 'test.com', + mediaType: 'video', + cpm: 0.5, + requestId: '23fhj33i987f', + ttl: 120, + creativeId: '2', + netRevenue: true, + currency: 'USD', + dealId: '1' + }] + }; + let videoResponses = spec.interpretResponse(video); + expect(videoResponses).to.be.an('array').that.is.not.empty; + + let dataItem = videoResponses[0]; + expect(dataItem).to.have.all.keys('requestId', 'cpm', 'vastUrl', 'ttl', 'creativeId', + 'netRevenue', 'currency', 'dealId', 'mediaType'); + expect(dataItem.requestId).to.equal('23fhj33i987f'); + expect(dataItem.cpm).to.equal(0.5); + expect(dataItem.vastUrl).to.equal('test.com'); + expect(dataItem.ttl).to.equal(120); + expect(dataItem.creativeId).to.equal('2'); + expect(dataItem.netRevenue).to.be.true; + expect(dataItem.currency).to.equal('USD'); + }); + it('Should interpret native response', function () { + const native = { + body: [{ + mediaType: 'native', + native: { + clickUrl: 'test.com', + title: 'Test', + image: 'test.com', + impressionTrackers: ['test.com'], + }, + ttl: 120, + cpm: 0.4, + requestId: '23fhj33i987f', + creativeId: '2', + netRevenue: true, + currency: 'USD', + }] + }; + let nativeResponses = spec.interpretResponse(native); + expect(nativeResponses).to.be.an('array').that.is.not.empty; + + let dataItem = nativeResponses[0]; + expect(dataItem).to.have.keys('requestId', 'cpm', 'ttl', 'creativeId', 'netRevenue', 'currency', 'mediaType', 'native'); + expect(dataItem.native).to.have.keys('clickUrl', 'impressionTrackers', 'title', 'image') + expect(dataItem.requestId).to.equal('23fhj33i987f'); + expect(dataItem.cpm).to.equal(0.4); + expect(dataItem.native.clickUrl).to.equal('test.com'); + expect(dataItem.native.title).to.equal('Test'); + expect(dataItem.native.image).to.equal('test.com'); + expect(dataItem.native.impressionTrackers).to.be.an('array').that.is.not.empty; + expect(dataItem.native.impressionTrackers[0]).to.equal('test.com'); + expect(dataItem.ttl).to.equal(120); + expect(dataItem.creativeId).to.equal('2'); + expect(dataItem.netRevenue).to.be.true; + expect(dataItem.currency).to.equal('USD'); + }); + it('Should return an empty array if invalid banner response is passed', function () { + const invBanner = { + body: [{ + width: 300, + cpm: 0.4, + ad: 'Test', + requestId: '23fhj33i987f', + ttl: 120, + creativeId: '2', + netRevenue: true, + currency: 'USD', + dealId: '1' + }] + }; + + let serverResponses = spec.interpretResponse(invBanner); + expect(serverResponses).to.be.an('array').that.is.empty; + }); + it('Should return an empty array if invalid video response is passed', function () { + const invVideo = { + body: [{ + mediaType: 'video', + cpm: 0.5, + requestId: '23fhj33i987f', + ttl: 120, + creativeId: '2', + netRevenue: true, + currency: 'USD', + dealId: '1' + }] + }; + let serverResponses = spec.interpretResponse(invVideo); + expect(serverResponses).to.be.an('array').that.is.empty; + }); + it('Should return an empty array if invalid native response is passed', function () { + const invNative = { + body: [{ + mediaType: 'native', + clickUrl: 'test.com', + title: 'Test', + impressionTrackers: ['test.com'], + ttl: 120, + requestId: '23fhj33i987f', + creativeId: '2', + netRevenue: true, + currency: 'USD', + }] + }; + let serverResponses = spec.interpretResponse(invNative); + expect(serverResponses).to.be.an('array').that.is.empty; + }); + it('Should return an empty array if invalid response is passed', function () { + const invalid = { + body: [{ + ttl: 120, + creativeId: '2', + netRevenue: true, + currency: 'USD', + dealId: '1' + }] + }; + let serverResponses = spec.interpretResponse(invalid); + expect(serverResponses).to.be.an('array').that.is.empty; + }); + }); +}); diff --git a/test/spec/modules/nobidBidAdapter_spec.js b/test/spec/modules/nobidBidAdapter_spec.js index 346356e7d5b..e67d3b41f1d 100644 --- a/test/spec/modules/nobidBidAdapter_spec.js +++ b/test/spec/modules/nobidBidAdapter_spec.js @@ -228,6 +228,75 @@ describe('Nobid Adapter', function () { }); }); + describe('buildRequestsEIDs', function () { + const SITE_ID = 2; + const REFERER = 'https://www.examplereferer.com'; + let bidRequests = [ + { + 'bidder': 'nobid', + 'params': { + 'siteId': SITE_ID + }, + 'adUnitCode': 'adunit-code', + 'sizes': [[300, 250]], + 'bidId': '30b31c1838de1e', + 'bidderRequestId': '22edbae2733bf6', + 'auctionId': '1d1a030790a475', + 'userIdAsEids': [ + { + 'source': 'criteo.com', + 'uids': [ + { + 'id': 'CRITEO_ID', + 'atype': 1 + } + ] + }, + { + 'source': 'id5-sync.com', + 'uids': [ + { + 'id': 'ID5_ID', + 'atype': 1 + } + ], + 'ext': { + 'linkType': 0 + } + }, + { + 'source': 'adserver.org', + 'uids': [ + { + 'id': 'TD_ID', + 'atype': 1, + 'ext': { + 'rtiPartner': 'TDID' + } + } + ] + } + ] + } + ]; + + let bidderRequest = { + refererInfo: {referer: REFERER} + } + + it('should criteo eid', function () { + const request = spec.buildRequests(bidRequests, bidderRequest); + const payload = JSON.parse(request.data); + expect(payload.sid).to.exist.and.to.equal(2); + expect(payload.eids[0].source).to.exist.and.to.equal('criteo.com'); + expect(payload.eids[0].uids[0].id).to.exist.and.to.equal('CRITEO_ID'); + expect(payload.eids[1].source).to.exist.and.to.equal('id5-sync.com'); + expect(payload.eids[1].uids[0].id).to.exist.and.to.equal('ID5_ID'); + expect(payload.eids[2].source).to.exist.and.to.equal('adserver.org'); + expect(payload.eids[2].uids[0].id).to.exist.and.to.equal('TD_ID'); + }); + }); + describe('buildRequests', function () { const SITE_ID = 2; const REFERER = 'https://www.examplereferer.com'; diff --git a/test/spec/modules/oneVideoBidAdapter_spec.js b/test/spec/modules/oneVideoBidAdapter_spec.js index ae29bcd48ec..83adf8a4b11 100644 --- a/test/spec/modules/oneVideoBidAdapter_spec.js +++ b/test/spec/modules/oneVideoBidAdapter_spec.js @@ -217,7 +217,7 @@ describe('OneVideoBidAdapter', function () { const placement = bidRequest.params.video.placement; const rewarded = bidRequest.params.video.rewarded; const inventoryid = bidRequest.params.video.inventoryid; - const VERSION = '3.0.4'; + const VERSION = '3.0.5'; expect(data.imp[0].video.w).to.equal(width); expect(data.imp[0].video.h).to.equal(height); expect(data.imp[0].bidfloor).to.equal(bidRequest.params.bidfloor); @@ -315,6 +315,64 @@ describe('OneVideoBidAdapter', function () { const schain = data.source.ext.schain; expect(schain.nodes[0].hp).to.equal(bidRequest.params.video.hp); }) + it('should not accept key values pairs if custom is Undefined ', function () { + bidRequest.params.video.custom = null; + const requests = spec.buildRequests([ bidRequest ], bidderRequest); + const data = requests[0].data; + expect(data.imp[0].ext.custom).to.be.undefined; + }); + it('should not accept key values pairs if custom is Array ', function () { + bidRequest.params.video.custom = []; + const requests = spec.buildRequests([ bidRequest ], bidderRequest); + const data = requests[0].data; + expect(data.imp[0].ext.custom).to.be.undefined; + }); + it('should not accept key values pairs if custom is Number ', function () { + bidRequest.params.video.custom = 123456; + const requests = spec.buildRequests([ bidRequest ], bidderRequest); + const data = requests[0].data; + expect(data.imp[0].ext.custom).to.be.undefined; + }); + it('should not accept key values pairs if custom is String ', function () { + bidRequest.params.video.custom = 'keyValuePairs'; + const requests = spec.buildRequests([ bidRequest ], bidderRequest); + const data = requests[0].data; + expect(data.imp[0].ext.custom).to.be.undefined; + }); + it('should not accept key values pairs if custom is Boolean ', function () { + bidRequest.params.video.custom = true; + const requests = spec.buildRequests([ bidRequest ], bidderRequest); + const data = requests[0].data; + expect(data.imp[0].ext.custom).to.be.undefined; + }); + it('should accept key values pairs if custom is Object ', function () { + bidRequest.params.video.custom = {}; + const requests = spec.buildRequests([ bidRequest ], bidderRequest); + const data = requests[0].data; + expect(data.imp[0].ext.custom).to.be.a('object'); + }); + it('should accept key values pairs if custom is Object ', function () { + bidRequest.params.video.custom = { + key1: 'value1', + key2: 'value2', + key3: 4444444, + key4: false, + key5: {nested: 'object'}, + key6: ['string', 2, true, null], + key7: null, + key8: undefined + }; + const requests = spec.buildRequests([ bidRequest ], bidderRequest); + const custom = requests[0].data.imp[0].ext.custom; + expect(custom['key1']).to.be.a('string'); + expect(custom['key2']).to.be.a('string'); + expect(custom['key3']).to.be.a('number'); + expect(custom['key4']).to.not.exist; + expect(custom['key5']).to.not.exist; + expect(custom['key6']).to.not.exist; + expect(custom['key7']).to.not.exist; + expect(custom['key8']).to.not.exist; + }); }); describe('spec.interpretResponse', function () { @@ -349,7 +407,7 @@ describe('OneVideoBidAdapter', function () { height: 480, mediaType: 'video', currency: 'USD', - ttl: 100, + ttl: 300, netRevenue: true, adUnitCode: bidRequest.adUnitCode, renderer: (bidRequest.mediaTypes.video.context === 'outstream') ? newRenderer(bidRequest, bidResponse) : undefined, @@ -376,6 +434,30 @@ describe('OneVideoBidAdapter', function () { expect(bidResponse.mediaType).to.equal('banner'); expect(bidResponse.renderer).to.be.undefined; }); + + it('should default ttl to 300', function () { + const serverResponse = {seatbid: [{bid: [{id: 1, adid: 123, crid: 2, price: 6.01, adm: ''}]}], cur: 'USD'}; + const bidResponse = spec.interpretResponse({ body: serverResponse }, { bidRequest }); + expect(bidResponse.ttl).to.equal(300); + }); + it('should not allow ttl above 3601, default to 300', function () { + bidRequest.params.video.ttl = 3601; + const serverResponse = {seatbid: [{bid: [{id: 1, adid: 123, crid: 2, price: 6.01, adm: ''}]}], cur: 'USD'}; + const bidResponse = spec.interpretResponse({ body: serverResponse }, { bidRequest }); + expect(bidResponse.ttl).to.equal(300); + }); + it('should not allow ttl below 1, default to 300', function () { + bidRequest.params.video.ttl = 0; + const serverResponse = {seatbid: [{bid: [{id: 1, adid: 123, crid: 2, price: 6.01, adm: ''}]}], cur: 'USD'}; + const bidResponse = spec.interpretResponse({ body: serverResponse }, { bidRequest }); + expect(bidResponse.ttl).to.equal(300); + }); + it('should use custom ttl if under 3600', function () { + bidRequest.params.video.ttl = 1000; + const serverResponse = {seatbid: [{bid: [{id: 1, adid: 123, crid: 2, price: 6.01, adm: ''}]}], cur: 'USD'}; + const bidResponse = spec.interpretResponse({ body: serverResponse }, { bidRequest }); + expect(bidResponse.ttl).to.equal(1000); + }); }); describe('when GDPR and uspConsent applies', function () { diff --git a/test/spec/modules/ooloAnalyticsAdapter_spec.js b/test/spec/modules/ooloAnalyticsAdapter_spec.js new file mode 100644 index 00000000000..f82f7856fb2 --- /dev/null +++ b/test/spec/modules/ooloAnalyticsAdapter_spec.js @@ -0,0 +1,793 @@ +import ooloAnalytics, { PAGEVIEW_ID } from 'modules/ooloAnalyticsAdapter.js'; +import {expect} from 'chai'; +import {server} from 'test/mocks/xhr.js'; +import constants from 'src/constants.json' +import events from 'src/events' +import { config } from 'src/config'; +import { buildAuctionData, generatePageViewId } from 'modules/ooloAnalyticsAdapter'; + +const auctionId = '0ea14159-2058-4b87-a966-9d7652176a56'; +const auctionStart = 1598513385415 +const timeout = 3000 +const adUnit1 = 'top_1'; +const adUnit2 = 'top_2'; +const bidId1 = '392b5a6b05d648' +const bidId2 = '392b5a6b05d649' +const bidId3 = '392b5a6b05d650' + +const auctionInit = { + timestamp: auctionStart, + auctionId: auctionId, + timeout: timeout, + auctionStart, + adUnits: [{ + code: adUnit1, + transactionId: 'abalksdkjfh-12sade' + }, { + code: adUnit2, + transactionId: 'abalksdkjfh-12sadf' + }] +}; + +const bidRequested = { + auctionId, + bidderCode: 'appnexus', + bidderRequestId: '2946b569352ef2', + start: 1598513405254, + bids: [ + { + auctionId, + bidId: bidId1, + bidderRequestId: '2946b569352ef2', + bidder: 'appnexus', + adUnitCode: adUnit1, + sizes: [[728, 90], [970, 90]], + mediaTypes: { + banner: { + sizes: [[728, 90], [970, 90]], + }, + }, + params: { + placementId: '4799418', + }, + schain: {} + }, + { + auctionId, + bidId: bidId2, + bidderRequestId: '2946b569352ef3', + bidder: 'rubicon', + adUnitCode: adUnit2, + sizes: [[728, 90], [970, 90]], + mediaTypes: { + banner: { + sizes: [[728, 90], [970, 90]], + }, + }, + params: { + placementId: '4799418', + }, + schain: {} + }, + { + auctionId, + bidId: bidId3, + bidderRequestId: '2946b569352ef4', + bidder: 'ix', + adUnitCode: adUnit2, + sizes: [[728, 90], [970, 90]], + mediaTypes: { + banner: { + sizes: [[728, 90], [970, 90]], + }, + }, + params: { + placementId: '4799418', + }, + schain: {} + }, + ], +} + +const noBid = { + auctionId, + adUnitCode: adUnit2, + bidId: bidId2, + requestId: bidId2 +} + +const bidResponse = { + auctionId, + bidderCode: 'appnexus', + mediaType: 'banner', + width: 0, + height: 0, + statusMessage: 'Bid available', + adId: '222bb26f9e8bd', + cpm: 0.112256, + responseTimestamp: 1598513485254, + requestTimestamp: 1462919238936, + bidder: 'appnexus', + adUnitCode: adUnit1, + timeToRespond: 401, + pbLg: '0.00', + pbMg: '0.10', + pbHg: '0.11', + pbAg: '0.10', + size: '0x0', + requestId: bidId1, + creativeId: '123456', + adserverTargeting: { + hb_bidder: 'appnexus', + hb_adid: '222bb26f9e8bd', + hb_pb: '10.00', + hb_size: '0x0', + foobar: '0x0', + }, + netRevenue: true, + currency: 'USD', + ttl: 300, +} + +const auctionEnd = { + auctionId: auctionId +}; + +const bidTimeout = [ + { + adUnitCode: adUnit2, + auctionId: auctionId, + bidId: bidId3, + bidder: 'ix', + timeout: timeout + } +]; + +const bidWon = { + auctionId, + adUnitCode: adUnit1, + bidId: bidId1, + cpm: 0.5 +} + +function simulateAuction () { + events.emit(constants.EVENTS.AUCTION_INIT, auctionInit); + events.emit(constants.EVENTS.BID_REQUESTED, bidRequested); + events.emit(constants.EVENTS.BID_RESPONSE, bidResponse); + events.emit(constants.EVENTS.NO_BID, noBid); + events.emit(constants.EVENTS.BID_TIMEOUT, bidTimeout); + events.emit(constants.EVENTS.AUCTION_END, auctionEnd); +} + +describe('oolo Prebid Analytic', () => { + let clock + + beforeEach(() => { + sinon.stub(events, 'getEvents').returns([]); + clock = sinon.useFakeTimers() + }); + + afterEach(() => { + ooloAnalytics.disableAnalytics(); + events.getEvents.restore() + clock.restore(); + }) + + describe('enableAnalytics init options', () => { + it('should not enable analytics if invalid config', () => { + ooloAnalytics.enableAnalytics({ + provider: 'oolo', + options: { + pid: undefined + } + }) + + expect(server.requests).to.have.length(0) + }) + + it('should send prebid config to the server', () => { + ooloAnalytics.enableAnalytics({ + provider: 'oolo', + options: { + pid: 123 + } + }) + + const conf = {} + const pbjsConfig = config.getConfig() + + Object.keys(pbjsConfig).forEach(key => { + if (key[0] !== '_') { + conf[key] = pbjsConfig[key] + } + }) + + expect(server.requests[1].url).to.contain('/hbconf') + expect(JSON.parse(server.requests[1].requestBody)).to.deep.equal(conf) + }) + + it('should request server config and send page data', () => { + ooloAnalytics.enableAnalytics({ + provider: 'oolo', + options: { + pid: 123 + } + }) + clock.next() + + expect(server.requests[0].url).to.contain('?pid=123') + + const pageData = JSON.parse(server.requests[2].requestBody) + + expect(pageData).to.have.property('timestamp') + expect(pageData).to.have.property('screenWidth') + expect(pageData).to.have.property('screenHeight') + expect(pageData).to.have.property('url') + expect(pageData).to.have.property('protocol') + expect(pageData).to.have.property('origin') + expect(pageData).to.have.property('referrer') + expect(pageData).to.have.property('pbVersion') + expect(pageData).to.have.property('pvid') + expect(pageData).to.have.property('pid') + expect(pageData).to.have.property('pbModuleVersion') + expect(pageData).to.have.property('domContentLoadTime') + expect(pageData).to.have.property('pageLoadTime') + }) + }) + + describe('data handling and sending events', () => { + it('should send an "auction" event to the server', () => { + ooloAnalytics.enableAnalytics({ + provider: 'oolo', + options: { + pid: 123 + } + }) + clock.next() + + server.requests[0].respond(500) + + simulateAuction() + + expect(server.requests.length).to.equal(3) + clock.tick(2000) + expect(server.requests.length).to.equal(4) + + const request = JSON.parse(server.requests[3].requestBody); + + expect(request).to.include({ + eventType: 'auction', + pid: 123, + auctionId, + auctionStart, + auctionEnd: 0, + timeout, + }) + expect(request.pvid).to.be.a('number') + expect(request.adUnits).to.have.length(2) + + const auctionAdUnit1 = request.adUnits.filter(adUnit => adUnit.adunid === adUnit1)[0] + const auctionAdUnit2 = request.adUnits.filter(adUnit => adUnit.adunid === adUnit2)[0] + + expect(auctionAdUnit1.auctionId).to.equal(auctionId) + expect(auctionAdUnit1.bids).to.be.an('array') + expect(auctionAdUnit1.bids).to.have.length(1) + + // bid response + expect(auctionAdUnit1.bids[0].bidId).to.equal(bidId1) + expect(auctionAdUnit1.bids[0].bst).to.equal('bidReceived') + expect(auctionAdUnit1.bids[0].bidder).to.equal('appnexus') + expect(auctionAdUnit1.bids[0].cpm).to.equal(0.112256) + expect(auctionAdUnit1.bids[0].cur).to.equal('USD') + expect(auctionAdUnit1.bids[0].s).to.equal(bidRequested.start) + expect(auctionAdUnit1.bids[0].e).to.equal(bidResponse.responseTimestamp) + expect(auctionAdUnit1.bids[0].rs).to.equal(bidRequested.start - auctionStart) + expect(auctionAdUnit1.bids[0].re).to.equal(bidResponse.responseTimestamp - auctionStart) + expect(auctionAdUnit1.bids[0].h).to.equal(0) + expect(auctionAdUnit1.bids[0].w).to.equal(0) + expect(auctionAdUnit1.bids[0].mt).to.equal('banner') + expect(auctionAdUnit1.bids[0].nrv).to.equal(true) + expect(auctionAdUnit1.bids[0].params).to.have.keys('placementId') + expect(auctionAdUnit1.bids[0].size).to.equal('0x0') + expect(auctionAdUnit1.bids[0].crId).to.equal('123456') + expect(auctionAdUnit1.bids[0].ttl).to.equal(bidResponse.ttl) + expect(auctionAdUnit1.bids[0].ttr).to.equal(bidResponse.timeToRespond) + + expect(auctionAdUnit2.auctionId).to.equal(auctionId) + expect(auctionAdUnit2.bids).to.be.an('array') + expect(auctionAdUnit2.bids).to.have.length(2) + + // no bid + expect(auctionAdUnit2.bids[0].bidId).to.equal(bidId2) + expect(auctionAdUnit2.bids[0].bst).to.equal('noBid') + expect(auctionAdUnit2.bids[0].bidder).to.equal('rubicon') + expect(auctionAdUnit2.bids[0].s).to.be.a('number') + expect(auctionAdUnit2.bids[0].e).to.be.a('number') + expect(auctionAdUnit2.bids[0].params).to.have.keys('placementId') + + // timeout + expect(auctionAdUnit2.bids[1].bidId).to.equal(bidId3) + expect(auctionAdUnit2.bids[1].bst).to.equal('bidTimedOut') + expect(auctionAdUnit2.bids[1].bidder).to.equal('ix') + expect(auctionAdUnit2.bids[1].s).to.be.a('number') + expect(auctionAdUnit2.bids[1].e).to.be.a('undefined') + }) + + it('should push events to a queue and process them once server configuration returns', () => { + ooloAnalytics.enableAnalytics({ + provider: 'oolo', + options: { + pid: 123 + } + }) + + events.emit(constants.EVENTS.AUCTION_INIT, auctionInit); + events.emit(constants.EVENTS.BID_REQUESTED, bidRequested); + events.emit(constants.EVENTS.BID_RESPONSE, bidResponse); + + // configuration returned in an arbitrary moment + server.requests[0].respond(500) + + events.emit(constants.EVENTS.NO_BID, noBid); + events.emit(constants.EVENTS.BID_TIMEOUT, bidTimeout); + events.emit(constants.EVENTS.AUCTION_END, auctionEnd); + + clock.tick(1500) + + const request = JSON.parse(server.requests[3].requestBody); + + expect(request).to.include({ + eventType: 'auction', + pid: 123, + auctionId, + auctionStart, + auctionEnd: 0, + timeout, + }) + expect(request.pvid).to.be.a('number') + expect(request.adUnits).to.have.length(2) + + const auctionAdUnit1 = request.adUnits.filter(adUnit => adUnit.adunid === adUnit1)[0] + const auctionAdUnit2 = request.adUnits.filter(adUnit => adUnit.adunid === adUnit2)[0] + + expect(auctionAdUnit1.auctionId).to.equal(auctionId) + expect(auctionAdUnit1.bids).to.be.an('array') + expect(auctionAdUnit1.bids).to.have.length(1) + + // bid response + expect(auctionAdUnit1.bids[0].bidId).to.equal(bidId1) + expect(auctionAdUnit1.bids[0].bst).to.equal('bidReceived') + expect(auctionAdUnit1.bids[0].bidder).to.equal('appnexus') + expect(auctionAdUnit1.bids[0].cpm).to.equal(0.112256) + expect(auctionAdUnit1.bids[0].cur).to.equal('USD') + expect(auctionAdUnit1.bids[0].s).to.equal(bidRequested.start) + expect(auctionAdUnit1.bids[0].e).to.equal(bidResponse.responseTimestamp) + expect(auctionAdUnit1.bids[0].rs).to.equal(bidRequested.start - auctionStart) + expect(auctionAdUnit1.bids[0].re).to.equal(bidResponse.responseTimestamp - auctionStart) + expect(auctionAdUnit1.bids[0].h).to.equal(0) + expect(auctionAdUnit1.bids[0].w).to.equal(0) + expect(auctionAdUnit1.bids[0].mt).to.equal('banner') + expect(auctionAdUnit1.bids[0].nrv).to.equal(true) + expect(auctionAdUnit1.bids[0].params).to.have.keys('placementId') + expect(auctionAdUnit1.bids[0].size).to.equal('0x0') + expect(auctionAdUnit1.bids[0].crId).to.equal('123456') + expect(auctionAdUnit1.bids[0].ttl).to.equal(bidResponse.ttl) + expect(auctionAdUnit1.bids[0].ttr).to.equal(bidResponse.timeToRespond) + + expect(auctionAdUnit2.auctionId).to.equal(auctionId) + expect(auctionAdUnit2.bids).to.be.an('array') + expect(auctionAdUnit2.bids).to.have.length(2) + + // no bid + expect(auctionAdUnit2.bids[0].bidId).to.equal(bidId2) + expect(auctionAdUnit2.bids[0].bst).to.equal('noBid') + expect(auctionAdUnit2.bids[0].bidder).to.equal('rubicon') + expect(auctionAdUnit2.bids[0].s).to.be.a('number') + expect(auctionAdUnit2.bids[0].e).to.be.a('number') + expect(auctionAdUnit2.bids[0].params).to.have.keys('placementId') + + // timeout + expect(auctionAdUnit2.bids[1].bidId).to.equal(bidId3) + expect(auctionAdUnit2.bids[1].bst).to.equal('bidTimedOut') + expect(auctionAdUnit2.bids[1].bidder).to.equal('ix') + expect(auctionAdUnit2.bids[1].s).to.be.a('number') + expect(auctionAdUnit2.bids[1].e).to.be.a('undefined') + }) + + it('should send "auction" event without all the fields that were set to undefined', () => { + ooloAnalytics.enableAnalytics({ + provider: 'oolo', + options: { + pid: 123 + } + }) + + server.requests[0].respond(500) + + simulateAuction() + clock.tick(1500) + + const request = JSON.parse(server.requests[3].requestBody); + + const auctionAdUnit1 = request.adUnits.filter(adUnit => adUnit.adunid === adUnit1)[0] + const auctionAdUnit2 = request.adUnits.filter(adUnit => adUnit.adunid === adUnit2)[0] + + expect(auctionAdUnit1.code).to.equal(undefined) + expect(auctionAdUnit1.transactionId).to.equal(undefined) + expect(auctionAdUnit1.adUnitCode).to.equal(undefined) + expect(auctionAdUnit1.bids[0].auctionStart).to.equal(undefined) + expect(auctionAdUnit1.bids[0].bids).to.equal(undefined) + expect(auctionAdUnit1.bids[0].refererInfo).to.equal(undefined) + expect(auctionAdUnit1.bids[0].bidRequestsCount).to.equal(undefined) + expect(auctionAdUnit1.bids[0].bidderRequestId).to.equal(undefined) + expect(auctionAdUnit1.bids[0].bidderRequestsCount).to.equal(undefined) + expect(auctionAdUnit1.bids[0].bidderWinsCount).to.equal(undefined) + expect(auctionAdUnit1.bids[0].schain).to.equal(undefined) + expect(auctionAdUnit1.bids[0].src).to.equal(undefined) + expect(auctionAdUnit1.bids[0].transactionId).to.equal(undefined) + + // no bid + expect(auctionAdUnit2.bids[0].schain).to.equal(undefined) + expect(auctionAdUnit2.bids[0].src).to.equal(undefined) + expect(auctionAdUnit2.bids[0].transactionId).to.equal(undefined) + }) + + it('should mark bid winner and send to the server along with the auction data', () => { + ooloAnalytics.enableAnalytics({ + provider: 'oolo', + options: { + pid: 123 + } + }) + + server.requests[0].respond(500) + simulateAuction() + events.emit(constants.EVENTS.BID_WON, bidWon); + clock.tick(1500) + + // no bidWon + expect(server.requests).to.have.length(4) + + const request = JSON.parse(server.requests[3].requestBody); + expect(request.adUnits[0].bids[0].isW).to.equal(true) + }) + + it('should take BID_WON_TIMEOUT from server config if exists', () => { + ooloAnalytics.enableAnalytics({ + provider: 'oolo', + options: { + pid: 123 + } + }) + + server.requests[0].respond(200, {}, JSON.stringify({ + 'events': {}, + 'BID_WON_TIMEOUT': 500 + })) + + simulateAuction() + events.emit(constants.EVENTS.BID_WON, bidWon); + clock.tick(499) + + // no auction data + expect(server.requests).to.have.length(3) + + clock.tick(1) + + // auction data + expect(server.requests).to.have.length(4) + const request = JSON.parse(server.requests[3].requestBody); + expect(request.adUnits[0].bids[0].isW).to.equal(true) + }) + + it('should send a "bidWon" event to the server', () => { + ooloAnalytics.enableAnalytics({ + provider: 'oolo', + options: { + pid: 123 + } + }) + + server.requests[0].respond(500) + simulateAuction() + clock.tick(1500) + events.emit(constants.EVENTS.BID_WON, bidWon); + + expect(server.requests).to.have.length(5) + + const request = JSON.parse(server.requests[4].requestBody); + + expect(request.eventType).to.equal('bidWon') + expect(request.auctionId).to.equal(auctionId) + expect(request.adunid).to.equal(adUnit1) + expect(request.bid.bst).to.equal('bidWon') + expect(request.bid.cur).to.equal('USD') + expect(request.bid.cpm).to.equal(0.5) + }) + + it('should sent adRenderFailed to the server', () => { + ooloAnalytics.enableAnalytics({ + provider: 'oolo', + options: { + pid: 123 + } + }) + + server.requests[0].respond(500) + simulateAuction() + clock.tick(1500) + events.emit(constants.EVENTS.AD_RENDER_FAILED, { bidId: 'abcdef', reason: 'exception' }); + + expect(server.requests).to.have.length(5) + + const request = JSON.parse(server.requests[4].requestBody); + + expect(request.eventType).to.equal('adRenderFailed') + expect(request.pvid).to.equal(PAGEVIEW_ID) + expect(request.bidId).to.equal('abcdef') + expect(request.reason).to.equal('exception') + }) + + it('should pick fields according to server configuration', () => { + ooloAnalytics.enableAnalytics({ + provider: 'oolo', + options: { + pid: 123 + } + }) + + server.requests[0].respond(200, {}, JSON.stringify({ + 'events': { + 'bidRequested': { + 'sendRaw': 0, + 'pickFields': ['transactionId'] + }, + 'noBid': { + 'sendRaw': 0, + 'pickFields': ['src'] + }, + 'bidResponse': { + 'sendRaw': 0, + 'pickFields': ['adUrl', 'statusMessage'] + }, + 'auctionEnd': { + 'sendRaw': 0, + 'pickFields': ['winningBids'] + }, + } + })) + + events.emit(constants.EVENTS.AUCTION_INIT, { ...auctionInit }); + events.emit(constants.EVENTS.BID_REQUESTED, { ...bidRequested, bids: bidRequested.bids.map(b => { b.transactionId = '123'; return b }) }); + events.emit(constants.EVENTS.NO_BID, { ...noBid, src: 'client' }); + events.emit(constants.EVENTS.BID_RESPONSE, { ...bidResponse, adUrl: '...' }); + events.emit(constants.EVENTS.AUCTION_END, { ...auctionEnd, winningBids: [] }); + events.emit(constants.EVENTS.BID_WON, { ...bidWon, statusMessage: 'msg2' }); + + clock.tick(1500) + + const request = JSON.parse(server.requests[3].requestBody) + + expect(request.adUnits[0].bids[0].transactionId).to.equal('123') + expect(request.adUnits[0].bids[0].adUrl).to.equal('...') + expect(request.adUnits[0].bids[0].statusMessage).to.equal('msg2') + expect(request.adUnits[1].bids[0].src).to.equal('client') + }) + + it('should omit fields according to server configuration', () => { + ooloAnalytics.enableAnalytics({ + provider: 'oolo', + options: { + pid: 123 + } + }) + clock.next() + + server.requests[0].respond(200, {}, JSON.stringify({ + 'events': { + 'auctionInit': { + 'sendRaw': 0, + 'omitFields': ['custom_1'] + }, + 'bidResponse': { + 'sendRaw': 0, + 'omitFields': ['custom_2', 'custom_4', 'custom_5'] + } + } + })) + + events.emit(constants.EVENTS.AUCTION_INIT, { ...auctionInit, custom_1: true }); + events.emit(constants.EVENTS.BID_REQUESTED, { ...bidRequested, bids: bidRequested.bids.map(b => { b.custom_2 = true; return b }) }); + events.emit(constants.EVENTS.NO_BID, { ...noBid, custom_3: true }); + events.emit(constants.EVENTS.BID_RESPONSE, { ...bidResponse, custom_4: true }); + events.emit(constants.EVENTS.AUCTION_END, { ...auctionEnd }); + events.emit(constants.EVENTS.BID_WON, { ...bidWon, custom_5: true }); + + clock.tick(1500) + + const request = JSON.parse(server.requests[3].requestBody) + + expect(request.custom_1).to.equal(undefined) + expect(request.custom_6).to.equal(undefined) + expect(request.adUnits[0].bids[0].custom_2).to.equal(undefined) + expect(request.adUnits[0].bids[0].custom_4).to.equal(undefined) + expect(request.adUnits[0].bids[0].custom_5).to.equal(undefined) + expect(request.adUnits[0].bids[0].custom_7).to.equal(undefined) + }) + + it('should omit fields from raw data according to server configuration', () => { + ooloAnalytics.enableAnalytics({ + provider: 'oolo', + options: { + pid: 123 + } + }) + clock.next() + + server.requests[0].respond(200, {}, JSON.stringify({ + 'events': { + 'auctionInit': { + 'sendRaw': 1, + 'omitRawFields': ['custom_1'] + }, + } + })) + + events.emit(constants.EVENTS.AUCTION_INIT, { ...auctionInit, custom_1: true }); + + clock.tick(1500) + + const request = JSON.parse(server.requests[3].requestBody) + + expect(request.eventType).to.equal('auctionInit') + expect(request.custom_1).to.equal(undefined) + }) + + it('should send raw data to custom endpoint if exists in server configuration', () => { + ooloAnalytics.enableAnalytics({ + provider: 'oolo', + options: { + pid: 123 + } + }) + clock.next() + + server.requests[0].respond(200, {}, JSON.stringify({ + 'events': { + 'auctionInit': { + 'sendRaw': 1, + 'endpoint': 'https://pbjs.com' + }, + } + })) + + events.emit(constants.EVENTS.AUCTION_INIT, { ...auctionInit }); + + expect(server.requests[3].url).to.equal('https://pbjs.com') + }) + + it('should send raw events based on server configuration', () => { + ooloAnalytics.enableAnalytics({ + provider: 'oolo', + options: { + pid: 123 + } + }) + clock.next() + + server.requests[0].respond(200, {}, JSON.stringify({ + 'events': { + 'auctionInit': { + 'sendRaw': 0, + }, + 'bidRequested': { + 'sendRaw': 1, + } + } + })) + + events.emit(constants.EVENTS.AUCTION_INIT, auctionInit) + events.emit(constants.EVENTS.BID_REQUESTED, bidRequested); + + const request = JSON.parse(server.requests[3].requestBody) + + expect(request).to.deep.equal({ + eventType: 'bidRequested', + pid: 123, + pvid: PAGEVIEW_ID, + pbModuleVersion: '1.0.0', + ...bidRequested + }) + }) + + it('should queue events and raw events until server configuration resolves', () => { + ooloAnalytics.enableAnalytics({ + provider: 'oolo', + options: { + pid: 123 + } + }) + + simulateAuction() + clock.tick(1500) + + expect(server.requests).to.have.length(3) + + server.requests[0].respond(200, {}, JSON.stringify({ + 'events': { + 'auctionInit': { + 'sendRaw': 1, + }, + 'bidRequested': { + 'sendRaw': 1, + } + } + })) + + expect(server.requests).to.have.length(5) + expect(JSON.parse(server.requests[3].requestBody).eventType).to.equal('auctionInit') + expect(JSON.parse(server.requests[4].requestBody).eventType).to.equal('bidRequested') + }) + }); + + describe('buildAuctionData', () => { + let auction = { + auctionId, + auctionStart, + auctionEnd, + adUnitCodes: ['mid_1'], + auctionStatus: 'running', + bidderRequests: [], + bidsReceived: [], + noBids: [], + winningBids: [], + timestamp: 1234567, + config: {}, + adUnits: { + mid_1: { + adUnitCode: 'mid_1', + code: 'mid_1', + transactionId: '123dsafasdf', + bids: { + [bidId1]: { + adUnitCode: 'mid_1', + cpm: 0.5 + } + } + } + } + } + + it('should turn adUnits and bids objects into arrays', () => { + const auctionData = buildAuctionData(auction, []) + + expect(auctionData.adUnits).to.be.an('array') + expect(auctionData.adUnits[0].bids).to.be.an('array') + }) + + it('should remove fields from the auction', () => { + const auctionData = buildAuctionData(auction, []) + const auctionFields = Object.keys(auctionData) + const adUnitFields = Object.keys(auctionData.adUnits[0]) + + expect(auctionFields).not.to.contain('adUnitCodes') + expect(auctionFields).not.to.contain('auctionStatus') + expect(auctionFields).not.to.contain('bidderRequests') + expect(auctionFields).not.to.contain('bidsReceived') + expect(auctionFields).not.to.contain('noBids') + expect(auctionFields).not.to.contain('winningBids') + expect(auctionFields).not.to.contain('timestamp') + expect(auctionFields).not.to.contain('config') + + expect(adUnitFields).not.to.contain('adUnitCoe') + expect(adUnitFields).not.to.contain('code') + expect(adUnitFields).not.to.contain('transactionId') + }) + }) + + describe('generatePageViewId', () => { + it('should generate a 19 digits number', () => { + expect(generatePageViewId()).length(19) + }) + }) +}); diff --git a/test/spec/modules/openxBidAdapter_spec.js b/test/spec/modules/openxBidAdapter_spec.js index 0808d78c0aa..121f8e76a07 100644 --- a/test/spec/modules/openxBidAdapter_spec.js +++ b/test/spec/modules/openxBidAdapter_spec.js @@ -1043,7 +1043,7 @@ describe('OpenxAdapter', function () { britepoolid: '1111-britepoolid', criteoId: '1111-criteoId', digitrustid: {data: {id: 'DTID', keyv: 4, privacy: {optout: false}, producer: 'ABC', version: 2}}, - id5id: '1111-id5id', + id5id: {uid: '1111-id5id'}, idl_env: '1111-idl_env', lipb: {lipbid: '1111-lipb'}, netId: 'fH5A3n2O8_CZZyPoJVD-eabc6ECb7jhxCicsds7qSg', @@ -1096,6 +1096,9 @@ describe('OpenxAdapter', function () { case 'parrableId': userIdValue = EXAMPLE_DATA_BY_ATTR.parrableId.eid; break; + case 'id5id': + userIdValue = EXAMPLE_DATA_BY_ATTR.id5id.uid; + break; default: userIdValue = EXAMPLE_DATA_BY_ATTR[userIdProviderKey]; } diff --git a/test/spec/modules/ozoneBidAdapter_spec.js b/test/spec/modules/ozoneBidAdapter_spec.js index f20e75dfedd..c1022608b4a 100644 --- a/test/spec/modules/ozoneBidAdapter_spec.js +++ b/test/spec/modules/ozoneBidAdapter_spec.js @@ -66,7 +66,7 @@ var validBidRequestsWithUserIdData = [ params: { publisherId: '9876abcd12-3', customData: [{'settings': {}, 'targeting': {'gender': 'bart', 'age': 'low'}}], lotameData: {'Profile': {'tpid': 'c8ef27a0d4ba771a81159f0d2e792db4', 'Audiences': {'Audience': [{'id': '99999', 'abbr': 'sports'}, {'id': '88888', 'abbr': 'movie'}, {'id': '77777', 'abbr': 'blogger'}]}}}, placementId: '1310000099', siteId: '1234567890', id: 'fea37168-78f1-4a23-a40e-88437a99377e', auctionId: '27dcb421-95c6-4024-a624-3c03816c5f99', imp: [ { id: '2899ec066a91ff8', tagid: 'undefined', secure: 1, banner: { format: [{ w: 300, h: 250 }, { w: 300, h: 600 }], h: 250, topframe: 1, w: 300 } } ] }, sizes: [[300, 250], [300, 600]], transactionId: '2e63c0ed-b10c-4008-aed5-84582cecfe87', - userId: {'pubcid': '12345678', 'id5id': 'ID5-someId', 'criteortus': {'ozone': {'userid': 'critId123'}}, 'idl_env': 'liverampId', 'lipb': {'lipbid': 'lipbidId123'}, 'parrableId': {eid: 'parrableid123'}} + userId: {'pubcid': '12345678', 'id5id': { 'uid': 'ID5-someId' }, 'criteortus': {'ozone': {'userid': 'critId123'}}, 'idl_env': 'liverampId', 'lipb': {'lipbid': 'lipbidId123'}, 'parrableId': {eid: 'parrableid123'}} } ]; var validBidRequestsMinimal = [ @@ -297,7 +297,7 @@ var validBidderRequest1OutstreamVideo2020 = { } }, 'userId': { - 'id5id': 'ID5-ZHMOpSv9CkZNiNd1oR4zc62AzCgSS73fPjmQ6Od7OA', + 'id5id': { uid: 'ID5-ZHMOpSv9CkZNiNd1oR4zc62AzCgSS73fPjmQ6Od7OA' }, 'pubcid': '2ada6ae6-aeca-4e07-8922-a99b3aaf8a56' }, 'userIdAsEids': [ @@ -2118,7 +2118,7 @@ describe('ozone Adapter', function () { bidRequests[0]['userId'] = { 'criteortus': '1111', 'digitrustid': {data: {id: 'DTID', keyv: 4, privacy: {optout: false}, producer: 'ABC', version: 2}}, - 'id5id': '2222', + 'id5id': {'uid': '2222'}, 'idl_env': '3333', 'lipb': {'lipbid': '4444'}, 'parrableId': {eid: 'eidVersion.encryptionKeyReference.encryptedValue'}, @@ -2138,7 +2138,7 @@ describe('ozone Adapter', function () { bidRequests[0]['userId'] = { 'criteortus': '1111', 'digitrustid': {data: {id: 'DTID', keyv: 4, privacy: {optout: false}, producer: 'ABC', version: 2}}, - 'id5id': '2222', + 'id5id': {'uid': '2222'}, 'idl_env': '3333', 'lipb': {'lipbid': '4444'}, 'parrableId': {eid: 'eidVersion.encryptionKeyReference.encryptedValue'}, diff --git a/test/spec/modules/parrableIdSystem_spec.js b/test/spec/modules/parrableIdSystem_spec.js index 1cc89240bc3..5e62af9b2fa 100644 --- a/test/spec/modules/parrableIdSystem_spec.js +++ b/test/spec/modules/parrableIdSystem_spec.js @@ -92,7 +92,7 @@ describe('Parrable ID System', function() { }) it('creates xhr to Parrable that synchronizes the ID', function() { - let getIdResult = parrableIdSubmodule.getId(P_CONFIG_MOCK.params); + let getIdResult = parrableIdSubmodule.getId(P_CONFIG_MOCK); getIdResult.callback(callbackSpy); @@ -128,7 +128,7 @@ describe('Parrable ID System', function() { let uspString = '1YNN'; uspDataHandler.setConsentData(uspString); parrableIdSubmodule.getId( - P_CONFIG_MOCK.params, + P_CONFIG_MOCK, null, null ).callback(callbackSpy); @@ -138,7 +138,7 @@ describe('Parrable ID System', function() { it('should log an error and continue to callback if ajax request errors', function () { let callBackSpy = sinon.spy(); - let submoduleCallback = parrableIdSubmodule.getId({partner: 'prebid'}).callback; + let submoduleCallback = parrableIdSubmodule.getId({ params: {partner: 'prebid'} }).callback; submoduleCallback(callBackSpy); let request = server.requests[0]; expect(request.url).to.contain('h.parrable.com'); @@ -155,7 +155,7 @@ describe('Parrable ID System', function() { describe('response id', function() { it('provides the stored Parrable values if a cookie exists', function() { writeParrableCookie({ eid: P_COOKIE_EID }); - let getIdResult = parrableIdSubmodule.getId(P_CONFIG_MOCK.params); + let getIdResult = parrableIdSubmodule.getId(P_CONFIG_MOCK); removeParrableCookie(); expect(getIdResult.id).to.deep.equal({ @@ -171,7 +171,7 @@ describe('Parrable ID System', function() { storage.setCookie(oldEidCookieName, oldEid); storage.setCookie(oldOptoutCookieName, 'true'); - let getIdResult = parrableIdSubmodule.getId(P_CONFIG_MOCK.params); + let getIdResult = parrableIdSubmodule.getId(P_CONFIG_MOCK); expect(getIdResult.id).to.deep.equal({ eid: oldEid, ibaOptout: true @@ -212,9 +212,9 @@ describe('Parrable ID System', function() { }); it('permits an impression when no timezoneFilter is configured', function() { - expect(parrableIdSubmodule.getId({ + expect(parrableIdSubmodule.getId({ params: { partner: 'prebid-test', - })).to.have.property('callback'); + } })).to.have.property('callback'); }); it('permits an impression from a blocked timezone when a cookie exists', function() { @@ -224,12 +224,12 @@ describe('Parrable ID System', function() { writeParrableCookie({ eid: P_COOKIE_EID }); - expect(parrableIdSubmodule.getId({ + expect(parrableIdSubmodule.getId({ params: { partner: 'prebid-test', timezoneFilter: { blockedZones: [ blockedZone ] } - })).to.have.property('callback'); + } })).to.have.property('callback'); expect(resolvedOptions.called).to.equal(false); removeParrableCookie(); @@ -240,12 +240,12 @@ describe('Parrable ID System', function() { const resolvedOptions = sinon.stub().returns({ timeZone: allowedZone }); Intl.DateTimeFormat.returns({ resolvedOptions }); - expect(parrableIdSubmodule.getId({ + expect(parrableIdSubmodule.getId({ params: { partner: 'prebid-test', timezoneFilter: { allowedZones: [ allowedZone ] } - })).to.have.property('callback'); + } })).to.have.property('callback'); expect(resolvedOptions.called).to.equal(true); }); @@ -254,12 +254,12 @@ describe('Parrable ID System', function() { const resolvedOptions = sinon.stub().returns({ timeZone: 'Iceland' }); Intl.DateTimeFormat.returns({ resolvedOptions }); - expect(parrableIdSubmodule.getId({ + expect(parrableIdSubmodule.getId({ params: { partner: 'prebid-test', timezoneFilter: { blockedZones: [ blockedZone ] } - })).to.have.property('callback'); + } })).to.have.property('callback'); expect(resolvedOptions.called).to.equal(true); }); @@ -268,12 +268,12 @@ describe('Parrable ID System', function() { const resolvedOptions = sinon.stub().returns({ timeZone: blockedZone }); Intl.DateTimeFormat.returns({ resolvedOptions }); - expect(parrableIdSubmodule.getId({ + expect(parrableIdSubmodule.getId({ params: { partner: 'prebid-test', timezoneFilter: { blockedZones: [ blockedZone ] } - })).to.equal(null); + } })).to.equal(null); expect(resolvedOptions.called).to.equal(true); }); @@ -282,13 +282,13 @@ describe('Parrable ID System', function() { const resolvedOptions = sinon.stub().returns({ timeZone: timezone }); Intl.DateTimeFormat.returns({ resolvedOptions }); - expect(parrableIdSubmodule.getId({ + expect(parrableIdSubmodule.getId({ params: { partner: 'prebid-test', timezoneFilter: { allowedZones: [ timezone ], blockedZones: [ timezone ] } - })).to.equal(null); + } })).to.equal(null); expect(resolvedOptions.called).to.equal(true); }); }); @@ -312,12 +312,12 @@ describe('Parrable ID System', function() { writeParrableCookie({ eid: P_COOKIE_EID }); - expect(parrableIdSubmodule.getId({ + expect(parrableIdSubmodule.getId({ params: { partner: 'prebid-test', timezoneFilter: { blockedOffsets: [ blockedOffset ] } - })).to.have.property('callback'); + } })).to.have.property('callback'); removeParrableCookie(); }); @@ -326,12 +326,12 @@ describe('Parrable ID System', function() { const allowedOffset = -5; Date.prototype.getTimezoneOffset.returns(allowedOffset * 60); - expect(parrableIdSubmodule.getId({ + expect(parrableIdSubmodule.getId({ params: { partner: 'prebid-test', timezoneFilter: { allowedOffsets: [ allowedOffset ] } - })).to.have.property('callback'); + } })).to.have.property('callback'); expect(Date.prototype.getTimezoneOffset.called).to.equal(true); }); @@ -340,12 +340,12 @@ describe('Parrable ID System', function() { const blockedOffset = 5; Date.prototype.getTimezoneOffset.returns(allowedOffset * 60); - expect(parrableIdSubmodule.getId({ + expect(parrableIdSubmodule.getId({ params: { partner: 'prebid-test', timezoneFilter: { blockedOffsets: [ blockedOffset ] } - })).to.have.property('callback'); + }})).to.have.property('callback'); expect(Date.prototype.getTimezoneOffset.called).to.equal(true); }); @@ -353,12 +353,12 @@ describe('Parrable ID System', function() { const blockedOffset = -5; Date.prototype.getTimezoneOffset.returns(blockedOffset * 60); - expect(parrableIdSubmodule.getId({ + expect(parrableIdSubmodule.getId({ params: { partner: 'prebid-test', timezoneFilter: { blockedOffsets: [ blockedOffset ] } - })).to.equal(null); + } })).to.equal(null); expect(Date.prototype.getTimezoneOffset.called).to.equal(true); }); @@ -366,13 +366,13 @@ describe('Parrable ID System', function() { const offset = -5; Date.prototype.getTimezoneOffset.returns(offset * 60); - expect(parrableIdSubmodule.getId({ + expect(parrableIdSubmodule.getId({ params: { partner: 'prebid-test', timezoneFilter: { allowedOffset: [ offset ], blockedOffsets: [ offset ] } - })).to.equal(null); + } })).to.equal(null); expect(Date.prototype.getTimezoneOffset.called).to.equal(true); }); }); diff --git a/test/spec/modules/prebidServerBidAdapter_spec.js b/test/spec/modules/prebidServerBidAdapter_spec.js index d6f755914e5..d069bb74944 100644 --- a/test/spec/modules/prebidServerBidAdapter_spec.js +++ b/test/spec/modules/prebidServerBidAdapter_spec.js @@ -1178,7 +1178,13 @@ describe('S2S Adapter', function () { lipbid: 'li-xyz', segments: ['segA', 'segB'] }, - idl_env: '0000-1111-2222-3333' + idl_env: '0000-1111-2222-3333', + id5id: { + uid: '11111', + ext: { + linkType: 'some-link-type' + } + } }; userIdBidRequest[0].bids[0].userIdAsEids = createEidsArray(userIdBidRequest[0].bids[0].userId); @@ -1199,6 +1205,9 @@ describe('S2S Adapter', function () { expect(requestBid.user.ext.eids.filter(eid => eid.source === 'liveintent.com')[0].ext.segments.length).is.equal(2); expect(requestBid.user.ext.eids.filter(eid => eid.source === 'liveintent.com')[0].ext.segments[0]).is.equal('segA'); expect(requestBid.user.ext.eids.filter(eid => eid.source === 'liveintent.com')[0].ext.segments[1]).is.equal('segB'); + expect(requestBid.user.ext.eids.filter(eid => eid.source === 'id5-sync.com')).is.not.empty; + expect(requestBid.user.ext.eids.filter(eid => eid.source === 'id5-sync.com')[0].uids[0].id).is.equal('11111'); + expect(requestBid.user.ext.eids.filter(eid => eid.source === 'id5-sync.com')[0].ext.linkType).is.equal('some-link-type'); // LiveRamp should exist expect(requestBid.user.ext.eids.filter(eid => eid.source === 'liveramp.com')[0].uids[0].id).is.equal('0000-1111-2222-3333'); }); diff --git a/test/spec/modules/prebidmanagerAnalyticsAdapter_spec.js b/test/spec/modules/prebidmanagerAnalyticsAdapter_spec.js index ce97789fe3e..ef7cb2bbe3b 100644 --- a/test/spec/modules/prebidmanagerAnalyticsAdapter_spec.js +++ b/test/spec/modules/prebidmanagerAnalyticsAdapter_spec.js @@ -1,6 +1,8 @@ import prebidmanagerAnalytics from 'modules/prebidmanagerAnalyticsAdapter.js'; import {expect} from 'chai'; import {server} from 'test/mocks/xhr.js'; +import * as utils from 'src/utils.js'; + let events = require('src/events'); let constants = require('src/constants.json'); @@ -98,7 +100,7 @@ describe('Prebid Manager Analytics Adapter', function () { events.emit(constants.EVENTS.AUCTION_END, {}); events.emit(constants.EVENTS.BID_TIMEOUT, {}); - sinon.assert.callCount(prebidmanagerAnalytics.track, 7); + sinon.assert.callCount(prebidmanagerAnalytics.track, 6); }); }); @@ -135,4 +137,23 @@ describe('Prebid Manager Analytics Adapter', function () { expect(pmEvents.utmTags.utm_content).to.equal(''); }); }); + + describe('build page info', function () { + afterEach(function () { + prebidmanagerAnalytics.disableAnalytics() + }); + it('should build page info', function () { + prebidmanagerAnalytics.enableAnalytics({ + provider: 'prebidmanager', + options: { + bundleId: 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx' + } + }); + + const pmEvents = JSON.parse(server.requests[0].requestBody.substring(2)); + + expect(pmEvents.pageInfo.domain).to.equal(window.location.hostname); + expect(pmEvents.pageInfo.referrerDomain).to.equal(utils.parseUrl(document.referrer).hostname); + }); + }); }); diff --git a/test/spec/modules/priceFloors_spec.js b/test/spec/modules/priceFloors_spec.js index ae45244f03d..984d6da1cb9 100644 --- a/test/spec/modules/priceFloors_spec.js +++ b/test/spec/modules/priceFloors_spec.js @@ -23,6 +23,38 @@ describe('the price floors module', function () { let clock; const basicFloorData = { modelVersion: 'basic model', + modelWeight: 10, + modelTimestamp: 1606772895, + currency: 'USD', + schema: { + delimiter: '|', + fields: ['mediaType'] + }, + values: { + 'banner': 1.0, + 'video': 5.0, + '*': 2.5 + } + }; + const basicFloorDataHigh = { + floorMin: 7.0, + modelVersion: 'basic model', + modelWeight: 10, + currency: 'USD', + schema: { + delimiter: '|', + fields: ['mediaType'] + }, + values: { + 'banner': 1.0, + 'video': 5.0, + '*': 2.5 + } + }; + const basicFloorDataLow = { + floorMin: 2.3, + modelVersion: 'basic model', + modelWeight: 10, currency: 'USD', schema: { delimiter: '|', @@ -46,6 +78,32 @@ describe('the price floors module', function () { }, data: basicFloorData } + const minFloorConfigHigh = { + enabled: true, + auctionDelay: 0, + floorMin: 7, + endpoint: {}, + enforcement: { + enforceJS: true, + enforcePBS: false, + floorDeals: false, + bidAdjustment: true + }, + data: basicFloorDataHigh + } + const minFloorConfigLow = { + enabled: true, + auctionDelay: 0, + floorMin: 2.3, + endpoint: {}, + enforcement: { + enforceJS: true, + enforcePBS: false, + floorDeals: false, + bidAdjustment: true + }, + data: basicFloorDataLow + } const basicBidRequest = { bidder: 'rubicon', adUnitCode: 'test_div_1', @@ -130,6 +188,8 @@ describe('the price floors module', function () { let resultingData = getFloorsDataForAuction(inputFloorData, 'test_div_1'); expect(resultingData).to.deep.equal({ modelVersion: 'basic model', + modelWeight: 10, + modelTimestamp: 1606772895, currency: 'USD', schema: { delimiter: '|', @@ -147,6 +207,8 @@ describe('the price floors module', function () { resultingData = getFloorsDataForAuction(inputFloorData, 'this_is_a_div'); expect(resultingData).to.deep.equal({ modelVersion: 'basic model', + modelWeight: 10, + modelTimestamp: 1606772895, currency: 'USD', schema: { delimiter: '^', @@ -165,22 +227,50 @@ describe('the price floors module', function () { it('selects the right floor for different mediaTypes', function () { // banner with * size (not in rule file so does not do anything) expect(getFirstMatchingFloor({...basicFloorData}, basicBidRequest, {mediaType: 'banner', size: '*'})).to.deep.equal({ + floorMin: 0, + floorRuleValue: 1.0, matchingFloor: 1.0, matchingData: 'banner', matchingRule: 'banner' }); // video with * size (not in rule file so does not do anything) expect(getFirstMatchingFloor({...basicFloorData}, basicBidRequest, {mediaType: 'video', size: '*'})).to.deep.equal({ + floorMin: 0, + floorRuleValue: 5.0, matchingFloor: 5.0, matchingData: 'video', matchingRule: 'video' }); // native (not in the rule list) with * size (not in rule file so does not do anything) expect(getFirstMatchingFloor({...basicFloorData}, basicBidRequest, {mediaType: 'native', size: '*'})).to.deep.equal({ + floorMin: 0, + floorRuleValue: 2.5, matchingFloor: 2.5, matchingData: 'native', matchingRule: '*' }); + // banner with floorMin higher than matching rule + handleSetFloorsConfig({ + ...minFloorConfigHigh + }); + expect(getFirstMatchingFloor({...basicFloorDataHigh}, basicBidRequest, {mediaType: 'banner', size: '*'})).to.deep.equal({ + floorMin: 7, + floorRuleValue: 1.0, + matchingFloor: 7, + matchingData: 'banner', + matchingRule: 'banner' + }); + // banner with floorMin higher than matching rule + handleSetFloorsConfig({ + ...minFloorConfigLow + }); + expect(getFirstMatchingFloor({...basicFloorDataLow}, basicBidRequest, {mediaType: 'video', size: '*'})).to.deep.equal({ + floorMin: 2.3, + floorRuleValue: 5, + matchingFloor: 5, + matchingData: 'video', + matchingRule: 'video' + }); }); it('does not alter cached matched input if conversion occurs', function () { let inputData = {...basicFloorData}; @@ -188,6 +278,8 @@ describe('the price floors module', function () { let result = getFirstMatchingFloor(inputData, basicBidRequest, {mediaType: 'banner', size: '*'}); // result should always be the same expect(result).to.deep.equal({ + floorMin: 0, + floorRuleValue: 1.0, matchingFloor: 1.0, matchingData: 'banner', matchingRule: 'banner' @@ -213,24 +305,32 @@ describe('the price floors module', function () { } // banner with 300x250 size expect(getFirstMatchingFloor(inputFloorData, basicBidRequest, {mediaType: 'banner', size: [300, 250]})).to.deep.equal({ + floorMin: 0, + floorRuleValue: 1.1, matchingFloor: 1.1, matchingData: '300x250', matchingRule: '300x250' }); // video with 300x250 size expect(getFirstMatchingFloor(inputFloorData, basicBidRequest, {mediaType: 'video', size: [300, 250]})).to.deep.equal({ + floorMin: 0, + floorRuleValue: 1.1, matchingFloor: 1.1, matchingData: '300x250', matchingRule: '300x250' }); // native (not in the rule list) with 300x600 size expect(getFirstMatchingFloor(inputFloorData, basicBidRequest, {mediaType: 'native', size: [600, 300]})).to.deep.equal({ + floorMin: 0, + floorRuleValue: 4.4, matchingFloor: 4.4, matchingData: '600x300', matchingRule: '600x300' }); // n/a mediaType with a size not in file should go to catch all expect(getFirstMatchingFloor(inputFloorData, basicBidRequest, {mediaType: undefined, size: [1, 1]})).to.deep.equal({ + floorMin: 0, + floorRuleValue: 5.5, matchingFloor: 5.5, matchingData: '1x1', matchingRule: '*' @@ -254,12 +354,16 @@ describe('the price floors module', function () { }; // banner with 300x250 size expect(getFirstMatchingFloor(inputFloorData, basicBidRequest, {mediaType: 'banner', size: [300, 250]})).to.deep.equal({ + floorMin: 0, + floorRuleValue: 1.1, matchingFloor: 1.1, matchingData: 'test_div_1^banner^300x250', matchingRule: 'test_div_1^banner^300x250' }); // video with 300x250 size -> No matching rule so should use default expect(getFirstMatchingFloor(inputFloorData, basicBidRequest, {mediaType: 'video', size: [300, 250]})).to.deep.equal({ + floorMin: 0, + floorRuleValue: 0.5, matchingFloor: 0.5, matchingData: 'test_div_1^video^300x250', matchingRule: undefined @@ -267,6 +371,8 @@ describe('the price floors module', function () { // remove default and should still return the same floor as above since matches are cached delete inputFloorData.default; expect(getFirstMatchingFloor(inputFloorData, basicBidRequest, {mediaType: 'video', size: [300, 250]})).to.deep.equal({ + floorMin: 0, + floorRuleValue: 0.5, matchingFloor: 0.5, matchingData: 'test_div_1^video^300x250', matchingRule: undefined @@ -274,6 +380,8 @@ describe('the price floors module', function () { // update adUnitCode to test_div_2 with weird other params let newBidRequest = { ...basicBidRequest, adUnitCode: 'test_div_2' } expect(getFirstMatchingFloor(inputFloorData, newBidRequest, {mediaType: 'badmediatype', size: [900, 900]})).to.deep.equal({ + floorMin: 0, + floorRuleValue: 3.3, matchingFloor: 3.3, matchingData: 'test_div_2^badmediatype^900x900', matchingRule: 'test_div_2^*^*' @@ -327,7 +435,10 @@ describe('the price floors module', function () { runStandardAuction(); validateBidRequests(false, { skipped: true, + floorMin: undefined, modelVersion: undefined, + modelWeight: undefined, + modelTimestamp: undefined, location: 'noData', skipRate: 0, fetchStatus: undefined, @@ -358,11 +469,50 @@ describe('the price floors module', function () { } }; runStandardAuction([adUnitWithFloors1, adUnitWithFloors2]); + validateBidRequests(true, { + skipped: false, + floorMin: undefined, + modelVersion: 'adUnit Model Version', + modelWeight: 10, + modelTimestamp: 1606772895, + location: 'adUnit', + skipRate: 0, + fetchStatus: undefined, + floorProvider: undefined + }); + }); + it('should use adUnit level data and minFloor should be set', function () { + handleSetFloorsConfig({ + ...minFloorConfigHigh, + data: undefined + }); + // attach floor data onto an adUnit and run an auction + let adUnitWithFloors1 = { + ...getAdUnitMock('adUnit-Div-1'), + floors: { + ...basicFloorData, + modelVersion: 'adUnit Model Version', // change the model name + } + }; + let adUnitWithFloors2 = { + ...getAdUnitMock('adUnit-Div-2'), + floors: { + ...basicFloorData, + values: { + 'banner': 5.0, + '*': 10.4 + } + } + }; + runStandardAuction([adUnitWithFloors1, adUnitWithFloors2]); validateBidRequests(true, { skipped: false, modelVersion: 'adUnit Model Version', + modelWeight: 10, + modelTimestamp: 1606772895, location: 'adUnit', skipRate: 0, + floorMin: 7, fetchStatus: undefined, floorProvider: undefined }); @@ -372,7 +522,10 @@ describe('the price floors module', function () { runStandardAuction(); validateBidRequests(true, { skipped: false, + floorMin: undefined, modelVersion: 'basic model', + modelWeight: 10, + modelTimestamp: 1606772895, location: 'setConfig', skipRate: 0, fetchStatus: undefined, @@ -392,7 +545,10 @@ describe('the price floors module', function () { runStandardAuction(); validateBidRequests(true, { skipped: false, + floorMin: undefined, modelVersion: 'basic model', + modelWeight: 10, + modelTimestamp: 1606772895, location: 'setConfig', skipRate: 0, fetchStatus: undefined, @@ -405,7 +561,10 @@ describe('the price floors module', function () { runStandardAuction(); validateBidRequests(true, { skipped: false, + floorMin: undefined, modelVersion: 'basic model', + modelWeight: 10, + modelTimestamp: 1606772895, location: 'setConfig', skipRate: 0, fetchStatus: undefined, @@ -418,7 +577,10 @@ describe('the price floors module', function () { runStandardAuction(); validateBidRequests(true, { skipped: false, + floorMin: undefined, modelVersion: 'basic model', + modelWeight: 10, + modelTimestamp: 1606772895, location: 'setConfig', skipRate: 0, fetchStatus: undefined, @@ -440,7 +602,10 @@ describe('the price floors module', function () { runStandardAuction(); validateBidRequests(true, { skipped: false, + floorMin: undefined, modelVersion: 'basic model', + modelWeight: 10, + modelTimestamp: 1606772895, location: 'setConfig', skipRate: 50, fetchStatus: undefined, @@ -453,7 +618,10 @@ describe('the price floors module', function () { runStandardAuction(); validateBidRequests(true, { skipped: false, + floorMin: undefined, modelVersion: 'basic model', + modelWeight: 10, + modelTimestamp: 1606772895, location: 'setConfig', skipRate: 10, fetchStatus: undefined, @@ -466,7 +634,10 @@ describe('the price floors module', function () { runStandardAuction(); validateBidRequests(true, { skipped: false, + floorMin: undefined, modelVersion: 'basic model', + modelWeight: 10, + modelTimestamp: 1606772895, location: 'setConfig', skipRate: 0, fetchStatus: undefined, @@ -529,7 +700,10 @@ describe('the price floors module', function () { runStandardAuction(); validateBidRequests(true, { skipped: false, + floorMin: undefined, modelVersion: 'model-1', + modelWeight: 10, + modelTimestamp: undefined, location: 'setConfig', skipRate: 0, fetchStatus: undefined, @@ -541,7 +715,10 @@ describe('the price floors module', function () { runStandardAuction(); validateBidRequests(true, { skipped: false, + floorMin: undefined, modelVersion: 'model-2', + modelWeight: 40, + modelTimestamp: undefined, location: 'setConfig', skipRate: 0, fetchStatus: undefined, @@ -553,7 +730,10 @@ describe('the price floors module', function () { runStandardAuction(); validateBidRequests(true, { skipped: false, + floorMin: undefined, modelVersion: 'model-3', + modelWeight: 50, + modelTimestamp: undefined, location: 'setConfig', skipRate: 0, fetchStatus: undefined, @@ -581,7 +761,10 @@ describe('the price floors module', function () { runStandardAuction(); validateBidRequests(true, { skipped: false, + floorMin: undefined, modelVersion: 'basic model', + modelWeight: 10, + modelTimestamp: 1606772895, location: 'setConfig', skipRate: 0, fetchStatus: undefined, @@ -659,7 +842,10 @@ describe('the price floors module', function () { // the exposedAdUnits should be from the fetch not setConfig level data validateBidRequests(true, { skipped: false, + floorMin: undefined, modelVersion: 'basic model', + modelWeight: 10, + modelTimestamp: 1606772895, location: 'setConfig', skipRate: 0, fetchStatus: 'timeout', @@ -696,7 +882,10 @@ describe('the price floors module', function () { // and fetchStatus is success since fetch worked validateBidRequests(true, { skipped: false, + floorMin: undefined, modelVersion: 'fetch model name', + modelWeight: 10, + modelTimestamp: 1606772895, location: 'fetch', skipRate: 0, fetchStatus: 'success', @@ -732,7 +921,10 @@ describe('the price floors module', function () { // and fetchStatus is success since fetch worked validateBidRequests(true, { skipped: false, + floorMin: undefined, modelVersion: 'fetch model name', + modelWeight: 10, + modelTimestamp: 1606772895, location: 'fetch', skipRate: 0, fetchStatus: 'success', @@ -771,7 +963,10 @@ describe('the price floors module', function () { // and fetchStatus is success since fetch worked validateBidRequests(true, { skipped: false, + floorMin: undefined, modelVersion: 'fetch model name', + modelWeight: 10, + modelTimestamp: 1606772895, location: 'fetch', skipRate: 95, fetchStatus: 'success', @@ -792,7 +987,10 @@ describe('the price floors module', function () { // and fetch failed is true validateBidRequests(true, { skipped: false, + floorMin: undefined, modelVersion: 'basic model', + modelWeight: 10, + modelTimestamp: 1606772895, location: 'setConfig', skipRate: 0, fetchStatus: 'error', @@ -815,7 +1013,10 @@ describe('the price floors module', function () { // and fetchStatus is 'success' but location is setConfig since it had bad data validateBidRequests(true, { skipped: false, + floorMin: undefined, modelVersion: 'basic model', + modelWeight: 10, + modelTimestamp: 1606772895, location: 'setConfig', skipRate: 0, fetchStatus: 'success', @@ -1303,6 +1504,7 @@ describe('the price floors module', function () { runBidResponse(); expect(returnedBidResponse).to.haveOwnProperty('floorData'); expect(returnedBidResponse.floorData).to.deep.equal({ + floorRuleValue: 0.3, floorValue: 0.3, floorCurrency: 'USD', floorRule: 'banner', @@ -1340,6 +1542,7 @@ describe('the price floors module', function () { expect(returnedBidResponse).to.haveOwnProperty('floorData'); expect(returnedBidResponse.floorData).to.deep.equal({ floorValue: 0.5, + floorRuleValue: 0.5, floorCurrency: 'USD', floorRule: 'banner|300x250', cpmAfterAdjustments: 0.5, @@ -1366,6 +1569,7 @@ describe('the price floors module', function () { }); expect(returnedBidResponse).to.haveOwnProperty('floorData'); expect(returnedBidResponse.floorData).to.deep.equal({ + floorRuleValue: 5.5, floorValue: 5.5, floorCurrency: 'USD', floorRule: 'video|*', diff --git a/test/spec/modules/pubgeniusBidAdapter_spec.js b/test/spec/modules/pubgeniusBidAdapter_spec.js index 52f2e3aeefe..382199dcffc 100644 --- a/test/spec/modules/pubgeniusBidAdapter_spec.js +++ b/test/spec/modules/pubgeniusBidAdapter_spec.js @@ -1,8 +1,9 @@ import { expect } from 'chai'; import { spec } from 'modules/pubgeniusBidAdapter.js'; -import { deepClone, parseQueryStringParameters } from 'src/utils.js'; import { config } from 'src/config.js'; +import { VIDEO } from 'src/mediaTypes.js'; +import { deepClone, parseQueryStringParameters } from 'src/utils.js'; import { server } from 'test/mocks/xhr.js'; const { @@ -23,8 +24,8 @@ describe('pubGENIUS adapter', () => { }); describe('supportedMediaTypes', () => { - it('should contain only banner', () => { - expect(supportedMediaTypes).to.deep.equal(['banner']); + it('should contain banner and video', () => { + expect(supportedMediaTypes).to.deep.equal(['banner', 'video']); }); }); @@ -77,6 +78,51 @@ describe('pubGENIUS adapter', () => { expect(isBidRequestValid(bid)).to.be.false; }); + + it('should return false without banner or video', () => { + bid.mediaTypes = {}; + + expect(isBidRequestValid(bid)).to.be.false; + }); + + it('should return true with valid video media type', () => { + bid.mediaTypes = { + video: { + context: 'instream', + playerSize: [[100, 100]], + mimes: ['video/mp4'], + protocols: [1], + }, + }; + + expect(isBidRequestValid(bid)).to.be.true; + }); + + it('should return true with valid video params', () => { + bid.params.video = { + placement: 1, + w: 200, + h: 200, + mimes: ['video/mp4'], + protocols: [1], + }; + + expect(isBidRequestValid(bid)).to.be.true; + }); + + it('should return false without video protocols', () => { + bid.mediaTypes = { + video: { + context: 'instream', + playerSize: [[100, 100]], + }, + }; + bid.params.video = { + mimes: ['video/mp4'], + }; + + expect(isBidRequestValid(bid)).to.be.false; + }); }); describe('buildRequests', () => { @@ -122,6 +168,7 @@ describe('pubGENIUS adapter', () => { bidderCode: 'pubgenius', bidderRequestId: 'fakebidderrequestid', refererInfo: {}, + timeout: 1200, }; expectedRequest = { @@ -142,14 +189,14 @@ describe('pubGENIUS adapter', () => { tmax: 1200, ext: { pbadapter: { - version: '1.0.0', + version: '1.1.0', }, }, }, }; config.setConfig({ - bidderTimeout: 1200, + bidderTimeout: 1000, pageUrl: undefined, coppa: undefined, }); @@ -313,6 +360,44 @@ describe('pubGENIUS adapter', () => { expect(buildRequests([bidRequest], bidderRequest)).to.deep.equal(expectedRequest); }); + + it('should build video imp', () => { + bidRequest.mediaTypes = { + video: { + context: 'instream', + playerSize: [[200, 100]], + mimes: ['video/mp4'], + protocols: [2, 3], + api: [1, 2], + playbackmethod: [3, 4], + }, + }; + bidRequest.params.video = { + minduration: 5, + maxduration: 100, + skip: 1, + skipafter: 1, + startdelay: -1, + }; + + delete expectedRequest.data.imp[0].banner; + expectedRequest.data.imp[0].video = { + mimes: ['video/mp4'], + minduration: 5, + maxduration: 100, + protocols: [2, 3], + w: 200, + h: 100, + startdelay: -1, + placement: 1, + skip: 1, + skipafter: 1, + playbackmethod: [3, 4], + api: [1, 2], + }; + + expect(buildRequests([bidRequest], bidderRequest)).to.deep.equal(expectedRequest); + }); }); describe('interpretResponse', () => { @@ -369,6 +454,29 @@ describe('pubGENIUS adapter', () => { it('should interpret no bids', () => { expect(interpretResponse({ body: {} })).to.deep.equal([]); }); + + it('should interpret video response', () => { + serverResponse.body.seatbid[0].bid[0] = { + ...serverResponse.body.seatbid[0].bid[0], + nurl: 'http://vasturl/cache?id=x', + ext: { + pbadapter: { + mediaType: 'video', + cacheKey: 'x', + }, + }, + }; + + delete expectedBidResponse.ad; + expectedBidResponse = { + ...expectedBidResponse, + vastUrl: 'http://vasturl/cache?id=x', + vastXml: 'fake_creative', + mediaType: VIDEO, + }; + + expect(interpretResponse(serverResponse)).to.deep.equal([expectedBidResponse]); + }); }); describe('getUserSyncs', () => { diff --git a/test/spec/modules/pubmaticAnalyticsAdapter_spec.js b/test/spec/modules/pubmaticAnalyticsAdapter_spec.js index 537ba4483cf..11de7b13208 100755 --- a/test/spec/modules/pubmaticAnalyticsAdapter_spec.js +++ b/test/spec/modules/pubmaticAnalyticsAdapter_spec.js @@ -11,6 +11,12 @@ let events = require('src/events'); let ajax = require('src/ajax'); let utils = require('src/utils'); +const DEFAULT_USER_AGENT = window.navigator.userAgent; +const MOBILE_USER_AGENT = 'Mozilla/5.0 (iPhone; CPU iPhone OS 13_3_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0.5 Mobile/15E148 Safari/604.1'; +const setUADefault = () => { window.navigator.__defineGetter__('userAgent', function () { return DEFAULT_USER_AGENT }) }; +const setUAMobile = () => { window.navigator.__defineGetter__('userAgent', function () { return MOBILE_USER_AGENT }) }; +const setUANull = () => { window.navigator.__defineGetter__('userAgent', function () { return null }) }; + const { EVENTS: { AUCTION_INIT, @@ -247,6 +253,7 @@ describe('pubmatic analytics adapter', function () { let clock; beforeEach(function () { + setUADefault(); sandbox = sinon.sandbox.create(); xhr = sandbox.useFakeXMLHttpRequest(); @@ -316,7 +323,7 @@ describe('pubmatic analytics adapter', function () { clock.tick(2000 + 1000); expect(requests.length).to.equal(3); // 1 logger and 2 win-tracker let request = requests[2]; // logger is executed late, trackers execute first - expect(request.url).to.equal('https://t.pubmatic.com/wl?pubid=9999&gdEn=1'); + expect(request.url).to.equal('https://t.pubmatic.com/wl?pubid=9999'); let data = getLoggerJsonFromRequest(request.requestBody); expect(data.pubid).to.equal('9999'); expect(data.pid).to.equal('1111'); @@ -326,8 +333,6 @@ describe('pubmatic analytics adapter', function () { expect(data.purl).to.equal('http://www.test.com/page.html'); expect(data.orig).to.equal('www.test.com'); expect(data.tst).to.equal(1519767016); - expect(data.cns).to.equal('here-goes-gdpr-consent-string'); - expect(data.gdpr).to.equal(1); expect(data.tgid).to.equal(15); expect(data.s).to.be.an('array'); expect(data.s.length).to.equal(2); @@ -423,7 +428,7 @@ describe('pubmatic analytics adapter', function () { clock.tick(2000 + 1000); expect(requests.length).to.equal(3); // 1 logger and 2 win-tracker let request = requests[2]; // logger is executed late, trackers execute first - expect(request.url).to.equal('https://t.pubmatic.com/wl?pubid=9999&gdEn=1'); + expect(request.url).to.equal('https://t.pubmatic.com/wl?pubid=9999'); let data = getLoggerJsonFromRequest(request.requestBody); expect(data.pubid).to.equal('9999'); expect(data.pid).to.equal('1111'); @@ -490,7 +495,7 @@ describe('pubmatic analytics adapter', function () { clock.tick(2000 + 1000); expect(requests.length).to.equal(3); // 1 logger and 2 win-tracker let request = requests[2]; // logger is executed late, trackers execute first - expect(request.url).to.equal('https://t.pubmatic.com/wl?pubid=9999&gdEn=1'); + expect(request.url).to.equal('https://t.pubmatic.com/wl?pubid=9999'); let data = getLoggerJsonFromRequest(request.requestBody); expect(data.pubid).to.equal('9999'); expect(data.pid).to.equal('1111'); @@ -645,6 +650,7 @@ describe('pubmatic analytics adapter', function () { }); it('Logger: currency conversion check', function() { + setUANull(); setConfig({ adServerCurrency: 'JPY', rates: { @@ -672,7 +678,7 @@ describe('pubmatic analytics adapter', function () { clock.tick(2000 + 1000); expect(requests.length).to.equal(3); // 1 logger and 2 win-tracker let request = requests[2]; // logger is executed late, trackers execute first - expect(request.url).to.equal('https://t.pubmatic.com/wl?pubid=9999&gdEn=1'); + expect(request.url).to.equal('https://t.pubmatic.com/wl?pubid=9999'); let data = getLoggerJsonFromRequest(request.requestBody); expect(data.s[1].sn).to.equal('/19968336/header-bid-tag-1'); expect(data.s[1].sz).to.deep.equal(['1000x300', '970x250', '728x90']); @@ -697,6 +703,212 @@ describe('pubmatic analytics adapter', function () { expect(data.s[1].ps[0].af).to.equal('banner'); expect(data.s[1].ps[0].ocpm).to.equal(100); expect(data.s[1].ps[0].ocry).to.equal('JPY'); + expect(data.dvc).to.deep.equal({'plt': 3}); + }); + + it('Logger: regexPattern in bid.params', function() { + setUAMobile(); + const BID_REQUESTED_COPY = utils.deepClone(MOCK.BID_REQUESTED); + BID_REQUESTED_COPY.bids[1].params.regexPattern = '*'; + events.emit(AUCTION_INIT, MOCK.AUCTION_INIT); + events.emit(BID_REQUESTED, BID_REQUESTED_COPY); + events.emit(BID_RESPONSE, MOCK.BID_RESPONSE[0]); + events.emit(BID_RESPONSE, BID2); + events.emit(BIDDER_DONE, MOCK.BIDDER_DONE); + events.emit(AUCTION_END, MOCK.AUCTION_END); + events.emit(SET_TARGETING, MOCK.SET_TARGETING); + events.emit(BID_WON, MOCK.BID_WON[0]); + events.emit(BID_WON, MOCK.BID_WON[1]); + + clock.tick(2000 + 1000); + expect(requests.length).to.equal(3); // 1 logger and 2 win-tracker + let request = requests[2]; // logger is executed late, trackers execute first + expect(request.url).to.equal('https://t.pubmatic.com/wl?pubid=9999'); + let data = getLoggerJsonFromRequest(request.requestBody); + expect(data.s[1].sn).to.equal('/19968336/header-bid-tag-1'); + expect(data.s[1].sz).to.deep.equal(['1000x300', '970x250', '728x90']); + expect(data.s[1].ps).to.be.an('array'); + expect(data.s[1].ps.length).to.equal(1); + expect(data.s[1].ps[0].pn).to.equal('pubmatic'); + expect(data.s[1].ps[0].bidid).to.equal('3bd4ebb1c900e2'); + expect(data.s[1].ps[0].db).to.equal(0); + expect(data.s[1].ps[0].kgpv).to.equal('*'); + expect(data.s[1].ps[0].kgpsv).to.equal('this-is-a-kgpv'); + expect(data.s[1].ps[0].psz).to.equal('728x90'); + expect(data.s[1].ps[0].eg).to.equal(1.52); + expect(data.s[1].ps[0].en).to.equal(1.52); + expect(data.s[1].ps[0].di).to.equal('the-deal-id'); + expect(data.s[1].ps[0].dc).to.equal('PMP'); + expect(data.s[1].ps[0].mi).to.equal('matched-impression'); + expect(data.s[1].ps[0].l1).to.equal(3214); + expect(data.s[1].ps[0].l2).to.equal(0); + expect(data.s[1].ps[0].ss).to.equal(1); + expect(data.s[1].ps[0].t).to.equal(0); + expect(data.s[1].ps[0].wb).to.equal(0); // bidPriceUSD is not getting set as currency module is not added, so unable to set wb to 1 + expect(data.s[1].ps[0].af).to.equal('banner'); + expect(data.s[1].ps[0].ocpm).to.equal(1.52); + expect(data.s[1].ps[0].ocry).to.equal('USD'); + expect(data.dvc).to.deep.equal({'plt': 2}); + // respective tracker slot + let firstTracker = requests[1].url; + expect(firstTracker.split('?')[0]).to.equal('https://t.pubmatic.com/wt'); + data = {}; + firstTracker.split('?')[1].split('&').map(e => e.split('=')).forEach(e => data[e[0]] = e[1]); + expect(data.kgpv).to.equal('*'); + }); + + it('Logger: regexPattern in bid.bidResponse', function() { + const BID2_COPY = utils.deepClone(BID2); + BID2_COPY.regexPattern = '*'; + events.emit(AUCTION_INIT, MOCK.AUCTION_INIT); + events.emit(AUCTION_INIT, MOCK.AUCTION_INIT); + events.emit(BID_REQUESTED, MOCK.BID_REQUESTED); + events.emit(BID_RESPONSE, MOCK.BID_RESPONSE[0]); + events.emit(BID_RESPONSE, BID2_COPY); + events.emit(BIDDER_DONE, MOCK.BIDDER_DONE); + events.emit(AUCTION_END, MOCK.AUCTION_END); + events.emit(SET_TARGETING, MOCK.SET_TARGETING); + events.emit(BID_WON, MOCK.BID_WON[0]); + events.emit(BID_WON, Object.assign({}, BID2_COPY, { + 'status': 'rendered' + })); + + clock.tick(2000 + 1000); + expect(requests.length).to.equal(3); // 1 logger and 2 win-tracker + let request = requests[2]; // logger is executed late, trackers execute first + expect(request.url).to.equal('https://t.pubmatic.com/wl?pubid=9999'); + let data = getLoggerJsonFromRequest(request.requestBody); + expect(data.s[1].sn).to.equal('/19968336/header-bid-tag-1'); + expect(data.s[1].sz).to.deep.equal(['1000x300', '970x250', '728x90']); + expect(data.s[1].ps).to.be.an('array'); + expect(data.s[1].ps.length).to.equal(1); + expect(data.s[1].ps[0].pn).to.equal('pubmatic'); + expect(data.s[1].ps[0].bidid).to.equal('3bd4ebb1c900e2'); + expect(data.s[1].ps[0].db).to.equal(0); + expect(data.s[1].ps[0].kgpv).to.equal('*'); + expect(data.s[1].ps[0].kgpsv).to.equal('this-is-a-kgpv'); + expect(data.s[1].ps[0].psz).to.equal('728x90'); + expect(data.s[1].ps[0].eg).to.equal(1.52); + expect(data.s[1].ps[0].en).to.equal(1.52); + expect(data.s[1].ps[0].di).to.equal('the-deal-id'); + expect(data.s[1].ps[0].dc).to.equal('PMP'); + expect(data.s[1].ps[0].mi).to.equal('matched-impression'); + expect(data.s[1].ps[0].l1).to.equal(3214); + expect(data.s[1].ps[0].l2).to.equal(0); + expect(data.s[1].ps[0].ss).to.equal(1); + expect(data.s[1].ps[0].t).to.equal(0); + expect(data.s[1].ps[0].wb).to.equal(0); // bidPriceUSD is not getting set as currency module is not added, so unable to set wb to 1 + expect(data.s[1].ps[0].af).to.equal('banner'); + expect(data.s[1].ps[0].ocpm).to.equal(1.52); + expect(data.s[1].ps[0].ocry).to.equal('USD'); + expect(data.dvc).to.deep.equal({'plt': 1}); + // respective tracker slot + let firstTracker = requests[1].url; + expect(firstTracker.split('?')[0]).to.equal('https://t.pubmatic.com/wt'); + data = {}; + firstTracker.split('?')[1].split('&').map(e => e.split('=')).forEach(e => data[e[0]] = e[1]); + expect(data.kgpv).to.equal('*'); + }); + + it('Logger: regexPattern in bid.params', function() { + const BID_REQUESTED_COPY = utils.deepClone(MOCK.BID_REQUESTED); + BID_REQUESTED_COPY.bids[1].params.regexPattern = '*'; + events.emit(AUCTION_INIT, MOCK.AUCTION_INIT); + events.emit(BID_REQUESTED, BID_REQUESTED_COPY); + events.emit(BID_RESPONSE, MOCK.BID_RESPONSE[0]); + events.emit(BID_RESPONSE, BID2); + events.emit(BIDDER_DONE, MOCK.BIDDER_DONE); + events.emit(AUCTION_END, MOCK.AUCTION_END); + events.emit(SET_TARGETING, MOCK.SET_TARGETING); + events.emit(BID_WON, MOCK.BID_WON[0]); + events.emit(BID_WON, MOCK.BID_WON[1]); + + clock.tick(2000 + 1000); + expect(requests.length).to.equal(3); // 1 logger and 2 win-tracker + let request = requests[2]; // logger is executed late, trackers execute first + expect(request.url).to.equal('https://t.pubmatic.com/wl?pubid=9999'); + let data = getLoggerJsonFromRequest(request.requestBody); + expect(data.s[1].sn).to.equal('/19968336/header-bid-tag-1'); + expect(data.s[1].sz).to.deep.equal(['1000x300', '970x250', '728x90']); + expect(data.s[1].ps).to.be.an('array'); + expect(data.s[1].ps.length).to.equal(1); + expect(data.s[1].ps[0].pn).to.equal('pubmatic'); + expect(data.s[1].ps[0].bidid).to.equal('3bd4ebb1c900e2'); + expect(data.s[1].ps[0].db).to.equal(0); + expect(data.s[1].ps[0].kgpv).to.equal('*'); + expect(data.s[1].ps[0].kgpsv).to.equal('this-is-a-kgpv'); + expect(data.s[1].ps[0].psz).to.equal('728x90'); + expect(data.s[1].ps[0].eg).to.equal(1.52); + expect(data.s[1].ps[0].en).to.equal(1.52); + expect(data.s[1].ps[0].di).to.equal('the-deal-id'); + expect(data.s[1].ps[0].dc).to.equal('PMP'); + expect(data.s[1].ps[0].mi).to.equal('matched-impression'); + expect(data.s[1].ps[0].l1).to.equal(3214); + expect(data.s[1].ps[0].l2).to.equal(0); + expect(data.s[1].ps[0].ss).to.equal(1); + expect(data.s[1].ps[0].t).to.equal(0); + expect(data.s[1].ps[0].wb).to.equal(0); // bidPriceUSD is not getting set as currency module is not added, so unable to set wb to 1 + expect(data.s[1].ps[0].af).to.equal('banner'); + expect(data.s[1].ps[0].ocpm).to.equal(1.52); + expect(data.s[1].ps[0].ocry).to.equal('USD'); + // respective tracker slot + let firstTracker = requests[1].url; + expect(firstTracker.split('?')[0]).to.equal('https://t.pubmatic.com/wt'); + data = {}; + firstTracker.split('?')[1].split('&').map(e => e.split('=')).forEach(e => data[e[0]] = e[1]); + expect(data.kgpv).to.equal('*'); + }); + + it('Logger: regexPattern in bid.bidResponse', function() { + const BID2_COPY = utils.deepClone(BID2); + BID2_COPY.regexPattern = '*'; + events.emit(AUCTION_INIT, MOCK.AUCTION_INIT); + events.emit(AUCTION_INIT, MOCK.AUCTION_INIT); + events.emit(BID_REQUESTED, MOCK.BID_REQUESTED); + events.emit(BID_RESPONSE, MOCK.BID_RESPONSE[0]); + events.emit(BID_RESPONSE, BID2_COPY); + events.emit(BIDDER_DONE, MOCK.BIDDER_DONE); + events.emit(AUCTION_END, MOCK.AUCTION_END); + events.emit(SET_TARGETING, MOCK.SET_TARGETING); + events.emit(BID_WON, MOCK.BID_WON[0]); + events.emit(BID_WON, Object.assign({}, BID2_COPY, { + 'status': 'rendered' + })); + + clock.tick(2000 + 1000); + expect(requests.length).to.equal(3); // 1 logger and 2 win-tracker + let request = requests[2]; // logger is executed late, trackers execute first + expect(request.url).to.equal('https://t.pubmatic.com/wl?pubid=9999'); + let data = getLoggerJsonFromRequest(request.requestBody); + expect(data.s[1].sn).to.equal('/19968336/header-bid-tag-1'); + expect(data.s[1].sz).to.deep.equal(['1000x300', '970x250', '728x90']); + expect(data.s[1].ps).to.be.an('array'); + expect(data.s[1].ps.length).to.equal(1); + expect(data.s[1].ps[0].pn).to.equal('pubmatic'); + expect(data.s[1].ps[0].bidid).to.equal('3bd4ebb1c900e2'); + expect(data.s[1].ps[0].db).to.equal(0); + expect(data.s[1].ps[0].kgpv).to.equal('*'); + expect(data.s[1].ps[0].kgpsv).to.equal('this-is-a-kgpv'); + expect(data.s[1].ps[0].psz).to.equal('728x90'); + expect(data.s[1].ps[0].eg).to.equal(1.52); + expect(data.s[1].ps[0].en).to.equal(1.52); + expect(data.s[1].ps[0].di).to.equal('the-deal-id'); + expect(data.s[1].ps[0].dc).to.equal('PMP'); + expect(data.s[1].ps[0].mi).to.equal('matched-impression'); + expect(data.s[1].ps[0].l1).to.equal(3214); + expect(data.s[1].ps[0].l2).to.equal(0); + expect(data.s[1].ps[0].ss).to.equal(1); + expect(data.s[1].ps[0].t).to.equal(0); + expect(data.s[1].ps[0].wb).to.equal(0); // bidPriceUSD is not getting set as currency module is not added, so unable to set wb to 1 + expect(data.s[1].ps[0].af).to.equal('banner'); + expect(data.s[1].ps[0].ocpm).to.equal(1.52); + expect(data.s[1].ps[0].ocry).to.equal('USD'); + // respective tracker slot + let firstTracker = requests[1].url; + expect(firstTracker.split('?')[0]).to.equal('https://t.pubmatic.com/wt'); + data = {}; + firstTracker.split('?')[1].split('&').map(e => e.split('=')).forEach(e => data[e[0]] = e[1]); + expect(data.kgpv).to.equal('*'); }); }); }); diff --git a/test/spec/modules/pubmaticBidAdapter_spec.js b/test/spec/modules/pubmaticBidAdapter_spec.js index c1f62ee9f2b..37deb0bca9c 100644 --- a/test/spec/modules/pubmaticBidAdapter_spec.js +++ b/test/spec/modules/pubmaticBidAdapter_spec.js @@ -1605,7 +1605,7 @@ describe('PubMatic adapter', function () { describe('ID5 Id', function() { it('send the id5 id if it is present', function() { bidRequests[0].userId = {}; - bidRequests[0].userId.id5id = 'id5-user-id'; + bidRequests[0].userId.id5id = { uid: 'id5-user-id' }; bidRequests[0].userIdAsEids = createEidsArray(bidRequests[0].userId); let request = spec.buildRequests(bidRequests, {}); let data = JSON.parse(request.data); @@ -1620,22 +1620,22 @@ describe('PubMatic adapter', function () { it('do not pass if not string', function() { bidRequests[0].userId = {}; - bidRequests[0].userId.id5id = 1; + bidRequests[0].userId.id5id = { uid: 1 }; bidRequests[0].userIdAsEids = createEidsArray(bidRequests[0].userId); let request = spec.buildRequests(bidRequests, {}); let data = JSON.parse(request.data); expect(data.user.eids).to.equal(undefined); - bidRequests[0].userId.id5id = []; + bidRequests[0].userId.id5id = { uid: [] }; bidRequests[0].userIdAsEids = createEidsArray(bidRequests[0].userId); request = spec.buildRequests(bidRequests, {}); data = JSON.parse(request.data); expect(data.user.eids).to.equal(undefined); - bidRequests[0].userId.id5id = null; + bidRequests[0].userId.id5id = { uid: null }; bidRequests[0].userIdAsEids = createEidsArray(bidRequests[0].userId); request = spec.buildRequests(bidRequests, {}); data = JSON.parse(request.data); expect(data.user.eids).to.equal(undefined); - bidRequests[0].userId.id5id = {}; + bidRequests[0].userId.id5id = { uid: {} }; bidRequests[0].userIdAsEids = createEidsArray(bidRequests[0].userId); request = spec.buildRequests(bidRequests, {}); data = JSON.parse(request.data); diff --git a/test/spec/modules/pubwiseBidAdapter_spec.js b/test/spec/modules/pubwiseBidAdapter_spec.js new file mode 100644 index 00000000000..450b028f6c7 --- /dev/null +++ b/test/spec/modules/pubwiseBidAdapter_spec.js @@ -0,0 +1,575 @@ +// import or require modules necessary for the test, e.g.: + +import {expect} from 'chai'; +import {spec} from 'modules/pubwiseBidAdapter.js'; +import {_checkMediaType} from 'modules/pubwiseBidAdapter.js'; // this is exported only for testing so maintaining the JS convention of _ to indicate the intent +import {_parseAdSlot} from 'modules/pubwiseBidAdapter.js'; // this is exported only for testing so maintaining the JS convention of _ to indicate the intent +import * as utils from 'src/utils.js'; + +const sampleRequestBanner = { + 'id': '6c148795eb836a', + 'tagid': 'div-gpt-ad-1460505748561-0', + 'bidfloor': 1, + 'secure': 1, + 'bidfloorcur': 'USD', + 'banner': { + 'w': 300, + 'h': 250, + 'format': [ + { + 'w': 300, + 'h': 600 + } + ], + 'pos': 0, + 'topframe': 1 + } +}; + +const sampleRequest = { + 'at': 1, + 'cur': [ + 'USD' + ], + 'imp': [ + sampleRequestBanner, + { + 'id': '7329ddc1d84eb3', + 'tagid': 'div-gpt-ad-1460505748561-1', + 'secure': 1, + 'bidfloorcur': 'USD', + 'native': { + 'request': '{"assets":[{"id":1,"required":1,"title":{"len":80}},{"id":5,"required":1,"data":{"type":2}},{"id":2,"required":1,"img":{"type":{"ID":2,"KEY":"image","TYPE":0},"w":150,"h":50}},{"id":4,"required":1,"data":{"type":1}}]}' + } + } + ], + 'site': { + 'page': 'http://localhost:9999/integrationExamples/gpt/hello_world.html?pbjs_debug=true', + 'ref': 'http://localhost:9999/integrationExamples/gpt/hello_world.html?pbjs_debug=true', + 'publisher': { + 'id': 'xxxxxx' + } + }, + 'device': { + 'ua': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/86.0.4240.198 Safari/537.36', + 'js': 1, + 'dnt': 0, + 'h': 600, + 'w': 800, + 'language': 'en-US', + 'geo': { + 'lat': 33.91989876432274, + 'lon': -84.38897708175764 + } + }, + 'user': { + 'gender': 'M', + 'geo': { + 'lat': 33.91989876432274, + 'lon': -84.38897708175764 + }, + 'yob': 2000 + }, + 'test': 0, + 'ext': { + 'version': '0.0.1' + }, + 'source': { + 'tid': '2c8cd034-f068-4419-8c30-f07292c0d17b' + } +}; + +const sampleValidBannerBidRequest = { + 'bidder': 'pubwise', + 'params': { + 'siteId': 'xxxxxx', + 'bidFloor': '1.00', + 'currency': 'USD', + 'gender': 'M', + 'lat': '33.91989876432274', + 'lon': '-84.38897708175764', + 'yob': '2000', + 'bcat': ['IAB25-3', 'IAB26-1', 'IAB26-2', 'IAB26-3', 'IAB26-4'], + }, + 'gdprConsent': { + 'consentString': 'BOEFEAyOEFEAyAHABDENAI4AAAB9vABAASA', + 'gdprApplies': 1, + }, + 'uspConsent': 1, + 'crumbs': { + 'pubcid': '9a62f261-3c0b-4cc8-8db3-a72ae86ec6ba' + }, + 'fpd': { + 'context': { + 'adServer': { + 'name': 'gam', + 'adSlot': '/19968336/header-bid-tag-0' + }, + 'pbAdSlot': '/19968336/header-bid-tag-0' + } + }, + 'mediaTypes': { + 'banner': { + 'sizes': [ + [ + 300, + 250 + ], + [ + 300, + 600 + ] + ] + } + }, + 'adUnitCode': 'div-gpt-ad-1460505748561-0', + 'transactionId': '2001a8b2-3bcf-417d-b64f-92641dae21e0', + 'sizes': [ + [ + 300, + 250 + ], + [ + 300, + 600 + ] + ], + 'bidId': '6c148795eb836a', + 'bidderRequestId': '18a45bff5ff705', + 'auctionId': '9f20663c-4629-4b5c-bff6-ff3aa8319358', + 'src': 'client', + 'bidRequestsCount': 1, + 'bidderRequestsCount': 1, + 'bidderWinsCount': 0 +}; + +const sampleValidBidRequests = [ + sampleValidBannerBidRequest, + { + 'bidder': 'pubwise', + 'params': { + 'siteId': 'xxxxxx' + }, + 'crumbs': { + 'pubcid': '9a62f261-3c0b-4cc8-8db3-a72ae86ec6ba' + }, + 'nativeParams': { + 'title': { + 'required': true, + 'len': 80 + }, + 'body': { + 'required': true + }, + 'image': { + 'required': true, + 'sizes': [ + 150, + 50 + ] + }, + 'sponsoredBy': { + 'required': true + }, + 'icon': { + 'required': false + } + }, + 'fpd': { + 'context': { + 'adServer': { + 'name': 'gam', + 'adSlot': '/19968336/header-bid-tag-0' + }, + 'pbAdSlot': '/19968336/header-bid-tag-0' + } + }, + 'mediaTypes': { + 'native': { + 'title': { + 'required': true, + 'len': 80 + }, + 'body': { + 'required': true + }, + 'image': { + 'required': true, + 'sizes': [ + 150, + 50 + ] + }, + 'sponsoredBy': { + 'required': true + }, + 'icon': { + 'required': false + } + } + }, + 'adUnitCode': 'div-gpt-ad-1460505748561-1', + 'transactionId': '2c8cd034-f068-4419-8c30-f07292c0d17b', + 'sizes': [], + 'bidId': '30ab7516a51a7c', + 'bidderRequestId': '18a45bff5ff705', + 'auctionId': '9f20663c-4629-4b5c-bff6-ff3aa8319358', + 'src': 'client', + 'bidRequestsCount': 1, + 'bidderRequestsCount': 1, + 'bidderWinsCount': 0 + } +] + +const sampleBidderBannerRequest = { + 'bidder': 'pubwise', + 'params': { + 'siteId': 'xxxxxx', + 'height': 250, + 'width': 300, + 'gender': 'M', + 'yob': '2000', + 'lat': '33.91989876432274', + 'lon': '-84.38897708175764', + 'bidFloor': '1.00', + 'currency': 'USD', + 'adSlot': '', + 'adUnit': '', + 'bcat': [ + 'IAB25-3', + 'IAB26-1', + 'IAB26-2', + 'IAB26-3', + 'IAB26-4', + ], + }, + 'crumbs': { + 'pubcid': '9a62f261-3c0b-4cc8-8db3-a72ae86ec6ba' + }, + 'fpd': { + 'context': { + 'adServer': { + 'name': 'gam', + 'adSlot': '/19968336/header-bid-tag-0' + }, + 'pbAdSlot': '/19968336/header-bid-tag-0' + } + }, + 'mediaTypes': { + 'banner': { + 'sizes': [ + [ + 300, + 600 + ] + ] + } + }, + 'adUnitCode': 'div-gpt-ad-1460505748561-0', + 'transactionId': '2001a8b2-3bcf-417d-b64f-92641dae21e0', + 'sizes': [ + [ + 300, + 250 + ], + [ + 300, + 600 + ] + ], + 'bidId': '6c148795eb836a', + 'bidderRequestId': '18a45bff5ff705', + 'auctionId': '9f20663c-4629-4b5c-bff6-ff3aa8319358', + 'src': 'client', + 'bidRequestsCount': 1, + 'bidderRequestsCount': 1, + 'bidderWinsCount': 0, + 'gdprConsent': { + 'consentString': 'BOEFEAyOEFEAyAHABDENAI4AAAB9vABAASA', + 'gdprApplies': 1, + }, + 'uspConsent': 1, +}; + +const sampleBidderRequest = { + 'bidderCode': 'pubwise', + 'auctionId': '9f20663c-4629-4b5c-bff6-ff3aa8319358', + 'bidderRequestId': '18a45bff5ff705', + 'bids': [ + sampleBidderBannerRequest, + { + 'bidder': 'pubwise', + 'params': { + 'siteId': 'xxxxxx' + }, + 'crumbs': { + 'pubcid': '9a62f261-3c0b-4cc8-8db3-a72ae86ec6ba' + }, + 'nativeParams': { + 'title': { + 'required': true, + 'len': 80 + }, + 'body': { + 'required': true + }, + 'image': { + 'required': true, + 'sizes': [ + 150, + 50 + ] + }, + 'sponsoredBy': { + 'required': true + }, + 'icon': { + 'required': false + } + }, + 'fpd': { + 'context': { + 'adServer': { + 'name': 'gam', + 'adSlot': '/19968336/header-bid-tag-0' + }, + 'pbAdSlot': '/19968336/header-bid-tag-0' + } + }, + 'mediaTypes': { + 'native': { + 'title': { + 'required': true, + 'len': 80 + }, + 'body': { + 'required': true + }, + 'image': { + 'required': true, + 'sizes': [ + 150, + 50 + ] + }, + 'sponsoredBy': { + 'required': true + }, + 'icon': { + 'required': false + } + } + }, + 'adUnitCode': 'div-gpt-ad-1460505748561-1', + 'transactionId': '2c8cd034-f068-4419-8c30-f07292c0d17b', + 'sizes': [], + 'bidId': '30ab7516a51a7c', + 'bidderRequestId': '18a45bff5ff705', + 'auctionId': '9f20663c-4629-4b5c-bff6-ff3aa8319358', + 'src': 'client', + 'bidRequestsCount': 1, + 'bidderRequestsCount': 1, + 'bidderWinsCount': 0 + } + ], + 'auctionStart': 1606269202001, + 'timeout': 1000, + 'gdprConsent': { + 'consentString': 'BOEFEAyOEFEAyAHABDENAI4AAAB9vABAASA', + 'gdprApplies': 1, + }, + 'uspConsent': 1, + 'refererInfo': { + 'referer': 'http://localhost:9999/integrationExamples/gpt/hello_world.html?pbjs_debug=true', + 'reachedTop': true, + 'isAmp': false, + 'numIframes': 0, + 'stack': [ + 'http://localhost:9999/integrationExamples/gpt/hello_world.html?pbjs_debug=true' + ], + 'canonicalUrl': null + }, + 'start': 1606269202004 +}; + +const sampleRTBResponse = { + 'body': { + 'id': '1606251348404', + 'seatbid': [ + { + 'bid': [ + { + 'id': '1606579704052', + 'impid': '6c148795eb836a', + 'price': 1.23, + 'adm': '\u003cdiv style="box-sizing: border-box;width:298px;height:248px;border: 1px solid rgba(0,0,0,.25);border-radius:10px;"\u003e\n\t\u003ch3 style="margin-top:80px;text-align: center;"\u003ePubWise Test Bid\u003c/h3\u003e\n\u003c/div\u003e', + 'crid': 'test', + 'w': 300, + 'h': 250 + }, + { + 'id': '1606579704052', + 'impid': '7329ddc1d84eb3', + 'price': 1.23, + 'adm': '{"ver":"1.2","assets":[{"id":1,"title":{"text":"PubWise Test"}},{"id":2,"img":{"type":3,"url":"http://www.pubwise.io","w":300,"h":250}},{"id":3,"img":{"type":1,"url":"http://www.pubwise.io","w":150,"h":125}},{"id":5,"data":{"type":2,"value":"PubWise Test Desc"}},{"id":4,"data":{"type":1,"value":"PubWise.io"}}],"link":{"url":"http://www.pubwise.io"}}', + 'crid': 'test', + 'w': 300, + 'h': 250 + } + ] + } + ], + 'bidid': 'testtesttest' + } +}; + +const samplePBBidObjects = [ + { + 'requestId': '6c148795eb836a', + 'cpm': '1.23', + 'width': 300, + 'height': 250, + 'creativeId': 'test', + 'currency': 'USD', + 'netRevenue': true, + 'ttl': 300, + 'ad': '
\n\t

PubWise Test Bid

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

PubWise Test Bid

'; + let newBid = {mediaType: 'unknown'}; + _checkMediaType(adm, newBid); + expect(newBid.mediaType).to.equal('banner', adm + ' Is a Banner adm'); + }); + }); + + describe('Properly Parses AdSlot Data', function () { + it('parses banner', function() { + let testBid = utils.deepClone(sampleValidBannerBidRequest) + _parseAdSlot(testBid) + expect(testBid).to.deep.equal(sampleBidderBannerRequest); + }); + }); + + describe('Properly Handles Response', function () { + it('handles response with muiltiple responses', function() { + // the request when it comes back is on the data object + let pbResponse = spec.interpretResponse(sampleRTBResponse, {'data': sampleRequest}) + expect(pbResponse).to.deep.equal(samplePBBidObjects); + }); + }); +}); diff --git a/test/spec/modules/pubxaiAnalyticsAdapter_spec.js b/test/spec/modules/pubxaiAnalyticsAdapter_spec.js new file mode 100644 index 00000000000..91c81dcae8d --- /dev/null +++ b/test/spec/modules/pubxaiAnalyticsAdapter_spec.js @@ -0,0 +1,734 @@ +import pubxaiAnalyticsAdapter from 'modules/pubxaiAnalyticsAdapter.js'; +import { getDeviceType } from 'modules/pubxaiAnalyticsAdapter.js'; +import { + expect +} from 'chai'; +import adapterManager from 'src/adapterManager.js'; +import * as utils from 'src/utils.js'; +import { + server +} from 'test/mocks/xhr.js'; + +let events = require('src/events'); +let constants = require('src/constants.json'); + +describe('pubxai analytics adapter', function() { + beforeEach(function() { + sinon.stub(events, 'getEvents').returns([]); + }); + + afterEach(function() { + events.getEvents.restore(); + }); + + describe('track', function() { + let initOptions = { + samplingRate: '1', + pubxId: '6c415fc0-8b0e-4cf5-be73-01526a4db625' + }; + + let prebidEvent = { + 'auctionInit': { + 'auctionId': 'bc3806e4-873e-453c-8ae5-204f35e923b4', + 'timestamp': 1603865707180, + 'auctionStatus': 'inProgress', + 'adUnits': [{ + 'code': '/19968336/header-bid-tag-1', + 'mediaTypes': { + 'banner': { + 'sizes': [ + [ + 300, + 250 + ] + ] + } + }, + 'bids': [{ + 'bidder': 'appnexus', + 'params': { + 'placementId': 13144370 + }, + 'auctionId': 'bc3806e4-873e-453c-8ae5-204f35e923b4', + 'floorData': { + 'skipped': false, + 'skipRate': 0, + 'modelVersion': 'new model 1.0', + 'location': 'fetch', + 'floorProvider': 'PubXFloor', + 'fetchStatus': 'success' + } + }], + 'sizes': [ + [ + 300, + 250 + ] + ], + 'transactionId': '41ec8eaf-3e7c-4a8b-8344-ab796ff6e294' + }], + 'adUnitCodes': [ + '/19968336/header-bid-tag-1' + ], + 'bidderRequests': [{ + 'bidderCode': 'appnexus', + 'auctionId': 'bc3806e4-873e-453c-8ae5-204f35e923b4', + 'bidderRequestId': '184cbc05bb90ba', + 'bids': [{ + 'bidder': 'appnexus', + 'params': { + 'placementId': 13144370 + }, + 'auctionId': 'bc3806e4-873e-453c-8ae5-204f35e923b4', + 'floorData': { + 'skipped': false, + 'skipRate': 0, + 'modelVersion': 'new model 1.0', + 'location': 'fetch', + 'floorProvider': 'PubXFloor', + 'fetchStatus': 'success' + }, + 'mediaTypes': { + 'banner': { + 'sizes': [ + [ + 300, + 250 + ] + ] + } + }, + 'adUnitCode': '/19968336/header-bid-tag-1', + 'transactionId': '41ec8eaf-3e7c-4a8b-8344-ab796ff6e294', + 'sizes': [ + [ + 300, + 250 + ] + ], + 'bidId': '248f9a4489835e', + 'bidderRequestId': '184cbc05bb90ba', + 'src': 'client', + 'bidRequestsCount': 1, + 'bidderRequestsCount': 1, + 'bidderWinsCount': 0 + }], + 'auctionStart': 1603865707180, + 'timeout': 1000, + 'refererInfo': { + 'referer': 'http://local-pnh.net:8080/stream/', + 'reachedTop': true, + 'isAmp': false, + 'numIframes': 0, + 'stack': [ + 'http://local-pnh.net:8080/stream/' + ], + 'canonicalUrl': null + }, + 'start': 1603865707182 + }], + 'noBids': [], + 'bidsReceived': [], + 'winningBids': [], + 'timeout': 1000, + 'config': { + 'samplingRate': '1', + 'pubxId': '6c415fc0-8b0e-4cf5-be73-01526a4db625' + } + }, + 'bidRequested': { + 'bidderCode': 'appnexus', + 'auctionId': 'bc3806e4-873e-453c-8ae5-204f35e923b4', + 'bidderRequestId': '184cbc05bb90ba', + 'bids': [{ + 'bidder': 'appnexus', + 'params': { + 'placementId': 13144370 + }, + 'auctionId': 'bc3806e4-873e-453c-8ae5-204f35e923b4', + 'floorData': { + 'skipped': false, + 'skipRate': 0, + 'modelVersion': 'new model 1.0', + 'location': 'fetch', + 'floorProvider': 'PubXFloor', + 'fetchStatus': 'success' + }, + 'mediaTypes': { + 'banner': { + 'sizes': [ + [ + 300, + 250 + ] + ] + } + }, + 'adUnitCode': '/19968336/header-bid-tag-1', + 'transactionId': '41ec8eaf-3e7c-4a8b-8344-ab796ff6e294', + 'sizes': [ + [ + 300, + 250 + ] + ], + 'bidId': '248f9a4489835e', + 'bidderRequestId': '184cbc05bb90ba', + 'src': 'client', + 'bidRequestsCount': 1, + 'bidderRequestsCount': 1, + 'bidderWinsCount': 0 + }], + 'auctionStart': 1603865707180, + 'timeout': 1000, + 'refererInfo': { + 'referer': 'http://local-pnh.net:8080/stream/', + 'reachedTop': true, + 'isAmp': false, + 'numIframes': 0, + 'stack': [ + 'http://local-pnh.net:8080/stream/' + ], + 'canonicalUrl': null + }, + 'start': 1603865707182 + }, + 'bidTimeout': [], + 'bidResponse': { + 'bidderCode': 'appnexus', + 'width': 300, + 'height': 250, + 'statusMessage': 'Bid available', + 'adId': '32780c4bc382cb', + 'requestId': '248f9a4489835e', + 'mediaType': 'banner', + 'source': 'client', + 'cpm': 0.5, + 'creativeId': 96846035, + 'currency': 'USD', + 'netRevenue': true, + 'ttl': 300, + 'adUnitCode': '/19968336/header-bid-tag-1', + 'appnexus': { + 'buyerMemberId': 9325 + }, + 'meta': { + 'advertiserId': 2529885 + }, + 'ad': '', + 'originalCpm': 0.5, + 'originalCurrency': 'USD', + 'floorData': { + 'floorValue': 0.4, + 'floorRule': '/19968336/header-bid-tag-1|banner', + 'floorCurrency': 'USD', + 'cpmAfterAdjustments': 0.5, + 'enforcements': { + 'enforceJS': true, + 'enforcePBS': false, + 'floorDeals': true, + 'bidAdjustment': true + }, + 'matchedFields': { + 'gptSlot': '/19968336/header-bid-tag-1', + 'mediaType': 'banner' + } + }, + 'auctionId': 'bc3806e4-873e-453c-8ae5-204f35e923b4', + 'responseTimestamp': 1603865707449, + 'requestTimestamp': 1603865707182, + 'bidder': 'appnexus', + 'timeToRespond': 267, + 'pbLg': '0.50', + 'pbMg': '0.50', + 'pbHg': '0.50', + 'pbAg': '0.50', + 'pbDg': '0.50', + 'pbCg': '0.50', + 'size': '300x250', + 'adserverTargeting': { + 'hb_bidder': 'appnexus', + 'hb_adid': '32780c4bc382cb', + 'hb_pb': '0.50', + 'hb_size': '300x250', + 'hb_source': 'client', + 'hb_format': 'banner' + }, + }, + 'auctionEnd': { + 'auctionId': 'bc3806e4-873e-453c-8ae5-204f35e923b4', + 'timestamp': 1603865707180, + 'auctionEnd': 1603865707180, + 'auctionStatus': 'completed', + 'adUnits': [{ + 'code': '/19968336/header-bid-tag-1', + 'mediaTypes': { + 'banner': { + 'sizes': [ + [ + 300, + 250 + ] + ] + } + }, + 'bids': [{ + 'bidder': 'appnexus', + 'params': { + 'placementId': 13144370 + }, + 'auctionId': 'bc3806e4-873e-453c-8ae5-204f35e923b4', + 'floorData': { + 'skipped': false, + 'skipRate': 0, + 'modelVersion': 'new model 1.0', + 'location': 'fetch', + 'floorProvider': 'PubXFloor', + 'fetchStatus': 'success' + } + }], + 'sizes': [ + [ + 300, + 250 + ] + ], + 'transactionId': '41ec8eaf-3e7c-4a8b-8344-ab796ff6e294' + }], + 'adUnitCodes': [ + '/19968336/header-bid-tag-1' + ], + 'bidderRequests': [{ + 'bidderCode': 'appnexus', + 'auctionId': 'bc3806e4-873e-453c-8ae5-204f35e923b4', + 'bidderRequestId': '184cbc05bb90ba', + 'bids': [{ + 'bidder': 'appnexus', + 'params': { + 'placementId': 13144370 + }, + 'auctionId': 'bc3806e4-873e-453c-8ae5-204f35e923b4', + 'floorData': { + 'skipped': false, + 'skipRate': 0, + 'modelVersion': 'new model 1.0', + 'location': 'fetch', + 'floorProvider': 'PubXFloor', + 'fetchStatus': 'success' + }, + 'mediaTypes': { + 'banner': { + 'sizes': [ + [ + 300, + 250 + ] + ] + } + }, + 'adUnitCode': '/19968336/header-bid-tag-1', + 'transactionId': '41ec8eaf-3e7c-4a8b-8344-ab796ff6e294', + 'sizes': [ + [ + 300, + 250 + ] + ], + 'bidId': '248f9a4489835e', + 'bidderRequestId': '184cbc05bb90ba', + 'src': 'client', + 'bidRequestsCount': 1, + 'bidderRequestsCount': 1, + 'bidderWinsCount': 0 + }], + 'auctionStart': 1603865707180, + 'timeout': 1000, + 'refererInfo': { + 'referer': 'http://local-pnh.net:8080/stream/', + 'reachedTop': true, + 'isAmp': false, + 'numIframes': 0, + 'stack': [ + 'http://local-pnh.net:8080/stream/' + ], + 'canonicalUrl': null + }, + 'start': 1603865707182 + }], + 'noBids': [], + 'bidsReceived': [{ + 'bidderCode': 'appnexus', + 'width': 300, + 'height': 250, + 'statusMessage': 'Bid available', + 'adId': '32780c4bc382cb', + 'requestId': '248f9a4489835e', + 'mediaType': 'banner', + 'source': 'client', + 'cpm': 0.5, + 'creativeId': 96846035, + 'currency': 'USD', + 'netRevenue': true, + 'ttl': 300, + 'adUnitCode': '/19968336/header-bid-tag-1', + 'appnexus': { + 'buyerMemberId': 9325 + }, + 'meta': { + 'advertiserId': 2529885 + }, + 'ad': '', + 'originalCpm': 0.5, + 'originalCurrency': 'USD', + 'floorData': { + 'floorValue': 0.4, + 'floorRule': '/19968336/header-bid-tag-1|banner', + 'floorCurrency': 'USD', + 'cpmAfterAdjustments': 0.5, + 'enforcements': { + 'enforceJS': true, + 'enforcePBS': false, + 'floorDeals': true, + 'bidAdjustment': true + }, + 'matchedFields': { + 'gptSlot': '/19968336/header-bid-tag-1', + 'mediaType': 'banner' + } + }, + 'auctionId': 'bc3806e4-873e-453c-8ae5-204f35e923b4', + 'responseTimestamp': 1603865707449, + 'requestTimestamp': 1603865707182, + 'bidder': 'appnexus', + 'timeToRespond': 267, + 'pbLg': '0.50', + 'pbMg': '0.50', + 'pbHg': '0.50', + 'pbAg': '0.50', + 'pbDg': '0.50', + 'pbCg': '0.50', + 'size': '300x250', + 'adserverTargeting': { + 'hb_bidder': 'appnexus', + 'hb_adid': '32780c4bc382cb', + 'hb_pb': '0.50', + 'hb_size': '300x250', + 'hb_source': 'client', + 'hb_format': 'banner' + }, + 'status': 'rendered', + 'params': [{ + 'placementId': 13144370 + }] + }], + 'winningBids': [], + 'timeout': 1000 + }, + 'bidWon': { + 'bidderCode': 'appnexus', + 'width': 300, + 'height': 250, + 'statusMessage': 'Bid available', + 'adId': '32780c4bc382cb', + 'requestId': '248f9a4489835e', + 'mediaType': 'banner', + 'source': 'client', + 'cpm': 0.5, + 'creativeId': 96846035, + 'currency': 'USD', + 'netRevenue': true, + 'ttl': 300, + 'adUnitCode': '/19968336/header-bid-tag-1', + 'appnexus': { + 'buyerMemberId': 9325 + }, + 'meta': { + 'advertiserId': 2529885 + }, + 'ad': '', + 'originalCpm': 0.5, + 'originalCurrency': 'USD', + 'floorData': { + 'floorValue': 0.4, + 'floorRule': '/19968336/header-bid-tag-1|banner', + 'floorCurrency': 'USD', + 'cpmAfterAdjustments': 0.5, + 'enforcements': { + 'enforceJS': true, + 'enforcePBS': false, + 'floorDeals': true, + 'bidAdjustment': true + }, + 'matchedFields': { + 'gptSlot': '/19968336/header-bid-tag-1', + 'mediaType': 'banner' + } + }, + 'auctionId': 'bc3806e4-873e-453c-8ae5-204f35e923b4', + 'responseTimestamp': 1603865707449, + 'requestTimestamp': 1603865707182, + 'bidder': 'appnexus', + 'timeToRespond': 267, + 'pbLg': '0.50', + 'pbMg': '0.50', + 'pbHg': '0.50', + 'pbAg': '0.50', + 'pbDg': '0.50', + 'pbCg': '0.50', + 'size': '300x250', + 'adserverTargeting': { + 'hb_bidder': 'appnexus', + 'hb_adid': '32780c4bc382cb', + 'hb_pb': '0.50', + 'hb_size': '300x250', + 'hb_source': 'client', + 'hb_format': 'banner' + }, + 'status': 'rendered', + 'params': [{ + 'placementId': 13144370 + }] + } + }; + let location = utils.getWindowLocation(); + + let expectedAfterBid = { + 'bids': [{ + 'bidderCode': 'appnexus', + 'bidId': '248f9a4489835e', + 'adUnitCode': '/19968336/header-bid-tag-1', + 'requestId': '184cbc05bb90ba', + 'auctionId': 'bc3806e4-873e-453c-8ae5-204f35e923b4', + 'sizes': '300x250', + 'renderStatus': 2, + 'requestTimestamp': 1603865707182, + 'creativeId': 96846035, + 'currency': 'USD', + 'cpm': 0.5, + 'netRevenue': true, + 'mediaType': 'banner', + 'statusMessage': 'Bid available', + 'floorData': { + 'floorValue': 0.4, + 'floorRule': '/19968336/header-bid-tag-1|banner', + 'floorCurrency': 'USD', + 'cpmAfterAdjustments': 0.5, + 'enforcements': { + 'enforceJS': true, + 'enforcePBS': false, + 'floorDeals': true, + 'bidAdjustment': true + }, + 'matchedFields': { + 'gptSlot': '/19968336/header-bid-tag-1', + 'mediaType': 'banner' + } + }, + 'timeToRespond': 267, + 'responseTimestamp': 1603865707449, + 'platform': navigator.platform, + 'deviceType': getDeviceType() + }], + 'auctionInit': { + 'host': location.host, + 'path': location.pathname, + 'search': location.search, + 'auctionId': 'bc3806e4-873e-453c-8ae5-204f35e923b4', + 'timestamp': 1603865707180, + 'auctionStatus': 'inProgress', + 'adUnits': [{ + 'code': '/19968336/header-bid-tag-1', + 'mediaTypes': { + 'banner': { + 'sizes': [ + [ + 300, + 250 + ] + ] + } + }, + 'bids': [{ + 'bidder': 'appnexus', + 'params': { + 'placementId': 13144370 + }, + 'auctionId': 'bc3806e4-873e-453c-8ae5-204f35e923b4', + 'floorData': { + 'skipped': false, + 'skipRate': 0, + 'modelVersion': 'new model 1.0', + 'location': 'fetch', + 'floorProvider': 'PubXFloor', + 'fetchStatus': 'success' + } + }], + 'sizes': [ + [ + 300, + 250 + ] + ], + 'transactionId': '41ec8eaf-3e7c-4a8b-8344-ab796ff6e294' + }], + 'adUnitCodes': [ + '/19968336/header-bid-tag-1' + ], + 'bidderRequests': [{ + 'bidderCode': 'appnexus', + 'auctionId': 'bc3806e4-873e-453c-8ae5-204f35e923b4', + 'bidderRequestId': '184cbc05bb90ba', + 'bids': [{ + 'bidder': 'appnexus', + 'params': { + 'placementId': 13144370 + }, + 'auctionId': 'bc3806e4-873e-453c-8ae5-204f35e923b4', + 'floorData': { + 'skipped': false, + 'skipRate': 0, + 'modelVersion': 'new model 1.0', + 'location': 'fetch', + 'floorProvider': 'PubXFloor', + 'fetchStatus': 'success' + }, + 'mediaTypes': { + 'banner': { + 'sizes': [ + [ + 300, + 250 + ] + ] + } + }, + 'adUnitCode': '/19968336/header-bid-tag-1', + 'transactionId': '41ec8eaf-3e7c-4a8b-8344-ab796ff6e294', + 'sizes': [ + [ + 300, + 250 + ] + ], + 'bidId': '248f9a4489835e', + 'bidderRequestId': '184cbc05bb90ba', + 'src': 'client', + 'bidRequestsCount': 1, + 'bidderRequestsCount': 1, + 'bidderWinsCount': 0 + }], + 'auctionStart': 1603865707180, + 'timeout': 1000, + 'refererInfo': { + 'referer': 'http://local-pnh.net:8080/stream/', + 'reachedTop': true, + 'isAmp': false, + 'numIframes': 0, + 'stack': [ + 'http://local-pnh.net:8080/stream/' + ], + 'canonicalUrl': null + }, + 'start': 1603865707182 + }], + 'noBids': [], + 'bidsReceived': [], + 'winningBids': [], + 'timeout': 1000, + 'config': { + 'samplingRate': '1', + 'pubxId': '6c415fc0-8b0e-4cf5-be73-01526a4db625' + } + }, + 'initOptions': initOptions + }; + + let expectedAfterBidWon = { + 'winningBid': { + 'bidderCode': 'appnexus', + 'bidId': '248f9a4489835e', + 'adUnitCode': '/19968336/header-bid-tag-1', + 'auctionId': 'bc3806e4-873e-453c-8ae5-204f35e923b4', + 'renderedSize': '300x250', + 'renderStatus': 4, + 'requestTimestamp': 1603865707182, + 'creativeId': 96846035, + 'currency': 'USD', + 'cpm': 0.5, + 'netRevenue': true, + 'mediaType': 'banner', + 'status': 'rendered', + 'statusMessage': 'Bid available', + 'floorData': { + 'floorValue': 0.4, + 'floorRule': '/19968336/header-bid-tag-1|banner', + 'floorCurrency': 'USD', + 'cpmAfterAdjustments': 0.5, + 'enforcements': { + 'enforceJS': true, + 'enforcePBS': false, + 'floorDeals': true, + 'bidAdjustment': true + }, + 'matchedFields': { + 'gptSlot': '/19968336/header-bid-tag-1', + 'mediaType': 'banner' + } + }, + 'timeToRespond': 267, + 'responseTimestamp': 1603865707449, + 'platform': navigator.platform, + 'deviceType': getDeviceType() + }, + 'initOptions': initOptions + } + + adapterManager.registerAnalyticsAdapter({ + code: 'pubxai', + adapter: pubxaiAnalyticsAdapter + }); + + beforeEach(function() { + adapterManager.enableAnalytics({ + provider: 'pubxai', + options: initOptions + }); + }); + + afterEach(function() { + pubxaiAnalyticsAdapter.disableAnalytics(); + }); + + it('builds and sends auction data', function() { + // Step 1: Send auction init event + events.emit(constants.EVENTS.AUCTION_INIT, prebidEvent['auctionInit']); + + // Step 2: Send bid requested event + events.emit(constants.EVENTS.BID_REQUESTED, prebidEvent['bidRequested']); + + // Step 3: Send bid response event + events.emit(constants.EVENTS.BID_RESPONSE, prebidEvent['bidResponse']); + + // Step 4: Send bid time out event + events.emit(constants.EVENTS.BID_TIMEOUT, prebidEvent['bidTimeout']); + + // Step 5: Send auction end event + events.emit(constants.EVENTS.AUCTION_END, prebidEvent['auctionEnd']); + + expect(server.requests.length).to.equal(1); + + let realAfterBid = JSON.parse(server.requests[0].requestBody); + + expect(realAfterBid).to.deep.equal(expectedAfterBid); + + // Step 6: Send auction bid won event + events.emit(constants.EVENTS.BID_WON, prebidEvent['bidWon']); + + expect(server.requests.length).to.equal(2); + + let winEventData = JSON.parse(server.requests[1].requestBody); + + expect(winEventData).to.deep.equal(expectedAfterBidWon); + }); + }); +}); diff --git a/test/spec/modules/pulsepointBidAdapter_spec.js b/test/spec/modules/pulsepointBidAdapter_spec.js index d71bd018ab3..c3830d5cb46 100644 --- a/test/spec/modules/pulsepointBidAdapter_spec.js +++ b/test/spec/modules/pulsepointBidAdapter_spec.js @@ -19,6 +19,11 @@ describe('PulsePoint Adapter Tests', function () { } }, { placementCode: '/DfpAccount2/slot2', + mediaTypes: { + banner: { + sizes: [[728, 90]] + } + }, bidId: 'bid23456', params: { cp: 'p10000', @@ -72,6 +77,11 @@ describe('PulsePoint Adapter Tests', function () { }]; const additionalParamsConfig = [{ placementCode: '/DfpAccount1/slot1', + mediaTypes: { + banner: { + sizes: [[1, 1]] + } + }, bidId: 'bid12345', params: { cp: 'p10000', @@ -89,6 +99,11 @@ describe('PulsePoint Adapter Tests', function () { const ortbParamsSlotConfig = [{ placementCode: '/DfpAccount1/slot1', + mediaTypes: { + banner: { + sizes: [[1, 1]] + } + }, bidId: 'bid12345', params: { cp: 'p10000', @@ -146,6 +161,11 @@ describe('PulsePoint Adapter Tests', function () { const schainParamsSlotConfig = [{ placementCode: '/DfpAccount1/slot1', + mediaTypes: { + banner: { + sizes: [[1, 1]] + } + }, bidId: 'bid12345', params: { cp: 'p10000', @@ -630,7 +650,7 @@ describe('PulsePoint Adapter Tests', function () { britepoolid: 'britepool_id123', criteoId: 'criteo_id234', idl_env: 'idl_id123', - id5id: 'id5id_234', + id5id: { uid: 'id5id_234' }, parrableId: { eid: 'parrable_id234' }, lipb: { lipbid: 'liveintent_id123' @@ -681,7 +701,10 @@ describe('PulsePoint Adapter Tests', function () { expect(ortbRequest.imp[1].banner).to.not.be.null; expect(ortbRequest.imp[1].banner.w).to.equal(728); expect(ortbRequest.imp[1].banner.h).to.equal(90); - expect(ortbRequest.imp[1].banner.format).to.be.null; + expect(ortbRequest.imp[1].banner.format).to.not.be.null; + expect(ortbRequest.imp[1].banner.format).to.have.lengthOf(1); + expect(ortbRequest.imp[1].banner.format[0].w).to.equal(728); + expect(ortbRequest.imp[1].banner.format[0].h).to.equal(90); // adsize on response const ortbResponse = { seatbid: [{ @@ -701,4 +724,58 @@ describe('PulsePoint Adapter Tests', function () { expect(bid.width).to.equal(728); expect(bid.height).to.equal(90); }); + it('Verify multi-format response', function () { + const bidRequests = deepClone(slotConfigs); + bidRequests[0].mediaTypes['native'] = { + title: { + required: true + }, + image: { + required: true + }, + sponsoredBy: { + required: true + } + }; + bidRequests[1].params.video = { + w: 400, + h: 300, + minduration: 5, + maxduration: 10, + }; + const request = spec.buildRequests(bidRequests, bidderRequest); + expect(request).to.be.not.null; + expect(request.data).to.be.not.null; + const ortbRequest = request.data; + expect(ortbRequest.imp).to.have.lengthOf(2); + // adsize on response + const ortbResponse = { + seatbid: [{ + bid: [{ + impid: ortbRequest.imp[0].id, + price: 1.25, + adm: 'This is an Ad', + crid: 'Creative#123', + w: 728, + h: 90 + }, { + impid: ortbRequest.imp[1].id, + price: 2.5, + adm: '', + crid: 'Creative#234', + w: 728, + h: 90 + }] + }] + }; + // request has both types - banner and native, response is parsed as banner. + // for impression#2, response is parsed as video + const bids = spec.interpretResponse({ body: ortbResponse }, request); + expect(bids).to.have.lengthOf(2); + const bid = bids[0]; + expect(bid.width).to.equal(728); + expect(bid.height).to.equal(90); + const secondBid = bids[1]; + expect(secondBid.vastXml).to.equal(''); + }); }); diff --git a/test/spec/modules/quantcastBidAdapter_spec.js b/test/spec/modules/quantcastBidAdapter_spec.js index caa554c8cd8..5b4e7963e60 100644 --- a/test/spec/modules/quantcastBidAdapter_spec.js +++ b/test/spec/modules/quantcastBidAdapter_spec.js @@ -7,7 +7,8 @@ import { QUANTCAST_TEST_PUBLISHER, QUANTCAST_PROTOCOL, QUANTCAST_PORT, - spec as qcSpec + spec as qcSpec, + storage } from '../../../modules/quantcastBidAdapter.js'; import { newBidder } from '../../../src/adapters/bidderFactory.js'; import { parseUrl } from 'src/utils.js'; @@ -42,6 +43,8 @@ describe('Quantcast adapter', function () { canonicalUrl: 'http://example.com/hello.html' } }; + + storage.setCookie('__qca', '', 'Thu, 01 Jan 1970 00:00:00 GMT'); }); function setupVideoBidRequest(videoParams) { @@ -140,7 +143,8 @@ describe('Quantcast adapter', function () { gdprSignal: 0, uspSignal: 0, coppa: 0, - prebidJsVersion: '$prebid.version$' + prebidJsVersion: '$prebid.version$', + fpa: '' }; it('sends banner bid requests contains all the required parameters', function () { @@ -208,7 +212,8 @@ describe('Quantcast adapter', function () { gdprSignal: 0, uspSignal: 0, coppa: 0, - prebidJsVersion: '$prebid.version$' + prebidJsVersion: '$prebid.version$', + fpa: '' }; expect(requests[0].data).to.equal(JSON.stringify(expectedVideoBidRequest)); @@ -244,7 +249,8 @@ describe('Quantcast adapter', function () { gdprSignal: 0, uspSignal: 0, coppa: 0, - prebidJsVersion: '$prebid.version$' + prebidJsVersion: '$prebid.version$', + fpa: '' }; expect(requests[0].data).to.equal(JSON.stringify(expectedVideoBidRequest)); @@ -276,7 +282,8 @@ describe('Quantcast adapter', function () { gdprSignal: 0, uspSignal: 0, coppa: 0, - prebidJsVersion: '$prebid.version$' + prebidJsVersion: '$prebid.version$', + fpa: '' }; expect(requests[0].data).to.equal(JSON.stringify(expectedVideoBidRequest)); @@ -340,7 +347,8 @@ describe('Quantcast adapter', function () { gdprSignal: 0, uspSignal: 0, coppa: 0, - prebidJsVersion: '$prebid.version$' + prebidJsVersion: '$prebid.version$', + fpa: '' }; expect(requests[0].data).to.equal(JSON.stringify(expectedBidRequest)); @@ -584,6 +592,13 @@ describe('Quantcast adapter', function () { expect(parsed.uspConsent).to.equal('consentString'); }); + it('propagates Quantcast first-party cookie (fpa)', function() { + storage.setCookie('__qca', 'P0-TestFPA'); + const requests = qcSpec.buildRequests([bidRequest], bidderRequest); + const parsed = JSON.parse(requests[0].data); + expect(parsed.fpa).to.equal('P0-TestFPA'); + }); + describe('propagates coppa', function() { let sandbox; beforeEach(() => { diff --git a/test/spec/modules/qwarryBidAdapter_spec.js b/test/spec/modules/qwarryBidAdapter_spec.js new file mode 100644 index 00000000000..91e3cf4bfdf --- /dev/null +++ b/test/spec/modules/qwarryBidAdapter_spec.js @@ -0,0 +1,136 @@ +import { expect } from 'chai' +import { ENDPOINT, spec } from 'modules/qwarryBidAdapter.js' +import { newBidder } from 'src/adapters/bidderFactory.js' + +const REQUEST = { + 'bidId': '456', + 'bidder': 'qwarry', + 'params': { + zoneToken: 'e64782a4-8e68-4c38-965b-80ccf115d46f', + pos: 7 + } +} + +const BIDDER_BANNER_RESPONSE = { + 'prebidResponse': [{ + 'ad': '
test
', + 'requestId': 'e64782a4-8e68-4c38-965b-80ccf115d46d', + 'cpm': 900.5, + 'currency': 'USD', + 'width': 640, + 'height': 480, + 'ttl': 300, + 'creativeId': 1, + 'netRevenue': true, + 'winUrl': 'http://test.com', + 'format': 'banner' + }] +} + +const BIDDER_VIDEO_RESPONSE = { + 'prebidResponse': [{ + 'ad': 'vast', + 'requestId': 'e64782a4-8e68-4c38-965b-80ccf115d46z', + 'cpm': 800.4, + 'currency': 'USD', + 'width': 1024, + 'height': 768, + 'ttl': 200, + 'creativeId': 2, + 'netRevenue': true, + 'winUrl': 'http://test.com', + 'format': 'video' + }] +} + +const BIDDER_NO_BID_RESPONSE = '' + +describe('qwarryBidAdapter', function () { + const adapter = newBidder(spec) + + describe('inherited functions', function () { + it('exists and is a function', function () { + expect(adapter.callBids).to.exist.and.to.be.a('function') + }) + }) + + describe('isBidRequestValid', function () { + it('should return true when required params found', function () { + expect(spec.isBidRequestValid(REQUEST)).to.equal(true) + }) + + it('should return false when required params are not passed', function () { + let bid = Object.assign({}, REQUEST) + delete bid.params.zoneToken + expect(spec.isBidRequestValid(bid)).to.equal(false) + delete bid.params + expect(spec.isBidRequestValid(bid)).to.equal(false) + }) + }) + + describe('buildRequests', function () { + let bidRequests = [REQUEST] + const bidderRequest = spec.buildRequests(bidRequests, { bidderRequestId: '123' }) + + it('sends bid request to ENDPOINT via POST', function () { + expect(bidderRequest.method).to.equal('POST') + expect(bidderRequest.data.requestId).to.equal('123') + expect(bidderRequest.data.bids).to.deep.contains({ bidId: '456', zoneToken: 'e64782a4-8e68-4c38-965b-80ccf115d46f', pos: 7 }) + expect(bidderRequest.options.customHeaders).to.deep.equal({ 'Rtb-Direct': true }) + expect(bidderRequest.options.contentType).to.equal('application/json') + expect(bidderRequest.url).to.equal(ENDPOINT) + }) + }) + + describe('interpretResponse', function () { + it('handles banner request : should get correct bid response', function () { + const result = spec.interpretResponse({ body: BIDDER_BANNER_RESPONSE }, {}) + + expect(result[0]).to.have.property('ad').equal('
test
') + expect(result[0]).to.have.property('requestId').equal('e64782a4-8e68-4c38-965b-80ccf115d46d') + expect(result[0]).to.have.property('cpm').equal(900.5) + expect(result[0]).to.have.property('currency').equal('USD') + expect(result[0]).to.have.property('width').equal(640) + expect(result[0]).to.have.property('height').equal(480) + expect(result[0]).to.have.property('ttl').equal(300) + expect(result[0]).to.have.property('creativeId').equal(1) + expect(result[0]).to.have.property('netRevenue').equal(true) + expect(result[0]).to.have.property('winUrl').equal('http://test.com') + expect(result[0]).to.have.property('format').equal('banner') + }) + + it('handles video request : should get correct bid response', function () { + const result = spec.interpretResponse({ body: BIDDER_VIDEO_RESPONSE }, {}) + + expect(result[0]).to.have.property('ad').equal('vast') + expect(result[0]).to.have.property('requestId').equal('e64782a4-8e68-4c38-965b-80ccf115d46z') + expect(result[0]).to.have.property('cpm').equal(800.4) + expect(result[0]).to.have.property('currency').equal('USD') + expect(result[0]).to.have.property('width').equal(1024) + expect(result[0]).to.have.property('height').equal(768) + expect(result[0]).to.have.property('ttl').equal(200) + expect(result[0]).to.have.property('creativeId').equal(2) + expect(result[0]).to.have.property('netRevenue').equal(true) + expect(result[0]).to.have.property('winUrl').equal('http://test.com') + expect(result[0]).to.have.property('format').equal('video') + expect(result[0]).to.have.property('vastXml').equal('vast') + }) + + it('handles no bid response : should get empty array', function () { + let result = spec.interpretResponse({ body: undefined }, {}) + expect(result).to.deep.equal([]) + + result = spec.interpretResponse({ body: BIDDER_NO_BID_RESPONSE }, {}) + expect(result).to.deep.equal([]) + }) + }) + + describe('onBidWon', function () { + it('handles banner win: should get true', function () { + const win = BIDDER_BANNER_RESPONSE.prebidResponse[0] + const bidWonResult = spec.onBidWon(win) + + expect(bidWonResult).to.equal(true) + }) + }) +}) diff --git a/test/spec/modules/realTimeDataModule_spec.js b/test/spec/modules/realTimeDataModule_spec.js new file mode 100644 index 00000000000..b84aef15feb --- /dev/null +++ b/test/spec/modules/realTimeDataModule_spec.js @@ -0,0 +1,160 @@ +import * as rtdModule from 'modules/rtdModule/index.js'; +import { config } from 'src/config.js'; +import * as sinon from 'sinon'; + +const getBidRequestDataSpy = sinon.spy(); + +const validSM = { + name: 'validSM', + init: () => { return true }, + getTargetingData: (adUnitsCodes) => { + return {'ad2': {'key': 'validSM'}} + }, + getBidRequestData: getBidRequestDataSpy +}; + +const validSMWait = { + name: 'validSMWait', + init: () => { return true }, + getTargetingData: (adUnitsCodes) => { + return {'ad1': {'key': 'validSMWait'}} + }, + getBidRequestData: getBidRequestDataSpy +}; + +const invalidSM = { + name: 'invalidSM' +}; + +const failureSM = { + name: 'failureSM', + init: () => { return false } +}; + +const nonConfSM = { + name: 'nonConfSM', + init: () => { return true } +}; + +const conf = { + 'realTimeData': { + 'auctionDelay': 100, + dataProviders: [ + { + 'name': 'validSMWait', + 'waitForIt': true, + }, + { + 'name': 'validSM', + 'waitForIt': false, + }, + { + 'name': 'invalidSM' + }, + { + 'name': 'failureSM' + }] + } +}; + +describe('Real time module', function () { + before(function () { + rtdModule.attachRealTimeDataProvider(validSM); + rtdModule.attachRealTimeDataProvider(invalidSM); + rtdModule.attachRealTimeDataProvider(failureSM); + rtdModule.attachRealTimeDataProvider(nonConfSM); + rtdModule.attachRealTimeDataProvider(validSMWait); + }); + + after(function () { + config.resetConfig(); + }); + + beforeEach(function () { + config.setConfig(conf); + }); + + it('should use only valid modules', function () { + rtdModule.init(config); + expect(rtdModule.subModules).to.eql([validSMWait, validSM]); + }); + + it('should be able to modify bid request', function (done) { + rtdModule.setBidRequestsData(() => { + assert(getBidRequestDataSpy.calledTwice); + assert(getBidRequestDataSpy.calledWith({bidRequest: {}})); + done(); + }, {bidRequest: {}}) + }); + + it('deep merge object', function () { + const obj1 = { + id1: { + key: 'value', + key2: 'value2' + }, + id2: { + k: 'v' + } + }; + const obj2 = { + id1: { + key3: 'value3' + } + }; + const obj3 = { + id3: { + key: 'value' + } + }; + const expected = { + id1: { + key: 'value', + key2: 'value2', + key3: 'value3' + }, + id2: { + k: 'v' + }, + id3: { + key: 'value' + } + }; + + const merged = rtdModule.deepMerge([obj1, obj2, obj3]); + assert.deepEqual(expected, merged); + }); + + it('sould place targeting on adUnits', function (done) { + const auction = { + adUnitCodes: ['ad1', 'ad2'], + adUnits: [ + { + code: 'ad1' + }, + { + code: 'ad2', + adserverTargeting: {preKey: 'preValue'} + } + ] + }; + + const expectedAdUnits = [ + { + code: 'ad1', + adserverTargeting: {key: 'validSMWait'} + }, + { + code: 'ad2', + adserverTargeting: { + preKey: 'preValue', + key: 'validSM' + } + } + ]; + + const adUnits = rtdModule.getAdUnitTargeting(auction); + assert.deepEqual(expectedAdUnits, adUnits) + done(); + }) +}); diff --git a/test/spec/modules/realTimeModule_spec.js b/test/spec/modules/realTimeModule_spec.js deleted file mode 100644 index f47068724d1..00000000000 --- a/test/spec/modules/realTimeModule_spec.js +++ /dev/null @@ -1,274 +0,0 @@ -import * as rtdModule from 'modules/rtdModule/index.js'; -import { config } from 'src/config.js'; -import {makeSlot} from '../integration/faker/googletag.js'; -import * as browsiRTD from '../../../modules/browsiRtdProvider.js'; - -const validSM = { - name: 'validSM', - init: () => { return true }, - getData: (adUnits, onDone) => { - setTimeout(() => { - return onDone({'key': 'validSM'}) - }, 500) - } -}; - -const validSMWait = { - name: 'validSMWait', - init: () => { return true }, - getData: (adUnits, onDone) => { - setTimeout(() => { - return onDone({'ad1': {'key': 'validSMWait'}}) - }, 50) - } -}; - -const invalidSM = { - name: 'invalidSM' -}; - -const failureSM = { - name: 'failureSM', - init: () => { return false } -}; - -const nonConfSM = { - name: 'nonConfSM', - init: () => { return true } -}; - -const conf = { - 'realTimeData': { - 'auctionDelay': 250, - dataProviders: [ - { - 'name': 'validSMWait', - 'waitForIt': true, - }, - { - 'name': 'validSM', - 'waitForIt': false, - }, - { - 'name': 'invalidSM' - }, - { - 'name': 'failureSM' - }] - } -}; - -function getAdUnitMock(code = 'adUnit-code') { - return { - code, - mediaTypes: { banner: {}, native: {} }, - sizes: [[300, 200], [300, 600]], - bids: [{ bidder: 'sampleBidder', params: { placementId: 'banner-only-bidder' } }] - }; -} - -describe('Real time module', function () { - after(function () { - config.resetConfig(); - }); - - beforeEach(function () { - config.setConfig(conf); - }); - - it('should use only valid modules', function (done) { - rtdModule.attachRealTimeDataProvider(validSM); - rtdModule.attachRealTimeDataProvider(invalidSM); - rtdModule.attachRealTimeDataProvider(failureSM); - rtdModule.attachRealTimeDataProvider(nonConfSM); - rtdModule.attachRealTimeDataProvider(validSMWait); - rtdModule.initSubModules(afterInitSubModules); - function afterInitSubModules() { - expect(rtdModule.subModules).to.eql([validSMWait, validSM]); - done(); - } - rtdModule.init(config); - }); - - it('should only wait for must have sub modules', function (done) { - rtdModule.getProviderData([], (data) => { - expect(data).to.eql({validSMWait: {'ad1': {'key': 'validSMWait'}}}); - done(); - }) - }); - - it('deep merge object', function () { - const obj1 = { - id1: { - key: 'value', - key2: 'value2' - }, - id2: { - k: 'v' - } - }; - const obj2 = { - id1: { - key3: 'value3' - } - }; - const obj3 = { - id3: { - key: 'value' - } - }; - const expected = { - id1: { - key: 'value', - key2: 'value2', - key3: 'value3' - }, - id2: { - k: 'v' - }, - id3: { - key: 'value' - } - }; - - const merged = rtdModule.deepMerge([obj1, obj2, obj3]); - assert.deepEqual(expected, merged); - }); - - it('check module using bidsBackCallback', function (done) { - // set slot - const slot = makeSlot({ code: '/code1', divId: 'ad1' }); - window.googletag.pubads().setSlots([slot]); - - function afterBidHook() { - expect(slot.getTargeting().length).to.equal(1); - expect(slot.getTargeting()[0].key).to.equal('validSMWait'); - done(); - } - rtdModule.setTargetsAfterRequestBids(afterBidHook, []); - }); - - it('check module using requestBidsHook', function (done) { - // set slot - const slotsB = makeSlot({ code: '/code1', divId: 'ad1' }); - window.googletag.pubads().setSlots([slotsB]); - let adUnits = [getAdUnitMock('ad1')]; - - function afterBidHook(data) { - expect(slotsB.getTargeting().length).to.equal(1); - expect(slotsB.getTargeting()[0].key).to.equal('validSMWait'); - - data.adUnits.forEach(unit => { - unit.bids.forEach(bid => { - expect(bid.realTimeData).to.have.property('key'); - expect(bid.realTimeData.key).to.equal('validSMWait'); - }); - }); - done(); - } - rtdModule.requestBidsHook(afterBidHook, { adUnits: adUnits }); - }); -}); - -describe('browsi Real time data sub module', function () { - const conf = { - 'realTimeData': { - 'auctionDelay': 250, - dataProviders: [{ - 'name': 'browsi', - 'params': { - 'url': 'testUrl.com', - 'siteKey': 'testKey', - 'pubKey': 'testPub', - 'keyName': 'bv' - } - }] - } - }; - - beforeEach(function () { - config.setConfig(conf); - }); - - after(function () { - config.resetConfig(); - }); - - it('should init and return true', function () { - browsiRTD.beforeInit(config); - expect(browsiRTD.browsiSubmodule.init()).to.equal(true) - }); - - it('should create browsi script', function () { - const script = browsiRTD.addBrowsiTag('scriptUrl.com'); - expect(script.getAttribute('data-sitekey')).to.equal('testKey'); - expect(script.getAttribute('data-pubkey')).to.equal('testPub'); - expect(script.async).to.equal(true); - expect(script.prebidData.kn).to.equal(conf.realTimeData.dataProviders[0].params.keyName); - }); - - it('should match placement with ad unit', function () { - const slot = makeSlot({ code: '/57778053/Browsi_Demo_300x250', divId: 'browsiAd_1' }); - - const test1 = browsiRTD.isIdMatchingAdUnit(slot, ['/57778053/Browsi_Demo_300x250']); // true - const test2 = browsiRTD.isIdMatchingAdUnit(slot, ['/57778053/Browsi_Demo_300x250', '/57778053/Browsi']); // true - const test3 = browsiRTD.isIdMatchingAdUnit(slot, ['/57778053/Browsi_Demo_Low']); // false - const test4 = browsiRTD.isIdMatchingAdUnit(slot, []); // true - - expect(test1).to.equal(true); - expect(test2).to.equal(true); - expect(test3).to.equal(false); - expect(test4).to.equal(true); - }); - - it('should return correct macro values', function () { - const slot = makeSlot({ code: '/57778053/Browsi_Demo_300x250', divId: 'browsiAd_1' }); - - slot.setTargeting('test', ['test', 'value']); - // slot getTargeting doesn't act like GPT so we can't expect real value - const macroResult = browsiRTD.getMacroId({p: '/'}, slot); - expect(macroResult).to.equal('/57778053/Browsi_Demo_300x250/NA'); - - const macroResultB = browsiRTD.getMacroId({}, slot); - expect(macroResultB).to.equal('browsiAd_1'); - - const macroResultC = browsiRTD.getMacroId({p: '', s: {s: 0, e: 1}}, slot); - expect(macroResultC).to.equal('/'); - }); - - describe('should return data to RTD module', function () { - it('should return empty if no ad units defined', function (done) { - browsiRTD.setData({}); - browsiRTD.browsiSubmodule.getData([], onDone); - function onDone(data) { - expect(data).to.eql({}); - done(); - } - }); - - it('should return NA if no prediction for ad unit', function (done) { - const adUnits = [getAdUnitMock('adMock')]; - browsiRTD.setData({}); - browsiRTD.browsiSubmodule.getData(adUnits, onDone); - function onDone(data) { - expect(data).to.eql({adMock: {bv: 'NA'}}); - done(); - } - }); - - it('should return prediction from server', function (done) { - const adUnits = [getAdUnitMock('hasPrediction')]; - const data = { - p: {'hasPrediction': {p: 0.234}}, - kn: 'bv', - pmd: undefined - }; - browsiRTD.setData(data); - browsiRTD.browsiSubmodule.getData(adUnits, onDone); - function onDone(data) { - expect(data).to.eql({hasPrediction: {bv: '0.20'}}); - done(); - } - }) - }) -}); diff --git a/test/spec/modules/reconciliationRtdProvider_spec.js b/test/spec/modules/reconciliationRtdProvider_spec.js new file mode 100644 index 00000000000..6efe55ddf46 --- /dev/null +++ b/test/spec/modules/reconciliationRtdProvider_spec.js @@ -0,0 +1,221 @@ +import { + reconciliationSubmodule, + track, + getTopIFrameWin, + getSlotByWin +} from 'modules/reconciliationRtdProvider.js'; +import { makeSlot } from '../integration/faker/googletag.js'; +import * as utils from 'src/utils.js'; + +describe('Reconciliation Real time data submodule', function () { + const conf = { + dataProviders: [{ + 'name': 'reconciliation', + 'params': { + 'publisherMemberId': 'test_prebid_publisher' + }, + }] + }; + + let trackPostStub; + + beforeEach(function () { + trackPostStub = sinon.stub(track, 'trackPost'); + }); + + afterEach(function () { + trackPostStub.restore(); + }); + + describe('reconciliationSubmodule', function () { + describe('initialization', function () { + let utilsLogErrorSpy; + + before(function () { + utilsLogErrorSpy = sinon.spy(utils, 'logError'); + }); + + after(function () { + utils.logError.restore(); + }); + + it('successfully instantiates', function () { + expect(reconciliationSubmodule.init(conf.dataProviders[0])).to.equal(true); + }); + + it('should log error if initializied without parameters', function () { + expect(reconciliationSubmodule.init({'name': 'reconciliation', 'params': {}})).to.equal(true); + expect(utilsLogErrorSpy.calledOnce).to.be.true; + }); + }); + + describe('getData', function () { + it('should return data in proper format', function () { + makeSlot({code: '/reconciliationAdunit1', divId: 'reconciliationAd1'}); + + const targetingData = reconciliationSubmodule.getTargetingData(['/reconciliationAdunit1']); + expect(targetingData['/reconciliationAdunit1'].RSDK_AUID).to.eql('/reconciliationAdunit1'); + expect(targetingData['/reconciliationAdunit1'].RSDK_ADID).to.be.a('string'); + }); + + it('should return unit path if called with divId', function () { + makeSlot({code: '/reconciliationAdunit2', divId: 'reconciliationAd2'}); + + const targetingData = reconciliationSubmodule.getTargetingData(['reconciliationAd2']); + expect(targetingData['reconciliationAd2'].RSDK_AUID).to.eql('/reconciliationAdunit2'); + expect(targetingData['reconciliationAd2'].RSDK_ADID).to.be.a('string'); + }); + + it('should skip empty adUnit id', function () { + makeSlot({code: '/reconciliationAdunit3', divId: 'reconciliationAd3'}); + + const targetingData = reconciliationSubmodule.getTargetingData(['reconciliationAd3', '']); + expect(targetingData).to.have.all.keys('reconciliationAd3'); + }); + }); + + describe('track events', function () { + it('should track init event with data', function () { + const adUnit = { + code: '/adunit' + }; + + reconciliationSubmodule.getTargetingData([adUnit.code]); + + expect(trackPostStub.calledOnce).to.be.true; + expect(trackPostStub.getCalls()[0].args[0]).to.eql('https://confirm.fiduciadlt.com/init'); + expect(trackPostStub.getCalls()[0].args[1].adUnits[0].adUnitId).to.eql(adUnit.code); + expect(trackPostStub.getCalls()[0].args[1].adUnits[0].adDeliveryId).be.a('string'); + expect(trackPostStub.getCalls()[0].args[1].publisherMemberId).to.eql('test_prebid_publisher'); + }); + }); + + describe('get topmost iframe', function () { + /** + * - top + * -- iframe.window <-- top iframe window + * --- iframe.window + * ---- iframe.window <-- win + */ + const mockFrameWin = (topWin, parentWin) => { + return { + top: topWin, + parent: parentWin + } + } + + it('should return null if called with null', function() { + expect(getTopIFrameWin(null)).to.be.null; + }); + + it('should return null if there is an error in frames chain', function() { + const topWin = {}; + const iframe1Win = mockFrameWin(topWin, null); // break chain + const iframe2Win = mockFrameWin(topWin, iframe1Win); + + expect(getTopIFrameWin(iframe1Win, topWin)).to.be.null; + }); + + it('should get the topmost iframe', function () { + const topWin = {}; + const iframe1Win = mockFrameWin(topWin, topWin); + const iframe2Win = mockFrameWin(topWin, iframe1Win); + + expect(getTopIFrameWin(iframe2Win, topWin)).to.eql(iframe1Win); + }); + }); + + describe('get slot by nested iframe window', function () { + it('should return the slot', function () { + const adSlotElement = document.createElement('div'); + const adSlotIframe = document.createElement('iframe'); + + adSlotElement.id = 'reconciliationAd'; + adSlotElement.appendChild(adSlotIframe); + document.body.appendChild(adSlotElement); + + const adSlot = makeSlot({code: '/reconciliationAdunit', divId: adSlotElement.id}); + + expect(getSlotByWin(adSlotIframe.contentWindow)).to.eql(adSlot); + }); + + it('should return null if the slot is not found', function () { + const adSlotElement = document.createElement('div'); + const adSlotIframe = document.createElement('iframe'); + + adSlotElement.id = 'reconciliationAd'; + document.body.appendChild(adSlotElement); + document.body.appendChild(adSlotIframe); // iframe is not in ad slot + + const adSlot = makeSlot({code: '/reconciliationAdunit', divId: adSlotElement.id}); + + expect(getSlotByWin(adSlotIframe.contentWindow)).to.be.null; + }); + }); + + describe('handle postMessage from Reconciliation Tag in ad iframe', function () { + it('should track impression pixel with parameters', function (done) { + const adSlotElement = document.createElement('div'); + const adSlotIframe = document.createElement('iframe'); + + adSlotElement.id = 'reconciliationAdMessage'; + adSlotElement.appendChild(adSlotIframe); + document.body.appendChild(adSlotElement); + + const adSlot = makeSlot({code: '/reconciliationAdunit', divId: adSlotElement.id}); + // Fix targeting methods + adSlot.targeting = {}; + adSlot.setTargeting = function(key, value) { + this.targeting[key] = [value]; + }; + adSlot.getTargeting = function(key) { + return this.targeting[key]; + }; + + adSlot.setTargeting('RSDK_AUID', '/reconciliationAdunit'); + adSlot.setTargeting('RSDK_ADID', '12345'); + adSlotIframe.contentDocument.open(); + adSlotIframe.contentDocument.write(``); + adSlotIframe.contentDocument.close(); + + setTimeout(() => { + expect(trackPostStub.calledOnce).to.be.true; + expect(trackPostStub.getCalls()[0].args[0]).to.eql('https://confirm.fiduciadlt.com/pimp'); + expect(trackPostStub.getCalls()[0].args[1].adUnitId).to.eql('/reconciliationAdunit'); + expect(trackPostStub.getCalls()[0].args[1].adDeliveryId).to.eql('12345'); + expect(trackPostStub.getCalls()[0].args[1].tagOwnerMemberId).to.eql('test_member_id'); ; + expect(trackPostStub.getCalls()[0].args[1].dataSources.length).to.eql(1); + expect(trackPostStub.getCalls()[0].args[1].dataRecipients.length).to.eql(2); + expect(trackPostStub.getCalls()[0].args[1].publisherMemberId).to.eql('test_prebid_publisher'); + done(); + }, 100); + }); + }); + }); +}); diff --git a/test/spec/modules/relaidoBidAdapter_spec.js b/test/spec/modules/relaidoBidAdapter_spec.js index cd4918460db..ebc62752f16 100644 --- a/test/spec/modules/relaidoBidAdapter_spec.js +++ b/test/spec/modules/relaidoBidAdapter_spec.js @@ -1,16 +1,20 @@ import { expect } from 'chai'; import { spec } from 'modules/relaidoBidAdapter.js'; import * as utils from 'src/utils.js'; +import { getStorageManager } from '../../../src/storageManager.js'; const UUID_KEY = 'relaido_uuid'; const DEFAULT_USER_AGENT = window.navigator.userAgent; const MOBILE_USER_AGENT = 'Mozilla/5.0 (iPhone; CPU iPhone OS 13_3_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0.5 Mobile/15E148 Safari/604.1'; +const relaido_uuid = 'hogehoge'; const setUADefault = () => { window.navigator.__defineGetter__('userAgent', function () { return DEFAULT_USER_AGENT }) }; const setUAMobile = () => { window.navigator.__defineGetter__('userAgent', function () { return MOBILE_USER_AGENT }) }; +const storage = getStorageManager(); +storage.setCookie(UUID_KEY, relaido_uuid); + describe('RelaidoAdapter', function () { - const relaido_uuid = 'hogehoge'; let bidRequest; let bidderRequest; let serverResponse; @@ -65,7 +69,6 @@ describe('RelaidoAdapter', function () { height: bidRequest.mediaTypes.video.playerSize[0][1], mediaType: 'video', }; - localStorage.setItem(UUID_KEY, relaido_uuid); }); describe('spec.isBidRequestValid', function () { @@ -86,10 +89,58 @@ describe('RelaidoAdapter', function () { setUADefault(); }); - it('should return false when the uuid are missing', function () { - localStorage.removeItem(UUID_KEY); - const result = !!(utils.isSafariBrowser()); - expect(spec.isBidRequestValid(bidRequest)).to.equal(result); + it('should return false when missing 300x250 over and 1x1 by banner', function () { + setUAMobile(); + bidRequest.mediaTypes = { + banner: { + sizes: [ + [100, 100], + [300, 100] + ] + } + }; + expect(spec.isBidRequestValid(bidRequest)).to.equal(false); + setUADefault(); + }); + + it('should return true when 300x250 by banner', function () { + setUAMobile(); + bidRequest.mediaTypes = { + banner: { + sizes: [ + [300, 250] + ] + } + }; + expect(spec.isBidRequestValid(bidRequest)).to.equal(true); + setUADefault(); + }); + + it('should return true when 1x1 by banner', function () { + setUAMobile(); + bidRequest.mediaTypes = { + banner: { + sizes: [ + [1, 1] + ] + } + }; + expect(spec.isBidRequestValid(bidRequest)).to.equal(true); + setUADefault(); + }); + + it('should return true when 300x250 over by banner', function () { + setUAMobile(); + bidRequest.mediaTypes = { + banner: { + sizes: [ + [100, 100], + [300, 250] + ] + } + }; + expect(spec.isBidRequestValid(bidRequest)).to.equal(true); + setUADefault(); }); it('should return false when the placementId params are missing', function () { diff --git a/test/spec/modules/richaudienceBidAdapter_spec.js b/test/spec/modules/richaudienceBidAdapter_spec.js index f18e67a3ac5..90723fb863f 100644 --- a/test/spec/modules/richaudienceBidAdapter_spec.js +++ b/test/spec/modules/richaudienceBidAdapter_spec.js @@ -348,34 +348,49 @@ describe('Richaudience adapter tests', function () { }); describe('UID test', function () { - pbjs.setConfig({ + config.setConfig({ consentManagement: { cmpApi: 'iab', timeout: 5000, allowAuctionWithoutConsent: true + }, + userSync: { + userIds: [{ + name: 'id5Id', + params: { + partner: 173, // change to the Partner Number you received from ID5 + pd: 'MT1iNTBjY...' // optional, see table below for a link to how to generate this + }, + storage: { + type: 'html5', // "html5" is the required storage type + name: 'id5id', // "id5id" is the required storage name + expires: 90, // storage lasts for 90 days + refreshInSeconds: 8 * 3600 // refresh ID every 8 hours to ensure it's fresh + } + }], + auctionDelay: 50 // 50ms maximum auction delay, applies to all userId modules } }); it('Verify build id5', function () { + var request; DEFAULT_PARAMS_WO_OPTIONAL[0].userId = {}; - DEFAULT_PARAMS_WO_OPTIONAL[0].userId.id5id = 'id5-user-id'; - - var request = spec.buildRequests(DEFAULT_PARAMS_WO_OPTIONAL, DEFAULT_PARAMS_GDPR); + DEFAULT_PARAMS_WO_OPTIONAL[0].userId.id5id = { uid: 1 }; + request = spec.buildRequests(DEFAULT_PARAMS_WO_OPTIONAL, DEFAULT_PARAMS_GDPR); var requestContent = JSON.parse(request[0].data); - expect(requestContent.user).to.deep.equal([{ - 'userId': 'id5-user-id', - 'source': 'id5-sync.com' - }]); + expect(requestContent.user.eids).to.equal(undefined); - var request; - DEFAULT_PARAMS_WO_OPTIONAL[0].userId = {}; - DEFAULT_PARAMS_WO_OPTIONAL[0].userId.id5id = 1; + DEFAULT_PARAMS_WO_OPTIONAL[0].userId.id5id = { uid: [] }; request = spec.buildRequests(DEFAULT_PARAMS_WO_OPTIONAL, DEFAULT_PARAMS_GDPR); - var requestContent = JSON.parse(request[0].data); + requestContent = JSON.parse(request[0].data); + expect(requestContent.user.eids).to.equal(undefined); + DEFAULT_PARAMS_WO_OPTIONAL[0].userId.id5id = { uid: null }; + request = spec.buildRequests(DEFAULT_PARAMS_WO_OPTIONAL, DEFAULT_PARAMS_GDPR); + requestContent = JSON.parse(request[0].data); expect(requestContent.user.eids).to.equal(undefined); - DEFAULT_PARAMS_WO_OPTIONAL[0].userId.id5id = []; + DEFAULT_PARAMS_WO_OPTIONAL[0].userId.id5id = { uid: {} }; request = spec.buildRequests(DEFAULT_PARAMS_WO_OPTIONAL, DEFAULT_PARAMS_GDPR); requestContent = JSON.parse(request[0].data); expect(requestContent.user.eids).to.equal(undefined); @@ -729,7 +744,7 @@ describe('Richaudience adapter tests', function () { }, [], {consentString: '', gdprApplies: true}); expect(syncs).to.have.lengthOf(0); - pbjs.setConfig({ + config.setConfig({ consentManagement: { cmpApi: 'iab', timeout: 5000, diff --git a/test/spec/modules/rubiconAnalyticsAdapter_spec.js b/test/spec/modules/rubiconAnalyticsAdapter_spec.js index 60b28b02361..1dd524e1838 100644 --- a/test/spec/modules/rubiconAnalyticsAdapter_spec.js +++ b/test/spec/modules/rubiconAnalyticsAdapter_spec.js @@ -3,6 +3,7 @@ import rubiconAnalyticsAdapter, { parseBidResponse, getHostNameFromReferer, storage, + rubiConf, } from 'modules/rubiconAnalyticsAdapter.js'; import CONSTANTS from 'src/constants.json'; import { config } from 'src/config.js'; @@ -12,9 +13,6 @@ import { setConfig, addBidResponseHook, } from 'modules/currency.js'; -import { getGlobal } from 'src/prebidGlobal.js'; - -let prebidGlobal = getGlobal(); let Ajv = require('ajv'); let schema = require('./rubiconAnalyticsSchema.json'); let ajv = new Ajv({ @@ -113,10 +111,60 @@ const BID2 = Object.assign({}, BID, { } }); +const BID3 = Object.assign({}, BID, { + adUnitCode: '/19968336/siderail-tag1', + bidId: '5fg6hyy4r879f0', + adId: 'fake_ad_id', + requestId: '5fg6hyy4r879f0', + width: 300, + height: 250, + mediaType: 'banner', + cpm: 2.01, + source: 'server', + seatBidId: 'aaaa-bbbb-cccc-dddd', + rubiconTargeting: { + 'rpfl_elemid': '/19968336/siderail-tag1', + 'rpfl_14062': '15_tier0200' + }, + adserverTargeting: { + 'hb_bidder': 'rubicon', + 'hb_adid': '5fg6hyy4r879f0', + 'hb_pb': '2.00', + 'hb_size': '300x250', + 'hb_source': 'server' + } +}); + +const floorMinRequest = { + 'bidder': 'rubicon', + 'params': { + 'accountId': '14062', + 'siteId': '70608', + 'zoneId': '335918', + 'userId': '12346', + 'keywords': ['a', 'b', 'c'], + 'inventory': {'rating': '4-star', 'prodtype': 'tech'}, + 'visitor': {'ucat': 'new', 'lastsearch': 'iphone'}, + 'position': 'atf' + }, + 'mediaTypes': { + 'banner': { + 'sizes': [[300, 250]] + } + }, + 'adUnitCode': '/19968336/siderail-tag1', + 'transactionId': 'c435626g-9e3f-401a-bee1-d56aec29a1d4', + 'sizes': [[300, 250]], + 'bidId': '5fg6hyy4r879f0', + 'bidderRequestId': '1be65d7958826a', + 'auctionId': '25c6d7f5-699a-4bfc-87c9-996f915341fa' +}; + const MOCK = { SET_TARGETING: { [BID.adUnitCode]: BID.adserverTargeting, - [BID2.adUnitCode]: BID2.adserverTargeting + [BID2.adUnitCode]: BID2.adserverTargeting, + [BID3.adUnitCode]: BID3.adserverTargeting }, AUCTION_INIT: { 'auctionId': '25c6d7f5-699a-4bfc-87c9-996f915341fa', @@ -207,7 +255,8 @@ const MOCK = { 'sizes': [[640, 480]], 'bidId': '2ecff0db240757', 'bidderRequestId': '1be65d7958826a', - 'auctionId': '25c6d7f5-699a-4bfc-87c9-996f915341fa' + 'auctionId': '25c6d7f5-699a-4bfc-87c9-996f915341fa', + 'src': 'client' }, { 'bidder': 'rubicon', @@ -231,7 +280,8 @@ const MOCK = { 'sizes': [[1000, 300], [970, 250], [728, 90]], 'bidId': '3bd4ebb1c900e2', 'bidderRequestId': '1be65d7958826a', - 'auctionId': '25c6d7f5-699a-4bfc-87c9-996f915341fa' + 'auctionId': '25c6d7f5-699a-4bfc-87c9-996f915341fa', + 'src': 's2s' } ], 'auctionStart': 1519149536560, @@ -243,7 +293,8 @@ const MOCK = { }, BID_RESPONSE: [ BID, - BID2 + BID2, + BID3 ], AUCTION_END: { 'auctionId': '25c6d7f5-699a-4bfc-87c9-996f915341fa' @@ -254,14 +305,21 @@ const MOCK = { }), Object.assign({}, BID2, { 'status': 'rendered' + }), + Object.assign({}, BID3, { + 'status': 'rendered' }) ], BIDDER_DONE: { 'bidderCode': 'rubicon', + 'serverResponseTimeMs': 42, 'bids': [ BID, Object.assign({}, BID2, { 'serverResponseTimeMs': 42, + }), + Object.assign({}, BID3, { + 'serverResponseTimeMs': 55, }) ] }, @@ -559,6 +617,73 @@ describe('rubicon analytics adapter', function () { expect(utils.logError.called).to.equal(true); }); + describe('config subscribe', function() { + it('should update the pvid if user asks', function () { + expect(utils.generateUUID.called).to.equal(false); + config.setConfig({rubicon: {updatePageView: true}}); + expect(utils.generateUUID.called).to.equal(true); + }); + it('should merge in and preserve older set configs', function () { + config.setConfig({ + rubicon: { + wrapperName: '1001_general', + int_type: 'dmpbjs', + fpkvs: { + source: 'fb' + } + } + }); + expect(rubiConf).to.deep.equal({ + pvid: '12345678', + wrapperName: '1001_general', + int_type: 'dmpbjs', + fpkvs: { + source: 'fb' + }, + updatePageView: true + }); + + // update it with stuff + config.setConfig({ + rubicon: { + fpkvs: { + link: 'email' + } + } + }); + expect(rubiConf).to.deep.equal({ + pvid: '12345678', + wrapperName: '1001_general', + int_type: 'dmpbjs', + fpkvs: { + source: 'fb', + link: 'email' + }, + updatePageView: true + }); + + // overwriting specific edge keys should update them + config.setConfig({ + rubicon: { + fpkvs: { + link: 'iMessage', + source: 'twitter' + } + } + }); + expect(rubiConf).to.deep.equal({ + pvid: '12345678', + wrapperName: '1001_general', + int_type: 'dmpbjs', + fpkvs: { + link: 'iMessage', + source: 'twitter' + }, + updatePageView: true + }); + }); + }); + describe('sampling', function () { beforeEach(function () { sandbox.stub(Math, 'random').returns(0.08); @@ -727,6 +852,8 @@ describe('rubicon analytics adapter', function () { auctionInit.bidderRequests[0].bids[0].floorData = { skipped: false, modelVersion: 'someModelName', + modelWeight: 10, + modelTimestamp: 1606772895, location: 'setConfig', skipRate: 15, fetchStatus: 'error', @@ -777,14 +904,40 @@ describe('rubicon analytics adapter', function () { } }; + let floorMinResponse = { + ...BID3, + floorData: { + floorValue: 1.5, + floorRuleValue: 1, + floorRule: '12345/entertainment|banner', + floorCurrency: 'USD', + cpmAfterAdjustments: 2.00, + enforcements: { + enforceJS: true, + enforcePBS: false, + floorDeals: false, + bidAdjustment: true + }, + matchedFields: { + gptSlot: '12345/entertainment', + mediaType: 'banner' + } + } + }; + + let bidRequest = utils.deepClone(MOCK.BID_REQUESTED); + bidRequest.bids.push(floorMinRequest) + // spoof the auction with just our duplicates events.emit(AUCTION_INIT, auctionInit); - events.emit(BID_REQUESTED, MOCK.BID_REQUESTED); + events.emit(BID_REQUESTED, bidRequest); events.emit(BID_RESPONSE, flooredResponse); events.emit(BID_RESPONSE, notFlooredResponse); + events.emit(BID_RESPONSE, floorMinResponse); events.emit(AUCTION_END, MOCK.AUCTION_END); events.emit(SET_TARGETING, MOCK.SET_TARGETING); events.emit(BID_WON, MOCK.BID_WON[1]); + events.emit(BID_WON, MOCK.BID_WON[2]); clock.tick(SEND_TIMEOUT + 1000); expect(server.requests.length).to.equal(1); @@ -795,13 +948,15 @@ describe('rubicon analytics adapter', function () { } it('should capture price floor information correctly', function () { - let message = performFloorAuction('rubicon') + let message = performFloorAuction('rubicon'); // verify our floor stuff is passed // top level floor info expect(message.auctions[0].floors).to.deep.equal({ location: 'setConfig', modelName: 'someModelName', + modelWeight: 10, + modelTimestamp: 1606772895, skipped: false, enforcement: true, dealsEnforced: false, @@ -827,6 +982,16 @@ describe('rubicon analytics adapter', function () { expect(message.auctions[0].adUnits[1].bids[0].status).to.equal('success'); expect(message.auctions[0].adUnits[1].bids[0].bidResponse.floorValue).to.equal(1); expect(message.auctions[0].adUnits[1].bids[0].bidResponse.bidPriceUSD).to.equal(1.52); + + // second adUnit's adSlot + expect(message.auctions[0].adUnits[2].gam.adSlot).to.equal('12345/entertainment'); + // top level adUnit status is success + expect(message.auctions[0].adUnits[2].status).to.equal('success'); + // second adUnits bid is success + expect(message.auctions[0].adUnits[2].bids[0].status).to.equal('success'); + expect(message.auctions[0].adUnits[2].bids[0].bidResponse.floorValue).to.equal(1.5); + expect(message.auctions[0].adUnits[2].bids[0].bidResponse.floorRuleValue).to.equal(1); + expect(message.auctions[0].adUnits[2].bids[0].bidResponse.bidPriceUSD).to.equal(2.01); }); it('should still send floor info if provider is not rubicon', function () { @@ -837,6 +1002,8 @@ describe('rubicon analytics adapter', function () { expect(message.auctions[0].floors).to.deep.equal({ location: 'setConfig', modelName: 'someModelName', + modelWeight: 10, + modelTimestamp: 1606772895, skipped: false, enforcement: true, dealsEnforced: false, @@ -862,20 +1029,22 @@ describe('rubicon analytics adapter', function () { expect(message.auctions[0].adUnits[1].bids[0].status).to.equal('success'); expect(message.auctions[0].adUnits[1].bids[0].bidResponse.floorValue).to.equal(1); expect(message.auctions[0].adUnits[1].bids[0].bidResponse.bidPriceUSD).to.equal(1.52); + + // second adUnit's adSlot + expect(message.auctions[0].adUnits[2].gam.adSlot).to.equal('12345/entertainment'); + // top level adUnit status is success + expect(message.auctions[0].adUnits[2].status).to.equal('success'); + // second adUnits bid is success + expect(message.auctions[0].adUnits[2].bids[0].status).to.equal('success'); + expect(message.auctions[0].adUnits[2].bids[0].bidResponse.floorValue).to.equal(1.5); + expect(message.auctions[0].adUnits[2].bids[0].bidResponse.floorRuleValue).to.equal(1); + expect(message.auctions[0].adUnits[2].bids[0].bidResponse.bidPriceUSD).to.equal(2.01); }); describe('with session handling', function () { - let pvid, kvps; + const expectedPvid = STUBBED_UUID.slice(0, 8); beforeEach(function () { - // custom dm stuff - prebidGlobal.rp = { - getCustomTargeting: () => kvps, - generatePageViewId: () => pvid - } - }); - - afterEach(function () { - prebidGlobal.rp = pvid = kvps = undefined; + config.setConfig({rubicon: {updatePageView: true}}); }); it('should not log any session data if local storage is not enabled', function () { @@ -899,7 +1068,6 @@ describe('rubicon analytics adapter', function () { }); it('should should pass along custom rubicon kv and pvid when defined', function () { - pvid = '1a2b3c'; config.setConfig({rubicon: { fpkvs: { source: 'fb', @@ -913,7 +1081,7 @@ describe('rubicon analytics adapter', function () { validate(message); let expectedMessage = utils.deepClone(ANALYTICS_MESSAGE); - expectedMessage.session.pvid = '1a2b3c'; + expectedMessage.session.pvid = STUBBED_UUID.slice(0, 8); expectedMessage.fpkvs = [ {key: 'source', value: 'fb'}, {key: 'link', value: 'email'} @@ -921,6 +1089,34 @@ describe('rubicon analytics adapter', function () { expect(message).to.deep.equal(expectedMessage); }); + it('should use the query utm param rubicon kv value and pass updated kv and pvid when defined', function () { + sandbox.stub(utils, 'getWindowLocation').returns({'search': '?utm_source=other', 'pbjs_debug': 'true'}); + + config.setConfig({rubicon: { + fpkvs: { + source: 'fb', + link: 'email' + } + }}); + performStandardAuction(); + expect(server.requests.length).to.equal(1); + let request = server.requests[0]; + let message = JSON.parse(request.requestBody); + validate(message); + + let expectedMessage = utils.deepClone(ANALYTICS_MESSAGE); + expectedMessage.session.pvid = STUBBED_UUID.slice(0, 8); + expectedMessage.fpkvs = [ + {key: 'source', value: 'other'}, + {key: 'link', value: 'email'} + ] + + message.fpkvs.sort((left, right) => left.key < right.key); + expectedMessage.fpkvs.sort((left, right) => left.key < right.key); + + expect(message).to.deep.equal(expectedMessage); + }); + it('should pick up existing localStorage and use its values', function () { // set some localStorage let inputlocalStorage = { @@ -932,7 +1128,6 @@ describe('rubicon analytics adapter', function () { }; getDataFromLocalStorageStub.withArgs('rpaSession').returns(btoa(JSON.stringify(inputlocalStorage))); - pvid = '1a2b3c'; config.setConfig({rubicon: { fpkvs: { link: 'email' // should merge this with what is in the localStorage! @@ -949,7 +1144,7 @@ describe('rubicon analytics adapter', function () { id: '987654', start: 1519766113781, expires: 1519787713781, - pvid: '1a2b3c' + pvid: expectedPvid } expectedMessage.fpkvs = [ {key: 'source', value: 'tw'}, @@ -970,7 +1165,66 @@ describe('rubicon analytics adapter', function () { expires: 1519787713781, // should have stayed same lastSeen: 1519767013781, // lastSeen updated to our "now" fpkvs: { source: 'tw', link: 'email' }, // link merged in - pvid: '1a2b3c' // new pvid stored + pvid: expectedPvid // new pvid stored + }); + }); + + it('should overwrite matching localstorge value and use its remaining values', function () { + sandbox.stub(utils, 'getWindowLocation').returns({'search': '?utm_source=fb&utm_click=dog'}); + + // set some localStorage + let inputlocalStorage = { + id: '987654', + start: 1519766113781, // 15 mins before "now" + expires: 1519787713781, // six hours later + lastSeen: 1519766113781, + fpkvs: { source: 'tw', link: 'email' } + }; + getDataFromLocalStorageStub.withArgs('rpaSession').returns(btoa(JSON.stringify(inputlocalStorage))); + + config.setConfig({rubicon: { + fpkvs: { + link: 'email' // should merge this with what is in the localStorage! + } + }}); + performStandardAuction(); + expect(server.requests.length).to.equal(1); + let request = server.requests[0]; + let message = JSON.parse(request.requestBody); + validate(message); + + let expectedMessage = utils.deepClone(ANALYTICS_MESSAGE); + expectedMessage.session = { + id: '987654', + start: 1519766113781, + expires: 1519787713781, + pvid: expectedPvid + } + expectedMessage.fpkvs = [ + {key: 'source', value: 'fb'}, + {key: 'link', value: 'email'}, + {key: 'click', value: 'dog'} + ] + + message.fpkvs.sort((left, right) => left.key < right.key); + expectedMessage.fpkvs.sort((left, right) => left.key < right.key); + + expect(message).to.deep.equal(expectedMessage); + + let calledWith; + try { + calledWith = JSON.parse(atob(setDataInLocalStorageStub.getCall(0).args[1])); + } catch (e) { + calledWith = {}; + } + + expect(calledWith).to.deep.equal({ + id: '987654', // should have stayed same + start: 1519766113781, // should have stayed same + expires: 1519787713781, // should have stayed same + lastSeen: 1519767013781, // lastSeen updated to our "now" + fpkvs: { source: 'fb', link: 'email', click: 'dog' }, // link merged in + pvid: expectedPvid // new pvid stored }); }); @@ -985,7 +1239,6 @@ describe('rubicon analytics adapter', function () { }; getDataFromLocalStorageStub.withArgs('rpaSession').returns(btoa(JSON.stringify(inputlocalStorage))); - pvid = '1a2b3c'; config.setConfig({rubicon: { fpkvs: { link: 'email' // should merge this with what is in the localStorage! @@ -1000,7 +1253,7 @@ describe('rubicon analytics adapter', function () { let expectedMessage = utils.deepClone(ANALYTICS_MESSAGE); // session should match what is already in ANALYTICS_MESSAGE, just need to add pvid - expectedMessage.session.pvid = '1a2b3c'; + expectedMessage.session.pvid = expectedPvid; // the saved fpkvs should have been thrown out since session expired expectedMessage.fpkvs = [ @@ -1021,7 +1274,7 @@ describe('rubicon analytics adapter', function () { expires: 1519788613781, // should have stayed same lastSeen: 1519767013781, // lastSeen updated to our "now" fpkvs: { link: 'email' }, // link merged in - pvid: '1a2b3c' // new pvid stored + pvid: expectedPvid // new pvid stored }); }); @@ -1036,7 +1289,6 @@ describe('rubicon analytics adapter', function () { }; getDataFromLocalStorageStub.withArgs('rpaSession').returns(btoa(JSON.stringify(inputlocalStorage))); - pvid = '1a2b3c'; config.setConfig({rubicon: { fpkvs: { link: 'email' // should merge this with what is in the localStorage! @@ -1051,7 +1303,7 @@ describe('rubicon analytics adapter', function () { let expectedMessage = utils.deepClone(ANALYTICS_MESSAGE); // session should match what is already in ANALYTICS_MESSAGE, just need to add pvid - expectedMessage.session.pvid = '1a2b3c'; + expectedMessage.session.pvid = expectedPvid; // the saved fpkvs should have been thrown out since session expired expectedMessage.fpkvs = [ @@ -1072,7 +1324,7 @@ describe('rubicon analytics adapter', function () { expires: 1519788613781, // should have stayed same lastSeen: 1519767013781, // lastSeen updated to our "now" fpkvs: { link: 'email' }, // link merged in - pvid: '1a2b3c' // new pvid stored + pvid: expectedPvid // new pvid stored }); }); }); diff --git a/test/spec/modules/rubiconBidAdapter_spec.js b/test/spec/modules/rubiconBidAdapter_spec.js index f4e35517636..6944034a787 100644 --- a/test/spec/modules/rubiconBidAdapter_spec.js +++ b/test/spec/modules/rubiconBidAdapter_spec.js @@ -1,10 +1,17 @@ import {expect} from 'chai'; -import {spec, getPriceGranularity, masSizeOrdering, resetUserSync, hasVideoMediaType, fastlaneEndpoint} from 'modules/rubiconBidAdapter.js'; +import { + spec, + getPriceGranularity, + masSizeOrdering, + resetUserSync, + hasVideoMediaType, + resetRubiConf +} from 'modules/rubiconBidAdapter.js'; import {parse as parseQuery} from 'querystring'; import {config} from 'src/config.js'; import * as utils from 'src/utils.js'; import find from 'core-js-pure/features/array/find.js'; -import { createEidsArray } from 'modules/userId/eids.js'; +import {createEidsArray} from 'modules/userId/eids.js'; const INTEGRATION = `pbjs_lite_v$prebid.version$`; // $prebid.version$ will be substituted in by gulp in built prebid const PBS_INTEGRATION = 'pbjs'; @@ -137,7 +144,7 @@ describe('the rubicon adapter', function () { 'targeting': [ { 'key': getProp('targeting_key', `rpfl_${i}`), - 'values': [ '43_tier_all_test' ] + 'values': ['43_tier_all_test'] } ] }; @@ -220,11 +227,26 @@ describe('the rubicon adapter', function () { 'size_id': 201, }; bid.userId = { - lipb: { lipbid: '0000-1111-2222-3333', segments: ['segA', 'segB'] }, + lipb: {lipbid: '0000-1111-2222-3333', segments: ['segA', 'segB']}, idl_env: '1111-2222-3333-4444', - sharedid: { id: '1111', third: '2222' }, + sharedid: {id: '1111', third: '2222'}, tdid: '3000', - pubcid: '4000' + pubcid: '4000', + pubProvidedId: [{ + source: 'example.com', + uids: [{ + id: '333333', + ext: { + stype: 'ppuid' + } + }] + }, { + source: 'id-partner.com', + uids: [{ + id: '4444444' + }] + }], + criteoId: '1111', }; bid.userIdAsEids = createEidsArray(bid.userId); bid.storedAuctionResponse = 11111; @@ -347,6 +369,8 @@ describe('the rubicon adapter', function () { afterEach(function () { sandbox.restore(); utils.logError.restore(); + config.resetConfig(); + resetRubiConf(); }); describe('MAS mapping / ordering', function () { @@ -372,7 +396,10 @@ describe('the rubicon adapter', function () { describe('to fastlane', function () { it('should make a well-formed request object', function () { sandbox.stub(Math, 'random').callsFake(() => 0.1); - let [request] = spec.buildRequests(bidderRequest.bids, bidderRequest); + let duplicate = Object.assign(bidderRequest); + duplicate.bids[0].params.floor = 0.01; + + let [request] = spec.buildRequests(duplicate.bids, duplicate); let data = parseQuery(request.data); expect(request.url).to.equal('https://fastlane.rubiconproject.com/a/api/fastlane.json'); @@ -461,35 +488,30 @@ describe('the rubicon adapter', function () { data = parseQuery(request.data); expect(data.rp_hard_floor).to.equal('1.23'); }); - it('should not send p_pos to AE if not params.position specified', function() { - var noposRequest = utils.deepClone(bidderRequest); - delete noposRequest.bids[0].params.position; + it('should not send p_pos to AE if not params.position specified', function () { + var noposRequest = utils.deepClone(bidderRequest); + delete noposRequest.bids[0].params.position; - let [request] = spec.buildRequests(noposRequest.bids, noposRequest); - let data = parseQuery(request.data); + let [request] = spec.buildRequests(noposRequest.bids, noposRequest); + let data = parseQuery(request.data); - expect(data['site_id']).to.equal('70608'); - expect(data['p_pos']).to.equal(undefined); + expect(data['site_id']).to.equal('70608'); + expect(data['p_pos']).to.equal(undefined); }); - it('should not send p_pos to AE if not params.position is invalid', function() { - var badposRequest = utils.deepClone(bidderRequest); - badposRequest.bids[0].params.position = 'bad'; + it('should not send p_pos to AE if not params.position is invalid', function () { + var badposRequest = utils.deepClone(bidderRequest); + badposRequest.bids[0].params.position = 'bad'; - let [request] = spec.buildRequests(badposRequest.bids, badposRequest); - let data = parseQuery(request.data); + let [request] = spec.buildRequests(badposRequest.bids, badposRequest); + let data = parseQuery(request.data); - expect(data['site_id']).to.equal('70608'); - expect(data['p_pos']).to.equal(undefined); + expect(data['site_id']).to.equal('70608'); + expect(data['p_pos']).to.equal(undefined); }); it('should correctly send p_pos in sra fashion', function() { - sandbox.stub(config, 'getConfig').callsFake((key) => { - const config = { - 'rubicon.singleRequest': true - }; - return config[key]; - }); + config.setConfig({rubicon: {singleRequest: true}}); // first one is atf var sraPosRequest = utils.deepClone(bidderRequest); @@ -519,11 +541,11 @@ describe('the rubicon adapter', function () { expect(data['p_pos']).to.equal('atf;;btf;;'); }); - it('should not send x_source.pchain to AE if params.pchain is not specified', function() { - var noPchainRequest = utils.deepClone(bidderRequest); - delete noPchainRequest.bids[0].params.pchain; + it('should not send x_source.pchain to AE if params.pchain is not specified', function () { + var noPchainRequest = utils.deepClone(bidderRequest); + delete noPchainRequest.bids[0].params.pchain; - let [request] = spec.buildRequests(noPchainRequest.bids, noPchainRequest); + let [request] = spec.buildRequests(noPchainRequest.bids, noPchainRequest); expect(request.data).to.contain('&site_id=70608&'); expect(request.data).to.not.contain('x_source.pchain'); }); @@ -532,7 +554,7 @@ describe('the rubicon adapter', function () { sandbox.stub(Math, 'random').callsFake(() => 0.1); let [request] = spec.buildRequests(bidderRequest.bids, bidderRequest); - const referenceOrdering = ['account_id', 'site_id', 'zone_id', 'size_id', 'alt_size_ids', 'p_pos', 'rf', 'p_geo.latitude', 'p_geo.longitude', 'kw', 'tg_v.ucat', 'tg_v.lastsearch', 'tg_v.likes', 'tg_i.rating', 'tg_i.prodtype', 'tk_flint', 'x_source.tid', 'x_source.pchain', 'p_screen_res', 'rp_floor', 'rp_secure', 'tk_user_key', 'tg_fl.eid', 'slots', 'rand']; + const referenceOrdering = ['account_id', 'site_id', 'zone_id', 'size_id', 'alt_size_ids', 'p_pos', 'rf', 'p_geo.latitude', 'p_geo.longitude', 'kw', 'tg_v.ucat', 'tg_v.lastsearch', 'tg_v.likes', 'tg_i.rating', 'tg_i.prodtype', 'tk_flint', 'x_source.tid', 'x_source.pchain', 'p_screen_res', 'rp_secure', 'tk_user_key', 'tg_fl.eid', 'slots', 'rand']; request.data.split('&').forEach((item, i) => { expect(item.split('=')[0]).to.equal(referenceOrdering[i]); @@ -547,7 +569,6 @@ describe('the rubicon adapter', function () { 'size_id': '15', 'alt_size_ids': '43', 'p_pos': 'atf', - 'rp_floor': '0.01', 'rp_secure': /[01]/, 'rand': '0.1', 'tk_flint': INTEGRATION, @@ -624,7 +645,7 @@ describe('the rubicon adapter', function () { expect(parseQuery(request.data).rf).to.equal('localhost'); delete bidderRequest.bids[0].params.referrer; - let refererInfo = { referer: 'https://www.prebid.org' }; + let refererInfo = {referer: 'https://www.prebid.org'}; bidderRequest = Object.assign({refererInfo}, bidderRequest); [request] = spec.buildRequests(bidderRequest.bids, bidderRequest); expect(parseQuery(request.data).rf).to.equal('https://www.prebid.org'); @@ -683,233 +704,6 @@ describe('the rubicon adapter', function () { expect(data['rp_floor']).to.equal('2'); }); - it('should send digitrust params', function () { - window.DigiTrust = { - getUser: function () { - } - }; - sandbox.stub(window.DigiTrust, 'getUser').callsFake(() => - ({ - success: true, - identity: { - privacy: {optout: false}, - id: 'testId', - keyv: 'testKeyV' - } - }) - ); - - let [request] = spec.buildRequests(bidderRequest.bids, bidderRequest); - let data = parseQuery(request.data); - - let expectedQuery = { - 'dt.id': 'testId', - 'dt.keyv': 'testKeyV', - 'dt.pref': '0' - }; - - // test that all values above are both present and correct - Object.keys(expectedQuery).forEach(key => { - let value = expectedQuery[key]; - expect(data[key]).to.equal(value); - }); - - delete window.DigiTrust; - }); - - it('should not send digitrust params when DigiTrust not loaded', function () { - let [request] = spec.buildRequests(bidderRequest.bids, bidderRequest); - let data = parseQuery(request.data); - - let undefinedKeys = ['dt.id', 'dt.keyv']; - - // Test that none of the DigiTrust keys are part of the query - undefinedKeys.forEach(key => { - expect(typeof data[key]).to.equal('undefined'); - }); - }); - - it('should not send digitrust params due to optout', function () { - window.DigiTrust = { - getUser: function () { - } - }; - sandbox.stub(window.DigiTrust, 'getUser').callsFake(() => - ({ - success: true, - identity: { - privacy: {optout: true}, - id: 'testId', - keyv: 'testKeyV' - } - }) - ); - - let [request] = spec.buildRequests(bidderRequest.bids, bidderRequest); - let data = parseQuery(request.data); - - let undefinedKeys = ['dt.id', 'dt.keyv']; - - // Test that none of the DigiTrust keys are part of the query - undefinedKeys.forEach(key => { - expect(typeof data[key]).to.equal('undefined'); - }); - - delete window.DigiTrust; - }); - - it('should not send digitrust params due to failure', function () { - window.DigiTrust = { - getUser: function () { - } - }; - sandbox.stub(window.DigiTrust, 'getUser').callsFake(() => - ({ - success: false, - identity: { - privacy: {optout: false}, - id: 'testId', - keyv: 'testKeyV' - } - }) - ); - - let [request] = spec.buildRequests(bidderRequest.bids, bidderRequest); - let data = parseQuery(request.data); - - let undefinedKeys = ['dt.id', 'dt.keyv']; - - // Test that none of the DigiTrust keys are part of the query - undefinedKeys.forEach(key => { - expect(typeof data[key]).to.equal('undefined'); - }); - - delete window.DigiTrust; - }); - - describe('digiTrustId config', function () { - beforeEach(function () { - window.DigiTrust = { - getUser: sandbox.spy() - }; - }); - - afterEach(function () { - delete window.DigiTrust; - }); - - it('should send digiTrustId config params', function () { - sandbox.stub(config, 'getConfig').callsFake((key) => { - var config = { - digiTrustId: { - success: true, - identity: { - privacy: {optout: false}, - id: 'testId', - keyv: 'testKeyV' - } - } - }; - return config[key]; - }); - - let [request] = spec.buildRequests(bidderRequest.bids, bidderRequest); - let data = parseQuery(request.data); - - let expectedQuery = { - 'dt.id': 'testId', - 'dt.keyv': 'testKeyV' - }; - - // test that all values above are both present and correct - Object.keys(expectedQuery).forEach(key => { - let value = expectedQuery[key]; - expect(data[key]).to.equal(value); - }); - - // should not have called DigiTrust.getUser() - expect(window.DigiTrust.getUser.notCalled).to.equal(true); - }); - - it('should not send digiTrustId config params due to optout', function () { - sandbox.stub(config, 'getConfig').callsFake((key) => { - var config = { - digiTrustId: { - success: true, - identity: { - privacy: {optout: true}, - id: 'testId', - keyv: 'testKeyV' - } - } - } - return config[key]; - }); - - let [request] = spec.buildRequests(bidderRequest.bids, bidderRequest); - let data = parseQuery(request.data); - - let undefinedKeys = ['dt.id', 'dt.keyv']; - - // Test that none of the DigiTrust keys are part of the query - undefinedKeys.forEach(key => { - expect(typeof data[key]).to.equal('undefined'); - }); - - // should not have called DigiTrust.getUser() - expect(window.DigiTrust.getUser.notCalled).to.equal(true); - }); - - it('should not send digiTrustId config params due to failure', function () { - sandbox.stub(config, 'getConfig').callsFake((key) => { - var config = { - digiTrustId: { - success: false, - identity: { - privacy: {optout: false}, - id: 'testId', - keyv: 'testKeyV' - } - } - } - return config[key]; - }); - - let [request] = spec.buildRequests(bidderRequest.bids, bidderRequest); - let data = parseQuery(request.data); - - let undefinedKeys = ['dt.id', 'dt.keyv']; - - // Test that none of the DigiTrust keys are part of the query - undefinedKeys.forEach(key => { - expect(typeof data[key]).to.equal('undefined'); - }); - - // should not have called DigiTrust.getUser() - expect(window.DigiTrust.getUser.notCalled).to.equal(true); - }); - - it('should not send digiTrustId config params if they do not exist', function () { - sandbox.stub(config, 'getConfig').callsFake((key) => { - var config = {}; - return config[key]; - }); - - let [request] = spec.buildRequests(bidderRequest.bids, bidderRequest); - let data = parseQuery(request.data); - - let undefinedKeys = ['dt.id', 'dt.keyv']; - - // Test that none of the DigiTrust keys are part of the query - undefinedKeys.forEach(key => { - expect(typeof data[key]).to.equal('undefined'); - }); - - // should have called DigiTrust.getUser() once - expect(window.DigiTrust.getUser.calledOnce).to.equal(true); - }); - }); - describe('GDPR consent config', function () { it('should send "gdpr" and "gdpr_consent", when gdprConsent defines consentString and gdprApplies', function () { createGdprBidderRequest(true); @@ -1041,7 +835,7 @@ describe('the rubicon adapter', function () { keywords: ['d'], gender: 'M', yob: '1984', - geo: { country: 'ca' } + geo: {country: 'ca'} }; sandbox.stub(config, 'getConfig').callsFake(key => { @@ -1082,12 +876,7 @@ describe('the rubicon adapter', function () { it('should group all bid requests with the same site id', function () { sandbox.stub(Math, 'random').callsFake(() => 0.1); - sandbox.stub(config, 'getConfig').callsFake((key) => { - const config = { - 'rubicon.singleRequest': true - }; - return config[key]; - }); + config.setConfig({rubicon: {singleRequest: true}}); const expectedQuery = { 'account_id': '14062', @@ -1096,7 +885,6 @@ describe('the rubicon adapter', function () { 'size_id': '15', 'alt_size_ids': '43', 'p_pos': 'atf', - 'rp_floor': '0.01', 'rp_secure': /[01]/, 'rand': '0.1', 'tk_flint': INTEGRATION, @@ -1195,13 +983,7 @@ describe('the rubicon adapter', function () { }); it('should not send more than 10 bids in a request (split into separate requests with <= 10 bids each)', function () { - sandbox.stub(config, 'getConfig').callsFake((key) => { - const config = { - 'rubicon.singleRequest': true - }; - return config[key]; - }); - + config.setConfig({rubicon: {singleRequest: true}}); let serverRequests; let data; @@ -1243,12 +1025,7 @@ describe('the rubicon adapter', function () { }); it('should not group bid requests if singleRequest does not equal true', function () { - sandbox.stub(config, 'getConfig').callsFake((key) => { - const config = { - 'rubicon.singleRequest': false - }; - return config[key]; - }); + config.setConfig({rubicon: {singleRequest: false}}); const bidCopy = utils.deepClone(bidderRequest.bids[0]); bidderRequest.bids.push(bidCopy); @@ -1266,12 +1043,7 @@ describe('the rubicon adapter', function () { }); it('should not group video bid requests', function () { - sandbox.stub(config, 'getConfig').callsFake((key) => { - const config = { - 'rubicon.singleRequest': true - }; - return config[key]; - }); + config.setConfig({rubicon: {singleRequest: true}}); const bidCopy = utils.deepClone(bidderRequest.bids[0]); bidderRequest.bids.push(bidCopy); @@ -1315,7 +1087,7 @@ describe('the rubicon adapter', function () { }); }); - describe('user id config', function() { + describe('user id config', function () { it('should send tpid_tdid when userIdAsEids contains unifiedId', function () { const clonedBid = utils.deepClone(bidderRequest.bids[0]); clonedBid.userId = { @@ -1326,6 +1098,7 @@ describe('the rubicon adapter', function () { let data = parseQuery(request.data); expect(data['tpid_tdid']).to.equal('abcd-efgh-ijkl-mnop-1234'); + expect(data['eid_adserver.org']).to.equal('abcd-efgh-ijkl-mnop-1234'); }); describe('LiveIntent support', function () { @@ -1333,7 +1106,8 @@ describe('the rubicon adapter', function () { const clonedBid = utils.deepClone(bidderRequest.bids[0]); clonedBid.userId = { lipb: { - lipbid: '0000-1111-2222-3333' + lipbid: '0000-1111-2222-3333', + segments: ['segA', 'segB'] } }; clonedBid.userIdAsEids = createEidsArray(clonedBid.userId); @@ -1341,6 +1115,8 @@ describe('the rubicon adapter', function () { let data = parseQuery(request.data); expect(data['tpid_liveintent.com']).to.equal('0000-1111-2222-3333'); + expect(data['eid_liveintent.com']).to.equal('0000-1111-2222-3333'); + expect(data['tg_v.LIseg']).to.equal('segA,segB'); }); it('should send tg_v.LIseg when userIdAsEids contains liveintentId with ext.segments as array', function () { @@ -1374,6 +1150,34 @@ describe('the rubicon adapter', function () { }); }); + describe('pubcid support', function () { + it('should send eid_pubcid.org when userIdAsEids contains pubcid', function () { + const clonedBid = utils.deepClone(bidderRequest.bids[0]); + clonedBid.userId = { + pubcid: '1111' + }; + clonedBid.userIdAsEids = createEidsArray(clonedBid.userId); + let [request] = spec.buildRequests([clonedBid], bidderRequest); + let data = parseQuery(request.data); + + expect(data['eid_pubcid.org']).to.equal('1111^1'); + }); + }); + + describe('Criteo support', function () { + it('should send eid_criteo.com when userIdAsEids contains criteo', function () { + const clonedBid = utils.deepClone(bidderRequest.bids[0]); + clonedBid.userId = { + criteoId: '1111' + }; + clonedBid.userIdAsEids = createEidsArray(clonedBid.userId); + let [request] = spec.buildRequests([clonedBid], bidderRequest); + let data = parseQuery(request.data); + + expect(data['eid_criteo.com']).to.equal('1111^1'); + }); + }); + describe('SharedID support', function () { it('should send sharedid when userIdAsEids contains sharedId', function () { const clonedBid = utils.deepClone(bidderRequest.bids[0]); @@ -1391,9 +1195,73 @@ describe('the rubicon adapter', function () { }); }); + describe('pubProvidedId support', function () { + it('should send pubProvidedId when userIdAsEids contains pubProvidedId ids', function () { + const clonedBid = utils.deepClone(bidderRequest.bids[0]); + clonedBid.userId = { + pubProvidedId: [{ + source: 'example.com', + uids: [{ + id: '11111', + ext: { + stype: 'ppuid' + } + }] + }, { + source: 'id-partner.com', + uids: [{ + id: '222222' + }] + }] + }; + clonedBid.userIdAsEids = createEidsArray(clonedBid.userId); + let [request] = spec.buildRequests([clonedBid], bidderRequest); + let data = parseQuery(request.data); + + expect(data['ppuid']).to.equal('11111'); + }); + }); + + describe('ID5 support', function () { + it('should send ID5 id when userIdAsEids contains ID5', function () { + const clonedBid = utils.deepClone(bidderRequest.bids[0]); + clonedBid.userId = { + id5id: { + uid: '11111', + ext: { + linkType: '22222' + } + } + }; + clonedBid.userIdAsEids = createEidsArray(clonedBid.userId); + let [request] = spec.buildRequests([clonedBid], bidderRequest); + let data = parseQuery(request.data); + + expect(data['eid_id5-sync.com']).to.equal('11111^1^22222'); + }); + }); + + describe('UserID catchall support', function () { + it('should send user id with generic format', function () { + const clonedBid = utils.deepClone(bidderRequest.bids[0]); + // Hardcoding userIdAsEids since createEidsArray returns empty array if source not found in eids.js + clonedBid.userIdAsEids = [{ + source: 'catchall', + uids: [{ + id: '11111', + atype: 2 + }] + }] + let [request] = spec.buildRequests([clonedBid], bidderRequest); + let data = parseQuery(request.data); + + expect(data['eid_catchall']).to.equal('11111^2'); + }); + }); + describe('Config user.id support', function () { it('should send ppuid when config defines user.id', function () { - config.setConfig({ user: { id: '123' } }); + config.setConfig({user: {id: '123'}}); const clonedBid = utils.deepClone(bidderRequest.bids[0]); clonedBid.userId = { sharedid: { @@ -1569,11 +1437,11 @@ describe('the rubicon adapter', function () { let [request] = spec.buildRequests(bidderRequest.bids, bidderRequest); let post = request.data; - expect(post).to.have.property('imp') + expect(post).to.have.property('imp'); // .with.length.of(1); let imp = post.imp[0]; expect(imp.id).to.equal(bidderRequest.bids[0].adUnitCode); - expect(imp.exp).to.equal(300); + expect(imp.exp).to.equal(undefined); // now undefined expect(imp.video.w).to.equal(640); expect(imp.video.h).to.equal(480); expect(imp.video.pos).to.equal(1); @@ -1603,20 +1471,10 @@ describe('the rubicon adapter', function () { expect(post.user.ext.eids[0].ext).to.have.property('segments').that.is.an('array'); expect(post.user.ext.eids[0].ext.segments[0]).to.equal('segA'); expect(post.user.ext.eids[0].ext.segments[1]).to.equal('segB'); - // Non-EID properties set using liveintent EID values - expect(post.user.ext).to.have.property('tpid').that.is.an('object'); - expect(post.user.ext.tpid.source).to.equal('liveintent.com'); - expect(post.user.ext.tpid.uid).to.equal('0000-1111-2222-3333'); - expect(post).to.have.property('rp').that.is.an('object'); - expect(post.rp).to.have.property('target').that.is.an('object'); - expect(post.rp.target).to.have.property('LIseg').that.is.an('array'); - expect(post.rp.target.LIseg[0]).to.equal('segA'); - expect(post.rp.target.LIseg[1]).to.equal('segB'); // LiveRamp should exist expect(post.user.ext.eids[1].source).to.equal('liveramp.com'); expect(post.user.ext.eids[1].uids[0].id).to.equal('1111-2222-3333-4444'); expect(post.user.ext.eids[1].uids[0].atype).to.equal(1); - // SharedId should exist expect(post.user.ext.eids[2].source).to.equal('sharedid.org'); expect(post.user.ext.eids[2].uids[0].id).to.equal('1111'); @@ -1630,6 +1488,16 @@ describe('the rubicon adapter', function () { expect(post.user.ext.eids[4].source).to.equal('pubcid.org'); expect(post.user.ext.eids[4].uids[0].atype).to.equal(1); expect(post.user.ext.eids[4].uids[0].id).to.equal('4000'); + // example should exist + expect(post.user.ext.eids[5].source).to.equal('example.com'); + expect(post.user.ext.eids[5].uids[0].id).to.equal('333333'); + // id-partner.com + expect(post.user.ext.eids[6].source).to.equal('id-partner.com'); + expect(post.user.ext.eids[6].uids[0].id).to.equal('4444444'); + // CriteoId should exist + expect(post.user.ext.eids[7].source).to.equal('criteo.com'); + expect(post.user.ext.eids[7].uids[0].id).to.equal('1111'); + expect(post.user.ext.eids[7].uids[0].atype).to.equal(1); expect(post.regs.ext.gdpr).to.equal(1); expect(post.regs.ext.us_privacy).to.equal('1NYN'); @@ -1710,7 +1578,7 @@ describe('the rubicon adapter', function () { expect(request.data.imp[0].bidfloor).to.be.undefined; }); - it('should add alias name to PBS Request', function() { + it('should add alias name to PBS Request', function () { createVideoBidderRequest(); bidderRequest.bidderCode = 'superRubicon'; @@ -1726,7 +1594,37 @@ describe('the rubicon adapter', function () { expect(request.data.imp[0].ext).to.not.haveOwnProperty('rubicon'); }); - it('should send correct bidfloor to PBS', function() { + it('should send video exp param correctly when set', function () { + createVideoBidderRequest(); + config.setConfig({s2sConfig: {defaultTtl: 600}}); + let [request] = spec.buildRequests(bidderRequest.bids, bidderRequest); + let post = request.data; + + // should exp set to the right value according to config + let imp = post.imp[0]; + expect(imp.exp).to.equal(600); + }); + + it('should not send video exp at all if not set in s2sConfig config', function () { + createVideoBidderRequest(); + let [request] = spec.buildRequests(bidderRequest.bids, bidderRequest); + let post = request.data; + + // should exp set to the right value according to config + let imp = post.imp[0]; + // bidderFactory stringifies request body before sending so removes undefined attributes: + expect(imp.exp).to.equal(undefined); + }); + + it('should send tmax as the bidderRequest timeout value', function () { + createVideoBidderRequest(); + bidderRequest.timeout = 3333; + let [request] = spec.buildRequests(bidderRequest.bids, bidderRequest); + let post = request.data; + expect(post.tmax).to.equal(3333); + }); + + it('should send correct bidfloor to PBS', function () { createVideoBidderRequest(); bidderRequest.bids[0].params.floor = 0.1; @@ -1916,14 +1814,14 @@ describe('the rubicon adapter', function () { let requests = spec.buildRequests(bidderRequest.bids, bidderRequest); expect(requests.length).to.equal(1); - expect(requests[0].url).to.equal(fastlaneEndpoint); + expect(requests[0].url).to.equal('https://fastlane.rubiconproject.com/a/api/fastlane.json'); bidderRequest.mediaTypes.video.context = 'instream'; requests = spec.buildRequests(bidderRequest.bids, bidderRequest); expect(requests.length).to.equal(1); - expect(requests[0].url).to.equal(fastlaneEndpoint); + expect(requests[0].url).to.equal('https://fastlane.rubiconproject.com/a/api/fastlane.json'); }); it('should send request as banner when invalid video bid in multiple mediaType bidRequest', function () { @@ -1942,7 +1840,7 @@ describe('the rubicon adapter', function () { let requests = spec.buildRequests(bidRequestCopy.bids, bidRequestCopy); expect(requests.length).to.equal(1); - expect(requests[0].url).to.equal(fastlaneEndpoint); + expect(requests[0].url).to.equal('https://fastlane.rubiconproject.com/a/api/fastlane.json'); }); it('should include coppa flag in video bid request', () => { @@ -1974,7 +1872,7 @@ describe('the rubicon adapter', function () { keywords: ['d'], gender: 'M', yob: '1984', - geo: { country: 'ca' } + geo: {country: 'ca'} }; sandbox.stub(config, 'getConfig').callsFake(key => { @@ -1988,7 +1886,7 @@ describe('the rubicon adapter', function () { }); const expected = [{ - bidders: [ 'rubicon' ], + bidders: ['rubicon'], config: { fpd: { site: Object.assign({}, bidderRequest.bids[0].params.inventory, context), @@ -2055,18 +1953,13 @@ describe('the rubicon adapter', function () { it('should use the integration type provided in the config instead of the default', () => { createVideoBidderRequest(); - sandbox.stub(config, 'getConfig').callsFake(function (key) { - const config = { - 'rubicon.int_type': 'testType' - }; - return config[key]; - }); + config.setConfig({rubicon: {int_type: 'testType'}}); const [request] = spec.buildRequests(bidderRequest.bids, bidderRequest); expect(request.data.ext.prebid.bidders.rubicon.integration).to.equal('testType'); }); it('should pass the user.id provided in the config', function () { - config.setConfig({ user: { id: '123' } }); + config.setConfig({user: {id: '123'}}); createVideoBidderRequest(); sandbox.stub(Date, 'now').callsFake(() => @@ -2080,7 +1973,7 @@ describe('the rubicon adapter', function () { // .with.length.of(1); let imp = post.imp[0]; expect(imp.id).to.equal(bidderRequest.bids[0].adUnitCode); - expect(imp.exp).to.equal(300); + expect(imp.exp).to.equal(undefined); expect(imp.video.w).to.equal(640); expect(imp.video.h).to.equal(480); expect(imp.video.pos).to.equal(1); @@ -2120,7 +2013,11 @@ describe('the rubicon adapter', function () { it('should combine an array of slot url params', function () { expect(spec.combineSlotUrlParams([])).to.deep.equal({}); - expect(spec.combineSlotUrlParams([{p1: 'foo', p2: 'test', p3: ''}])).to.deep.equal({p1: 'foo', p2: 'test', p3: ''}); + expect(spec.combineSlotUrlParams([{p1: 'foo', p2: 'test', p3: ''}])).to.deep.equal({ + p1: 'foo', + p2: 'test', + p3: '' + }); expect(spec.combineSlotUrlParams([{}, {p1: 'foo', p2: 'test'}])).to.deep.equal({p1: ';foo', p2: ';test'}); @@ -2151,7 +2048,6 @@ describe('the rubicon adapter', function () { 'size_id': 15, 'alt_size_ids': '43', 'p_pos': 'atf', - 'rp_floor': 0.01, 'rp_secure': /[01]/, 'tk_flint': INTEGRATION, 'x_source.tid': 'd45dd707-a418-42ec-b8a7-b70a6c6fab0b', @@ -2262,6 +2158,7 @@ describe('the rubicon adapter', function () { 'impression_id': '153dc240-8229-4604-b8f5-256933b9374c', 'size_id': '15', 'ad_id': '6', + 'adomain': ['test.com'], 'advertiser': 7, 'network': 8, 'creative_id': 'crid-9', @@ -2283,6 +2180,7 @@ describe('the rubicon adapter', function () { 'impression_id': '153dc240-8229-4604-b8f5-256933b9374d', 'size_id': '43', 'ad_id': '7', + 'adomain': ['test.com'], 'advertiser': 7, 'network': 8, 'creative_id': 'crid-9', @@ -2317,6 +2215,8 @@ describe('the rubicon adapter', function () { expect(bids[0].rubicon.networkId).to.equal(8); expect(bids[0].creativeId).to.equal('crid-9'); expect(bids[0].currency).to.equal('USD'); + expect(bids[0].meta.mediaType).to.equal('banner'); + expect(String(bids[0].meta.advertiserDomains)).to.equal('test.com'); expect(bids[0].ad).to.contain(`alert('foo')`) .and.to.contain(``) .and.to.contain(`
`); @@ -2676,7 +2576,7 @@ describe('the rubicon adapter', function () { }] }; - let bids = spec.interpretResponse({ body: response }, { + let bids = spec.interpretResponse({body: response}, { bidRequest: [utils.deepClone(bidderRequest.bids[0])] }); @@ -2687,7 +2587,7 @@ describe('the rubicon adapter', function () { describe('singleRequest enabled', function () { it('handles bidRequest of type Array and returns associated adUnits', function () { const overrideMap = []; - overrideMap[0] = { impression_id: '1' }; + overrideMap[0] = {impression_id: '1'}; const stubAds = []; for (let i = 0; i < 10; i++) { @@ -2709,7 +2609,8 @@ describe('the rubicon adapter', function () { 'tracking': '', 'inventory': {}, 'ads': stubAds - }}, { bidRequest: stubBids }); + } + }, {bidRequest: stubBids}); expect(bids).to.be.a('array').with.lengthOf(10); bids.forEach((bid) => { @@ -2742,7 +2643,7 @@ describe('the rubicon adapter', function () { it('handles incorrect adUnits length by returning all bids with matching ads', function () { const overrideMap = []; - overrideMap[0] = { impression_id: '1' }; + overrideMap[0] = {impression_id: '1'}; const stubAds = []; for (let i = 0; i < 6; i++) { @@ -2764,7 +2665,8 @@ describe('the rubicon adapter', function () { 'tracking': '', 'inventory': {}, 'ads': stubAds - }}, { bidRequest: stubBids }); + } + }, {bidRequest: stubBids}); // no bids expected because response didn't match requested bid number expect(bids).to.be.a('array').with.lengthOf(6); @@ -2775,11 +2677,11 @@ describe('the rubicon adapter', function () { // Create overrides to break associations between bids and ads // Each override should cause one less bid to be returned by interpretResponse const overrideMap = []; - overrideMap[0] = { impression_id: '1' }; - overrideMap[2] = { status: 'error' }; - overrideMap[4] = { status: 'error' }; - overrideMap[7] = { status: 'error' }; - overrideMap[8] = { status: 'error' }; + overrideMap[0] = {impression_id: '1'}; + overrideMap[2] = {status: 'error'}; + overrideMap[4] = {status: 'error'}; + overrideMap[7] = {status: 'error'}; + overrideMap[8] = {status: 'error'}; for (let i = 0; i < 10; i++) { stubAds.push(createResponseAdByIndex(i, sizeMap[i].sizeId, overrideMap)); @@ -2800,7 +2702,8 @@ describe('the rubicon adapter', function () { 'tracking': '', 'inventory': {}, 'ads': stubAds - }}, { bidRequest: stubBids }); + } + }, {bidRequest: stubBids}); expect(bids).to.be.a('array').with.lengthOf(6); bids.forEach((bid) => { @@ -2847,13 +2750,15 @@ describe('the rubicon adapter', function () { bid: [{ id: '0', impid: 'instream_video1', + adomain: ['test.com'], price: 2, crid: '4259970', ext: { bidder: { rp: { mime: 'application/javascript', - size_id: 201 + size_id: 201, + advid: 12345 } }, prebid: { @@ -2882,6 +2787,9 @@ describe('the rubicon adapter', function () { expect(bids[0].netRevenue).to.equal(true); expect(bids[0].adserverTargeting).to.deep.equal({hb_uuid: '0c498f63-5111-4bed-98e2-9be7cb932a64'}); expect(bids[0].mediaType).to.equal('video'); + expect(bids[0].meta.mediaType).to.equal('video'); + expect(String(bids[0].meta.advertiserDomains)).to.equal('test.com'); + expect(bids[0].meta.advertiserId).to.equal(12345); expect(bids[0].bidderCode).to.equal('rubicon'); expect(bids[0].currency).to.equal('USD'); expect(bids[0].width).to.equal(640); @@ -2891,12 +2799,7 @@ describe('the rubicon adapter', function () { describe('config with integration type', () => { it('should use the integration type provided in the config instead of the default', () => { - sandbox.stub(config, 'getConfig').callsFake(function (key) { - const config = { - 'rubicon.int_type': 'testType' - }; - return config[key]; - }); + config.setConfig({rubicon: {int_type: 'testType'}}); const [request] = spec.buildRequests(bidderRequest.bids, bidderRequest); expect(parseQuery(request.data).tk_flint).to.equal('testType_v$prebid.version$'); }); @@ -2931,7 +2834,7 @@ describe('the rubicon adapter', function () { }); it('should pass gdpr params if consent is true', function () { - expect(spec.getUserSyncs({ iframeEnabled: true }, {}, { + expect(spec.getUserSyncs({iframeEnabled: true}, {}, { gdprApplies: true, consentString: 'foo' })).to.deep.equal({ type: 'iframe', url: `${emilyUrl}?gdpr=1&gdpr_consent=foo` @@ -2939,7 +2842,7 @@ describe('the rubicon adapter', function () { }); it('should pass gdpr params if consent is false', function () { - expect(spec.getUserSyncs({ iframeEnabled: true }, {}, { + expect(spec.getUserSyncs({iframeEnabled: true}, {}, { gdprApplies: false, consentString: 'foo' })).to.deep.equal({ type: 'iframe', url: `${emilyUrl}?gdpr=0&gdpr_consent=foo` @@ -2947,7 +2850,7 @@ describe('the rubicon adapter', function () { }); it('should pass gdpr param gdpr_consent only when gdprApplies is undefined', function () { - expect(spec.getUserSyncs({ iframeEnabled: true }, {}, { + expect(spec.getUserSyncs({iframeEnabled: true}, {}, { consentString: 'foo' })).to.deep.equal({ type: 'iframe', url: `${emilyUrl}?gdpr_consent=foo` @@ -2955,13 +2858,13 @@ describe('the rubicon adapter', function () { }); it('should pass no params if gdpr consentString is not defined', function () { - expect(spec.getUserSyncs({ iframeEnabled: true }, {}, {})).to.deep.equal({ + expect(spec.getUserSyncs({iframeEnabled: true}, {}, {})).to.deep.equal({ type: 'iframe', url: `${emilyUrl}` }); }); it('should pass no params if gdpr consentString is a number', function () { - expect(spec.getUserSyncs({ iframeEnabled: true }, {}, { + expect(spec.getUserSyncs({iframeEnabled: true}, {}, { consentString: 0 })).to.deep.equal({ type: 'iframe', url: `${emilyUrl}` @@ -2969,7 +2872,7 @@ describe('the rubicon adapter', function () { }); it('should pass no params if gdpr consentString is null', function () { - expect(spec.getUserSyncs({ iframeEnabled: true }, {}, { + expect(spec.getUserSyncs({iframeEnabled: true}, {}, { consentString: null })).to.deep.equal({ type: 'iframe', url: `${emilyUrl}` @@ -2977,7 +2880,7 @@ describe('the rubicon adapter', function () { }); it('should pass no params if gdpr consentString is a object', function () { - expect(spec.getUserSyncs({ iframeEnabled: true }, {}, { + expect(spec.getUserSyncs({iframeEnabled: true}, {}, { consentString: {} })).to.deep.equal({ type: 'iframe', url: `${emilyUrl}` @@ -2985,19 +2888,19 @@ describe('the rubicon adapter', function () { }); it('should pass no params if gdpr is not defined', function () { - expect(spec.getUserSyncs({ iframeEnabled: true }, {}, undefined)).to.deep.equal({ + expect(spec.getUserSyncs({iframeEnabled: true}, {}, undefined)).to.deep.equal({ type: 'iframe', url: `${emilyUrl}` }); }); it('should pass us_privacy if uspConsent is defined', function () { - expect(spec.getUserSyncs({ iframeEnabled: true }, {}, undefined, '1NYN')).to.deep.equal({ + expect(spec.getUserSyncs({iframeEnabled: true}, {}, undefined, '1NYN')).to.deep.equal({ type: 'iframe', url: `${emilyUrl}?us_privacy=1NYN` }); }); it('should pass us_privacy after gdpr if both are present', function () { - expect(spec.getUserSyncs({ iframeEnabled: true }, {}, { + expect(spec.getUserSyncs({iframeEnabled: true}, {}, { consentString: 'foo' }, '1NYN')).to.deep.equal({ type: 'iframe', url: `${emilyUrl}?gdpr_consent=foo&us_privacy=1NYN` @@ -3005,8 +2908,8 @@ describe('the rubicon adapter', function () { }); }); - describe('get price granularity', function() { - it('should return correct buckets for all price granularity values', function() { + describe('get price granularity', function () { + it('should return correct buckets for all price granularity values', function () { const CUSTOM_PRICE_BUCKET_ITEM = {max: 5, increment: 0.5}; const mockConfig = { @@ -3039,7 +2942,7 @@ describe('the rubicon adapter', function () { }); }); - describe('Supply Chain Support', function() { + describe('Supply Chain Support', function () { const nodePropsOrder = ['asi', 'sid', 'hp', 'rid', 'name', 'domain']; let bidRequests; let schainConfig; diff --git a/test/spec/modules/shareUserIds_spec.js b/test/spec/modules/shareUserIds_spec.js index 451892919cb..67e39533fc7 100644 --- a/test/spec/modules/shareUserIds_spec.js +++ b/test/spec/modules/shareUserIds_spec.js @@ -50,6 +50,16 @@ describe('#userIdTargeting', function() { pubads.setTargeting('test', ['TEST']); config.GAM_KEYS.tdid = ''; userIdTargeting(userIds, config); + expect(pubads.getTargeting('tdid')).to.be.an('array').that.is.empty; expect(pubads.getTargeting('test')).to.deep.equal(['TEST']); }); + + it('User Id Targeting is added to googletag queue when GPT is not ready', function() { + let pubads = window.googletag.pubads; + delete window.googletag.pubads; + userIdTargeting(userIds, config); + window.googletag.pubads = pubads; + window.googletag.cmd.map(command => command()); + expect(window.googletag.pubads().getTargeting('TD_ID')).to.deep.equal(['my-tdid']); + }); }); diff --git a/test/spec/modules/sharethroughBidAdapter_spec.js b/test/spec/modules/sharethroughBidAdapter_spec.js index 92f9fd11eeb..cd9071a6098 100644 --- a/test/spec/modules/sharethroughBidAdapter_spec.js +++ b/test/spec/modules/sharethroughBidAdapter_spec.js @@ -12,7 +12,14 @@ const bidRequests = [ params: { pkey: 'aaaa1111' }, - userId: { tdid: 'fake-tdid' } + userId: { + tdid: 'fake-tdid', + pubcid: 'fake-pubcid', + idl_env: 'fake-identity-link' + }, + crumbs: { + pubcid: 'fake-pubcid-in-crumbs-obj' + } }, { bidder: 'sharethrough', @@ -33,13 +40,33 @@ const bidRequests = [ pkey: 'cccc3333', iframe: true, iframeSize: [500, 500] - }, + } + }, + { + bidder: 'sharethrough', + bidId: 'bidId4', + sizes: [[700, 400]], + placementCode: 'bar', + params: { + pkey: 'dddd4444', + badv: ['domain1.com', 'domain2.com'] + } + }, + { + bidder: 'sharethrough', + bidId: 'bidId5', + sizes: [[700, 400]], + placementCode: 'bar', + params: { + pkey: 'eeee5555', + bcat: ['IAB1-1', 'IAB1-2'] + } }, ]; const prebidRequests = [ { - method: 'GET', + method: 'POST', url: 'https://btlr.sharethrough.com/WYu2BXv1/v1', data: { bidId: 'bidId', @@ -51,7 +78,7 @@ const prebidRequests = [ } }, { - method: 'GET', + method: 'POST', url: 'https://btlr.sharethrough.com/WYu2BXv1/v1', data: { bidId: 'bidId', @@ -63,7 +90,7 @@ const prebidRequests = [ } }, { - method: 'GET', + method: 'POST', url: 'https://btlr.sharethrough.com/WYu2BXv1/v1', data: { bidId: 'bidId', @@ -76,7 +103,7 @@ const prebidRequests = [ } }, { - method: 'GET', + method: 'POST', url: 'https://btlr.sharethrough.com/WYu2BXv1/v1', data: { bidId: 'bidId', @@ -88,7 +115,7 @@ const prebidRequests = [ } }, { - method: 'GET', + method: 'POST', url: 'https://btlr.sharethrough.com/WYu2BXv1/v1', data: { bidId: 'bidId', @@ -98,7 +125,7 @@ const prebidRequests = [ skipIframeBusting: false, sizes: [[300, 250], [300, 300], [250, 250], [600, 50]] } - }, + } ]; const bidderResponse = { @@ -121,12 +148,12 @@ const bidderResponse = { }; const setUserAgent = (uaString) => { - window.navigator['__defineGetter__']('userAgent', function () { + window.navigator['__defineGetter__']('userAgent', function() { return uaString; }); }; -describe('sharethrough internal spec', function () { +describe('sharethrough internal spec', function() { let windowSpy, windowTopSpy; beforeEach(function() { @@ -141,7 +168,7 @@ describe('sharethrough internal spec', function () { window.top.STR = undefined; }); - describe('we cannot access top level document', function () { + describe('we cannot access top level document', function() { beforeEach(function() { window.lockedInFrame = true; }); @@ -150,12 +177,12 @@ describe('sharethrough internal spec', function () { window.lockedInFrame = false; }); - it('appends sfp.js to the safeframe', function () { + it('appends sfp.js to the safeframe', function() { sharethroughInternal.handleIframe(); expect(windowSpy.calledOnce).to.be.true; }); - it('does not append anything if sfp.js is already loaded in the safeframe', function () { + it('does not append anything if sfp.js is already loaded in the safeframe', function() { window.STR = { Tag: true }; sharethroughInternal.handleIframe(); expect(windowSpy.notCalled).to.be.true; @@ -163,14 +190,14 @@ describe('sharethrough internal spec', function () { }); }); - describe('we are able to bust out of the iframe', function () { - it('appends sfp.js to window.top', function () { + describe('we are able to bust out of the iframe', function() { + it('appends sfp.js to window.top', function() { sharethroughInternal.handleIframe(); expect(windowSpy.calledOnce).to.be.true; expect(windowTopSpy.calledOnce).to.be.true; }); - it('only appends sfp-set-targeting.js if sfp.js is already loaded on the page', function () { + it('only appends sfp-set-targeting.js if sfp.js is already loaded on the page', function() { window.top.STR = { Tag: true }; sharethroughInternal.handleIframe(); expect(windowSpy.calledOnce).to.be.true; @@ -179,15 +206,15 @@ describe('sharethrough internal spec', function () { }); }); -describe('sharethrough adapter spec', function () { - describe('.code', function () { - it('should return a bidder code of sharethrough', function () { +describe('sharethrough adapter spec', function() { + describe('.code', function() { + it('should return a bidder code of sharethrough', function() { expect(spec.code).to.eql('sharethrough'); }); }); - describe('.isBidRequestValid', function () { - it('should return false if req has no pkey', function () { + describe('.isBidRequestValid', function() { + it('should return false if req has no pkey', function() { const invalidBidRequest = { bidder: 'sharethrough', params: { @@ -197,7 +224,7 @@ describe('sharethrough adapter spec', function () { expect(spec.isBidRequestValid(invalidBidRequest)).to.eql(false); }); - it('should return false if req has wrong bidder code', function () { + it('should return false if req has wrong bidder code', function() { const invalidBidRequest = { bidder: 'notSharethrough', params: { @@ -207,22 +234,22 @@ describe('sharethrough adapter spec', function () { expect(spec.isBidRequestValid(invalidBidRequest)).to.eql(false); }); - it('should return true if req is correct', function () { + it('should return true if req is correct', function() { expect(spec.isBidRequestValid(bidRequests[0])).to.eq(true); expect(spec.isBidRequestValid(bidRequests[1])).to.eq(true); - }) + }); }); - describe('.buildRequests', function () { - it('should return an array of requests', function () { + describe('.buildRequests', function() { + it('should return an array of requests', function() { const builtBidRequests = spec.buildRequests(bidRequests); expect(builtBidRequests[0].url).to.eq('https://btlr.sharethrough.com/WYu2BXv1/v1'); expect(builtBidRequests[1].url).to.eq('https://btlr.sharethrough.com/WYu2BXv1/v1'); - expect(builtBidRequests[0].method).to.eq('GET'); + expect(builtBidRequests[0].method).to.eq('POST'); }); - it('should set the instant_play_capable parameter correctly based on browser userAgent string', function () { + it('should set the instant_play_capable parameter correctly based on browser userAgent string', function() { setUserAgent('Android Chrome/60'); let builtBidRequests = spec.buildRequests(bidRequests); expect(builtBidRequests[0].data.instant_play_capable).to.be.true; @@ -252,31 +279,31 @@ describe('sharethrough adapter spec', function () { const stub = sinon.stub(sharethroughInternal, 'getProtocol').returns('http:'); const bidRequest = spec.buildRequests(bidRequests, null)[0]; expect(bidRequest.data.secure).to.be.false; - stub.restore() + stub.restore(); }); it('should set the secure parameter to true when the protocol is https', function() { const stub = sinon.stub(sharethroughInternal, 'getProtocol').returns('https:'); const bidRequest = spec.buildRequests(bidRequests, null)[0]; expect(bidRequest.data.secure).to.be.true; - stub.restore() + stub.restore(); }); it('should set the secure parameter to true when the protocol is neither http or https', function() { const stub = sinon.stub(sharethroughInternal, 'getProtocol').returns('about:'); const bidRequest = spec.buildRequests(bidRequests, null)[0]; expect(bidRequest.data.secure).to.be.true; - stub.restore() + stub.restore(); }); - it('should add ccpa parameter if uspConsent is present', function () { + it('should add ccpa parameter if uspConsent is present', function() { const uspConsent = '1YNN'; const bidderRequest = { uspConsent: uspConsent }; const bidRequest = spec.buildRequests(bidRequests, bidderRequest)[0]; expect(bidRequest.data.us_privacy).to.eq(uspConsent); }); - it('should add consent parameters if gdprConsent is present', function () { + it('should add consent parameters if gdprConsent is present', function() { const gdprConsent = { consentString: 'consent_string123', gdprApplies: true }; const bidderRequest = { gdprConsent: gdprConsent }; const bidRequest = spec.buildRequests(bidRequests, bidderRequest)[0]; @@ -284,19 +311,37 @@ describe('sharethrough adapter spec', function () { expect(bidRequest.data.consent_string).to.eq('consent_string123'); }); - it('should handle gdprConsent is present but values are undefined case', function () { + it('should handle gdprConsent is present but values are undefined case', function() { const gdprConsent = { consent_string: undefined, gdprApplies: undefined }; const bidderRequest = { gdprConsent: gdprConsent }; const bidRequest = spec.buildRequests(bidRequests, bidderRequest)[0]; - expect(bidRequest.data).to.not.include.any.keys('consent_string') + expect(bidRequest.data).to.not.include.any.keys('consent_string'); }); - it('should add the ttduid parameter if a bid request contains a value for Unified ID from The Trade Desk', function () { + it('should add the ttduid parameter if a bid request contains a value for Unified ID from The Trade Desk', function() { const bidRequest = spec.buildRequests(bidRequests)[0]; expect(bidRequest.data.ttduid).to.eq('fake-tdid'); }); - it('should add Sharethrough specific parameters', function () { + it('should add the pubcid parameter if a bid request contains a value for the Publisher Common ID Module in the' + + ' userId object of the bidrequest', function() { + const bidRequest = spec.buildRequests(bidRequests)[0]; + expect(bidRequest.data.pubcid).to.eq('fake-pubcid'); + }); + + it('should add the pubcid parameter if a bid request contains a value for the Publisher Common ID Module in the' + + ' crumbs object of the bidrequest', function() { + const bidRequest = spec.buildRequests(bidRequests)[0]; + delete bidRequest.userId; + expect(bidRequest.data.pubcid).to.eq('fake-pubcid'); + }); + + it('should add the idluid parameter if a bid request contains a value for Identity Link from Live Ramp', function() { + const bidRequest = spec.buildRequests(bidRequests)[0]; + expect(bidRequest.data.idluid).to.eq('fake-identity-link'); + }); + + it('should add Sharethrough specific parameters', function() { const builtBidRequests = spec.buildRequests(bidRequests); expect(builtBidRequests[0]).to.deep.include({ strData: { @@ -327,6 +372,18 @@ describe('sharethrough adapter spec', function () { expect(builtBidRequest.data.schain).to.eq(JSON.stringify(bidRequest.schain)); }); + it('should add badv if provided', () => { + const builtBidRequest = spec.buildRequests([bidRequests[3]])[0]; + + expect(builtBidRequest.data.badv).to.have.members(['domain1.com', 'domain2.com']) + }); + + it('should add bcat if provided', () => { + const builtBidRequest = spec.buildRequests([bidRequests[4]])[0]; + + expect(builtBidRequest.data.bcat).to.have.members(['IAB1-1', 'IAB1-2']) + }); + it('should not add a supply chain parameter if schain is missing', function() { const bidRequest = spec.buildRequests(bidRequests)[0]; expect(bidRequest.data).to.not.include.any.keys('schain'); @@ -346,8 +403,8 @@ describe('sharethrough adapter spec', function () { }); }); - describe('.interpretResponse', function () { - it('returns a correctly parsed out response', function () { + describe('.interpretResponse', function() { + it('returns a correctly parsed out response', function() { expect(spec.interpretResponse(bidderResponse, prebidRequests[0])[0]).to.include( { width: 1, @@ -357,11 +414,11 @@ describe('sharethrough adapter spec', function () { dealId: 'aDealId', currency: 'USD', netRevenue: true, - ttl: 360, + ttl: 360 }); }); - it('returns a correctly parsed out response with largest size when strData.skipIframeBusting is true', function () { + it('returns a correctly parsed out response with largest size when strData.skipIframeBusting is true', function() { expect(spec.interpretResponse(bidderResponse, prebidRequests[1])[0]).to.include( { width: 300, @@ -371,11 +428,11 @@ describe('sharethrough adapter spec', function () { dealId: 'aDealId', currency: 'USD', netRevenue: true, - ttl: 360, + ttl: 360 }); }); - it('returns a correctly parsed out response with explicitly defined size when strData.skipIframeBusting is true and strData.iframeSize is provided', function () { + it('returns a correctly parsed out response with explicitly defined size when strData.skipIframeBusting is true and strData.iframeSize is provided', function() { expect(spec.interpretResponse(bidderResponse, prebidRequests[2])[0]).to.include( { width: 500, @@ -385,11 +442,11 @@ describe('sharethrough adapter spec', function () { dealId: 'aDealId', currency: 'USD', netRevenue: true, - ttl: 360, + ttl: 360 }); }); - it('returns a correctly parsed out response with explicitly defined size when strData.skipIframeBusting is false and strData.sizes contains [0, 0] only', function () { + it('returns a correctly parsed out response with explicitly defined size when strData.skipIframeBusting is false and strData.sizes contains [0, 0] only', function() { expect(spec.interpretResponse(bidderResponse, prebidRequests[3])[0]).to.include( { width: 0, @@ -399,11 +456,11 @@ describe('sharethrough adapter spec', function () { dealId: 'aDealId', currency: 'USD', netRevenue: true, - ttl: 360, + ttl: 360 }); }); - it('returns a correctly parsed out response with explicitly defined size when strData.skipIframeBusting is false and strData.sizes contains multiple sizes', function () { + it('returns a correctly parsed out response with explicitly defined size when strData.skipIframeBusting is false and strData.sizes contains multiple sizes', function() { expect(spec.interpretResponse(bidderResponse, prebidRequests[4])[0]).to.include( { width: 300, @@ -413,26 +470,26 @@ describe('sharethrough adapter spec', function () { dealId: 'aDealId', currency: 'USD', netRevenue: true, - ttl: 360, + ttl: 360 }); }); - it('returns a blank array if there are no creatives', function () { + it('returns a blank array if there are no creatives', function() { const bidResponse = { body: { creatives: [] } }; expect(spec.interpretResponse(bidResponse, prebidRequests[0])).to.be.an('array').that.is.empty; }); - it('returns a blank array if body object is empty', function () { + it('returns a blank array if body object is empty', function() { const bidResponse = { body: {} }; expect(spec.interpretResponse(bidResponse, prebidRequests[0])).to.be.an('array').that.is.empty; }); - it('returns a blank array if body is null', function () { + it('returns a blank array if body is null', function() { const bidResponse = { body: null }; expect(spec.interpretResponse(bidResponse, prebidRequests[0])).to.be.an('array').that.is.empty; }); - it('correctly generates ad markup when skipIframeBusting is false', function () { + it('correctly generates ad markup when skipIframeBusting is false', function() { const adMarkup = spec.interpretResponse(bidderResponse, prebidRequests[0])[0].ad; let resp = null; @@ -447,7 +504,7 @@ describe('sharethrough adapter spec', function () { expect(adMarkup).to.match(/handleIframe/); }); - it('correctly generates ad markup when skipIframeBusting is true', function () { + it('correctly generates ad markup when skipIframeBusting is true', function() { const adMarkup = spec.interpretResponse(bidderResponse, prebidRequests[1])[0].ad; let resp = null; @@ -461,11 +518,11 @@ describe('sharethrough adapter spec', function () { }); }); - describe('.getUserSyncs', function () { + describe('.getUserSyncs', function() { const cookieSyncs = ['cookieUrl1', 'cookieUrl2', 'cookieUrl3']; const serverResponses = [{ body: { cookieSyncUrls: cookieSyncs } }]; - it('returns an array of correctly formatted user syncs', function () { + it('returns an array of correctly formatted user syncs', function() { const syncArray = spec.getUserSyncs({ pixelEnabled: true }, serverResponses, null, 'fake-privacy-signal'); expect(syncArray).to.deep.equal([ { type: 'image', url: 'cookieUrl1&us_privacy=fake-privacy-signal' }, @@ -474,22 +531,22 @@ describe('sharethrough adapter spec', function () { ); }); - it('returns an empty array if serverResponses is empty', function () { + it('returns an empty array if serverResponses is empty', function() { const syncArray = spec.getUserSyncs({ pixelEnabled: true }, []); expect(syncArray).to.be.an('array').that.is.empty; }); - it('returns an empty array if the body is null', function () { + it('returns an empty array if the body is null', function() { const syncArray = spec.getUserSyncs({ pixelEnabled: true }, [{ body: null }]); expect(syncArray).to.be.an('array').that.is.empty; }); - it('returns an empty array if the body.cookieSyncUrls is missing', function () { + it('returns an empty array if the body.cookieSyncUrls is missing', function() { const syncArray = spec.getUserSyncs({ pixelEnabled: true }, [{ body: { creatives: ['creative'] } }]); expect(syncArray).to.be.an('array').that.is.empty; }); - it('returns an empty array if pixels are not enabled', function () { + it('returns an empty array if pixels are not enabled', function() { const syncArray = spec.getUserSyncs({ pixelEnabled: false }, serverResponses); expect(syncArray).to.be.an('array').that.is.empty; }); diff --git a/test/spec/modules/sizeMappingV2_spec.js b/test/spec/modules/sizeMappingV2_spec.js index 77e17ef360f..ab06ddc8f51 100644 --- a/test/spec/modules/sizeMappingV2_spec.js +++ b/test/spec/modules/sizeMappingV2_spec.js @@ -205,6 +205,18 @@ describe('sizeMappingV2', function () { expect(adUnits[0].code).to.equal('div-gpt-ad-1460505748561-1'); }); + it('should filter out adUnit if it does not contain the required property "bids"', function() { + let adUnits = utils.deepClone(AD_UNITS); + delete adUnits[0].mediaTypes; + // before checkAdUnitSetupHook is called, the length of the adUnits should equal '2' + expect(adUnits.length).to.equal(2); + adUnits = checkAdUnitSetupHook(adUnits); + + // after checkAdUnitSetupHook is called, the length of the adUnits should be '1' + expect(adUnits.length).to.equal(1); + expect(adUnits[0].code).to.equal('div-gpt-ad-1460505748561-1'); + }); + it('should filter out adUnit if it has declared property mediaTypes with an empty object', function () { let adUnits = utils.deepClone(AD_UNITS); adUnits[0].mediaTypes = {}; @@ -657,7 +669,7 @@ describe('sizeMappingV2', function () { }, native: {} }, - bids: [] + bids: [{bidder: 'appnexus', params: 1234}] }]; checkAdUnitSetupHook(adUnit); @@ -679,7 +691,7 @@ describe('sizeMappingV2', function () { }, native: {} }, - bids: [] + bids: [{bidder: 'appnexus', params: 1234}] }]; checkAdUnitSetupHook(adUnit); @@ -698,7 +710,7 @@ describe('sizeMappingV2', function () { mediaTypes: { native: {} }, - bids: [] + bids: [{bidder: 'appnexus', params: 1234}] }]; checkAdUnitSetupHook(adUnit); diff --git a/test/spec/modules/smaatoBidAdapter_spec.js b/test/spec/modules/smaatoBidAdapter_spec.js index 95eb36d8a0d..6af0a855800 100644 --- a/test/spec/modules/smaatoBidAdapter_spec.js +++ b/test/spec/modules/smaatoBidAdapter_spec.js @@ -1,6 +1,7 @@ import { spec } from 'modules/smaatoBidAdapter.js'; import * as utils from 'src/utils.js'; import {config} from 'src/config.js'; +import {createEidsArray} from 'modules/userId/eids.js'; const imageAd = { image: { @@ -291,6 +292,67 @@ const combinedBannerAndVideoBidRequest = { bidderWinsCount: 0 }; +const inAppBidRequest = { + bidder: 'smaato', + params: { + publisherId: 'publisherId', + adspaceId: 'adspaceId', + app: { + ifa: 'aDeviceId', + geo: { + lat: 33.3, + lon: -88.8 + } + } + }, + mediaTypes: { + banner: { + sizes: [[300, 50]] + } + }, + adUnitCode: '/19968336/header-bid-tag-0', + transactionId: 'transactionId', + sizes: [[300, 50]], + bidId: 'bidId', + bidderRequestId: 'bidderRequestId', + auctionId: 'auctionId', + src: 'client', + bidRequestsCount: 1, + bidderRequestsCount: 1, + bidderWinsCount: 0 +}; + +const userIdBidRequest = { + bidder: 'smaato', + params: { + publisherId: 'publisherId', + adspaceId: 'adspaceId' + }, + mediaTypes: { + banner: { + sizes: [[300, 50]] + } + }, + adUnitCode: '/19968336/header-bid-tag-0', + transactionId: 'transactionId', + sizes: [[300, 50]], + bidId: 'bidId', + bidderRequestId: 'bidderRequestId', + auctionId: 'auctionId', + src: 'client', + bidRequestsCount: 1, + bidderRequestsCount: 1, + bidderWinsCount: 0, + userId: { + criteoId: '123456', + tdid: '89145' + }, + userIdAsEids: createEidsArray({ + criteoId: '123456', + tdid: '89145' + }) +}; + describe('smaatoBidAdapterTest', () => { describe('isBidRequestValid', () => { it('has valid params', () => { @@ -392,7 +454,11 @@ describe('smaatoBidAdapterTest', () => { expect(req_fpd.user.ext.consent).to.equal('HFIDUYFIUYIUYWIPOI87392DSU'); expect(req_fpd.site.keywords).to.eql('power tools,drills'); expect(req_fpd.site.publisher.id).to.equal('publisherId'); - }) + }); + + it('has no user ids', () => { + expect(this.req.user.ext.eids).to.not.exist; + }); }); describe('buildRequests for video imps', () => { @@ -462,6 +528,35 @@ describe('smaatoBidAdapterTest', () => { }); }); + describe('in-app requests', () => { + it('add geo and ifa info to device object', () => { + let req = JSON.parse(spec.buildRequests([inAppBidRequest], defaultBidderRequest).data); + expect(req.device.geo).to.deep.equal({'lat': 33.3, 'lon': -88.8}); + expect(req.device.ifa).to.equal('aDeviceId'); + }); + it('add only ifa to device object', () => { + let inAppBidRequestWithoutGeo = utils.deepClone(inAppBidRequest); + delete inAppBidRequestWithoutGeo.params.app.geo + let req = JSON.parse(spec.buildRequests([inAppBidRequestWithoutGeo], defaultBidderRequest).data); + + expect(req.device.geo).to.not.exist; + expect(req.device.ifa).to.equal('aDeviceId'); + }); + it('add no specific device info if param does not exist', () => { + let req = JSON.parse(spec.buildRequests([singleBannerBidRequest], defaultBidderRequest).data); + expect(req.device.geo).to.not.exist; + expect(req.device.ifa).to.not.exist; + }); + }); + + describe('user ids in requests', () => { + it('user ids are added to user.ext.eids', () => { + let req = JSON.parse(spec.buildRequests([userIdBidRequest], defaultBidderRequest).data); + expect(req.user.ext.eids).to.exist; + expect(req.user.ext.eids).to.have.length(2); + }); + }); + describe('interpretResponse', () => { it('single image reponse', () => { const bids = spec.interpretResponse(openRtbBidResponse(ADTYPE_IMG), request); @@ -501,5 +596,11 @@ describe('smaatoBidAdapterTest', () => { expect(bids[0].ttl).to.equal(400); clock.restore(); }); + it('uses net revenue flag send from server', () => { + let resp = openRtbBidResponse(ADTYPE_IMG); + resp.body.seatbid[0].bid[0].ext = {net: false}; + const bids = spec.interpretResponse(resp, request); + expect(bids[0].netRevenue).to.equal(false); + }) }); }); diff --git a/test/spec/modules/smartadserverBidAdapter_spec.js b/test/spec/modules/smartadserverBidAdapter_spec.js index 2faad47aca8..e3bca240a47 100644 --- a/test/spec/modules/smartadserverBidAdapter_spec.js +++ b/test/spec/modules/smartadserverBidAdapter_spec.js @@ -71,7 +71,7 @@ describe('Smart bid adapter tests', function () { britepoolid: '1111', criteoId: '1111', digitrustid: { data: { id: 'DTID', keyv: 4, privacy: { optout: false }, producer: 'ABC', version: 2 } }, - id5id: '1111', + id5id: { uid: '1111' }, idl_env: '1111', lipbid: '1111', parrableid: 'eidVersion.encryptionKeyReference.encryptedValue', @@ -425,6 +425,7 @@ describe('Smart bid adapter tests', function () { expect(bid.mediaType).to.equal('video'); expect(bid.vastUrl).to.equal('http://awesome.fake-vast.url'); expect(bid.vastXml).to.equal(''); + expect(bid.content).to.equal(''); expect(bid.width).to.equal(640); expect(bid.height).to.equal(480); expect(bid.creativeId).to.equal('zioeufg'); @@ -473,6 +474,109 @@ describe('Smart bid adapter tests', function () { }); }); + describe('Outstream video tests', function () { + afterEach(function () { + config.resetConfig(); + $$PREBID_GLOBAL$$.requestBids.removeAll(); + }); + + const OUTSTREAM_DEFAULT_PARAMS = [{ + adUnitCode: 'sas_43', + bidId: 'abcd1234', + bidder: 'smartadserver', + mediaTypes: { + video: { + context: 'outstream', + playerSize: [[800, 600]] // It seems prebid.js transforms the player size array into an array of array... + } + }, + params: { + siteId: '1234', + pageId: '5678', + formatId: '91', + target: 'test=prebid-outstream', + bidfloor: 0.430, + buId: '7579', + appName: 'Mozilla', + ckId: 43, + video: { + protocol: 7 + } + }, + requestId: 'efgh5679', + transactionId: 'zsfgzzga' + }]; + + var OUTSTREAM_BID_RESPONSE = { + body: { + cpm: 14, + width: 800, + height: 600, + creativeId: 'zioeufga', + currency: 'USD', + isNetCpm: true, + ttl: 300, + adUrl: 'http://awesome.fake-vast2.url', + ad: '', + cSyncUrl: 'http://awesome.fake2.csync.url' + } + }; + + it('Verify outstream video build request', function () { + config.setConfig({ + 'currency': { + 'adServerCurrency': 'EUR' + } + }); + const request = spec.buildRequests(OUTSTREAM_DEFAULT_PARAMS); + expect(request[0]).to.have.property('url').and.to.equal('https://prg.smartadserver.com/prebid/v1'); + expect(request[0]).to.have.property('method').and.to.equal('POST'); + const requestContent = JSON.parse(request[0].data); + expect(requestContent).to.have.property('siteid').and.to.equal('1234'); + expect(requestContent).to.have.property('pageid').and.to.equal('5678'); + expect(requestContent).to.have.property('formatid').and.to.equal('91'); + expect(requestContent).to.have.property('currencyCode').and.to.equal('EUR'); + expect(requestContent).to.have.property('bidfloor').and.to.equal(0.43); + expect(requestContent).to.have.property('targeting').and.to.equal('test=prebid-outstream'); + expect(requestContent).to.have.property('tagId').and.to.equal('sas_43'); + expect(requestContent).to.not.have.property('pageDomain'); + expect(requestContent).to.have.property('transactionId').and.to.not.equal(null).and.to.not.be.undefined; + expect(requestContent).to.have.property('buid').and.to.equal('7579'); + expect(requestContent).to.have.property('appname').and.to.equal('Mozilla'); + expect(requestContent).to.have.property('ckid').and.to.equal(43); + expect(requestContent).to.have.property('isVideo').and.to.equal(false); + expect(requestContent).to.have.property('videoData'); + expect(requestContent.videoData).to.have.property('videoProtocol').and.to.equal(7); + expect(requestContent.videoData).to.have.property('playerWidth').and.to.equal(800); + expect(requestContent.videoData).to.have.property('playerHeight').and.to.equal(600); + }); + + it('Verify outstream parse response', function () { + const request = spec.buildRequests(OUTSTREAM_DEFAULT_PARAMS); + const bids = spec.interpretResponse(OUTSTREAM_BID_RESPONSE, request[0]); + expect(bids).to.have.lengthOf(1); + const bid = bids[0]; + expect(bid.cpm).to.equal(14); + expect(bid.mediaType).to.equal('video'); + expect(bid.vastUrl).to.equal('http://awesome.fake-vast2.url'); + expect(bid.vastXml).to.equal(''); + expect(bid.content).to.equal(''); + expect(bid.width).to.equal(800); + expect(bid.height).to.equal(600); + expect(bid.creativeId).to.equal('zioeufga'); + expect(bid.currency).to.equal('USD'); + expect(bid.netRevenue).to.equal(true); + expect(bid.ttl).to.equal(300); + expect(bid.requestId).to.equal(OUTSTREAM_DEFAULT_PARAMS[0].bidId); + + expect(function () { + spec.interpretResponse(OUTSTREAM_BID_RESPONSE, { + data: 'invalid Json' + }) + }).to.not.throw(); + }); + }); + describe('External ids tests', function () { it('Verify external ids in request and ids found', function () { config.setConfig({ diff --git a/test/spec/modules/sovrnBidAdapter_spec.js b/test/spec/modules/sovrnBidAdapter_spec.js index 54ccff870eb..2bb5cdbdf3c 100644 --- a/test/spec/modules/sovrnBidAdapter_spec.js +++ b/test/spec/modules/sovrnBidAdapter_spec.js @@ -239,8 +239,8 @@ describe('sovrnBidAdapter', function() { expect(data.source.ext.schain.nodes.length).to.equal(1) }); - it('should add the unifiedID if present', function() { - const digitrustRequests = [{ + it('should add ids to the bid request', function() { + const criteoIdRequest = [{ 'bidder': 'sovrn', 'params': { 'tagid': 403370 @@ -254,6 +254,7 @@ describe('sovrnBidAdapter', function() { 'bidderRequestId': '22edbae2733bf6', 'auctionId': '1d1a030790a475', 'userId': { + 'criteoId': 'A_CRITEO_ID', 'tdid': 'SOMESORTOFID', } }].concat(bidRequests); @@ -263,12 +264,46 @@ describe('sovrnBidAdapter', function() { } }; - const data = JSON.parse(spec.buildRequests(digitrustRequests, bidderRequest).data); - expect(data.user.ext.eids[0].source).to.equal('adserver.org') - expect(data.user.ext.eids[0].uids[0].id).to.equal('SOMESORTOFID') - expect(data.user.ext.eids[0].uids[0].ext.rtiPartner).to.equal('TDID') + const data = JSON.parse(spec.buildRequests(criteoIdRequest, bidderRequest).data); + expect(data.user.ext.eids[0].source).to.equal('criteo.com') + expect(data.user.ext.eids[0].uids[0].id).to.equal('A_CRITEO_ID') + expect(data.user.ext.eids[0].uids[0].atype).to.equal(1) + expect(data.user.ext.eids[1].source).to.equal('adserver.org') + expect(data.user.ext.eids[1].uids[0].id).to.equal('SOMESORTOFID') + expect(data.user.ext.eids[1].uids[0].ext.rtiPartner).to.equal('TDID') + expect(data.user.ext.eids[1].uids[0].atype).to.equal(1) + expect(data.user.ext.tpid[0].source).to.equal('criteo.com') + expect(data.user.ext.tpid[0].uid).to.equal('A_CRITEO_ID') + expect(data.user.ext.prebid_criteoid).to.equal('A_CRITEO_ID') + }); + + it('should ignore empty segments', function() { + const payload = JSON.parse(request.data) + expect(payload.imp[0].ext).to.be.undefined + }) + + it('should pass the segments param value as trimmed deal ids array', function() { + const segmentsRequests = [{ + 'bidder': 'sovrn', + 'params': { + 'segments': ' test1,test2 ' + }, + 'adUnitCode': 'adunit-code', + 'sizes': [ + [300, 250], + [300, 600] + ], + 'bidId': '30b31c1838de1e', + 'bidderRequestId': '22edbae2733bf6', + 'auctionId': '1d1a030790a475' + }]; + const request = spec.buildRequests(segmentsRequests, bidderRequest) + const payload = JSON.parse(request.data) + expect(payload.imp[0].ext.deals[0]).to.equal('test1') + expect(payload.imp[0].ext.deals[1]).to.equal('test2') }) }); + describe('interpretResponse', function () { let response; beforeEach(function () { @@ -322,16 +357,16 @@ describe('sovrnBidAdapter', function() { 'currency': 'USD', 'netRevenue': true, 'mediaType': 'banner', - 'ad': decodeURIComponent(`>`), - 'ttl': 60000 + 'ad': decodeURIComponent(``), + 'ttl': 90 }]; let result = spec.interpretResponse(response); - expect(Object.keys(result[0])).to.deep.equal(Object.keys(expectedResponse[0])); + expect(result[0]).to.deep.equal(expectedResponse[0]); }); it('should get correct bid response when dealId is passed', function () { - response.body.dealid = 'baking'; + response.body.seatbid[0].bid[0].dealid = 'baking'; let expectedResponse = [{ 'requestId': '263c448586f5a1', @@ -343,12 +378,33 @@ describe('sovrnBidAdapter', function() { 'currency': 'USD', 'netRevenue': true, 'mediaType': 'banner', - 'ad': decodeURIComponent(`>`), - 'ttl': 60000 + 'ad': decodeURIComponent(``), + 'ttl': 90 }]; let result = spec.interpretResponse(response); - expect(Object.keys(result[0])).to.deep.equal(Object.keys(expectedResponse[0])); + expect(result[0]).to.deep.equal(expectedResponse[0]); + }); + + it('should get correct bid response when ttl is set', function () { + response.body.seatbid[0].bid[0].ext = { 'ttl': 480 }; + + let expectedResponse = [{ + 'requestId': '263c448586f5a1', + 'cpm': 0.45882675, + 'width': 728, + 'height': 90, + 'creativeId': 'creativelycreatedcreativecreative', + 'dealId': null, + 'currency': 'USD', + 'netRevenue': true, + 'mediaType': 'banner', + 'ad': decodeURIComponent(``), + 'ttl': 480 + }]; + + let result = spec.interpretResponse(response); + expect(result[0]).to.deep.equal(expectedResponse[0]); }); it('handles empty bid response', function () { diff --git a/test/spec/modules/spotxBidAdapter_spec.js b/test/spec/modules/spotxBidAdapter_spec.js index baece710ee1..798fb3eec10 100644 --- a/test/spec/modules/spotxBidAdapter_spec.js +++ b/test/spec/modules/spotxBidAdapter_spec.js @@ -1,4 +1,5 @@ import {expect} from 'chai'; +import {config} from 'src/config.js'; import {spec, GOOGLE_CONSENT} from 'modules/spotxBidAdapter.js'; describe('the spotx adapter', function () { @@ -89,6 +90,7 @@ describe('the spotx adapter', function () { expect(spec.isBidRequestValid(bid)).to.equal(false); }); }); + describe('buildRequests', function() { var bid, bidRequestObj; @@ -125,6 +127,7 @@ describe('the spotx adapter', function () { page: 'prebid.js' }); }); + it('should change request parameters based on options sent', function() { var request = spec.buildRequests([bid], bidRequestObj)[0]; expect(request.data.imp.video.ext).to.deep.equal({ @@ -152,7 +155,7 @@ describe('the spotx adapter', function () { }; bid.userId = { - id5id: 'id5id_1', + id5id: { uid: 'id5id_1' }, tdid: 'tdid_1' }; @@ -202,7 +205,8 @@ describe('the spotx adapter', function () { source: 'id5-sync.com', uids: [{ id: 'id5id_1' - }] + }], + ext: {} }, { source: 'adserver.org', @@ -331,6 +335,50 @@ describe('the spotx adapter', function () { expect(request.data.imp.video.ext.placement).to.equal(2); expect(request.data.imp.video.ext.pos).to.equal(5); }); + + it('should pass page param and override refererInfo.referer', function() { + var request; + + bid.params.page = 'https://example.com'; + + var origGetConfig = config.getConfig; + sinon.stub(config, 'getConfig').callsFake(function (key) { + if (key === 'pageUrl') { + return 'https://www.spotx.tv'; + } + return origGetConfig.apply(config, arguments); + }); + + request = spec.buildRequests([bid], bidRequestObj)[0]; + + expect(request.data.site.page).to.equal('https://example.com'); + config.getConfig.restore(); + }); + + it('should use pageUrl from config if page param is not passed', function() { + var request; + + var origGetConfig = config.getConfig; + sinon.stub(config, 'getConfig').callsFake(function (key) { + if (key === 'pageUrl') { + return 'https://www.spotx.tv'; + } + return origGetConfig.apply(config, arguments); + }); + + request = spec.buildRequests([bid], bidRequestObj)[0]; + + expect(request.data.site.page).to.equal('https://www.spotx.tv'); + config.getConfig.restore(); + }); + + it('should use refererInfo.referer if no page or pageUrl are passed', function() { + var request; + + request = spec.buildRequests([bid], bidRequestObj)[0]; + + expect(request.data.site.page).to.equal('prebid.js'); + }); }); describe('interpretResponse', function() { diff --git a/test/spec/modules/sspBCAdapter_spec.js b/test/spec/modules/sspBCAdapter_spec.js index 2cb0e8defa4..29718deb031 100644 --- a/test/spec/modules/sspBCAdapter_spec.js +++ b/test/spec/modules/sspBCAdapter_spec.js @@ -1,6 +1,8 @@ import { assert, expect } from 'chai'; import { spec } from 'modules/sspBCAdapter.js'; import * as utils from 'src/utils.js'; +import * as sinon from 'sinon'; +import * as ajax from 'src/ajax.js'; const BIDDER_CODE = 'sspBC'; const BIDDER_URL = 'https://ssp.wp.pl/bidder/'; @@ -60,10 +62,33 @@ describe('SSPBC adapter', function () { }, auctionId, bidderRequestId, - bidId: auctionId + '1', + bidId: auctionId + '2', transactionId, } ]; + const bids_timeouted = [{ + adUnitCode: 'test_wideboard', + bidder: BIDDER_CODE, + params: [{ + id: '003', + siteId: '8816', + }], + auctionId, + bidId: auctionId + '1', + timeout: 100, + }, + { + adUnitCode: 'test_rectangle', + bidder: BIDDER_CODE, + params: [{ + id: '005', + siteId: '8816', + }], + auctionId, + bidId: auctionId + '2', + timeout: 100, + } + ]; const bids_test = [{ adUnitCode: 'test_wideboard', bidder: BIDDER_CODE, @@ -209,6 +234,7 @@ describe('SSPBC adapter', function () { return { bids, bids_test, + bids_timeouted, bidRequest, bidRequestSingle, bidRequestTest, @@ -323,7 +349,7 @@ describe('SSPBC adapter', function () { it('should provide correct url, if frame sync is allowed', function () { expect(syncResultAll).to.have.length(1); - expect(syncResultAll[0].url).to.be.equal(SYNC_URL); + expect(syncResultAll[0].url).to.have.string(SYNC_URL); }); it('should send no syncs, if frame sync is not allowed', function () { @@ -331,4 +357,44 @@ describe('SSPBC adapter', function () { expect(syncResultNone).to.be.undefined; }); }); + + describe('onBidWon', function () { + it('should generate no notification if bid is undefined', function () { + let notificationPayload = spec.onBidWon(); + expect(notificationPayload).to.be.undefined; + }); + + it('should generate notification with event name and request/site/slot data, if correct bid is provided', function () { + const { bids } = prepareTestData(); + let bid = bids[0]; + bid.params = [bid.params]; + + let notificationPayload = spec.onBidWon(bid); + expect(notificationPayload).to.have.property('event').that.equals('bidWon'); + expect(notificationPayload).to.have.property('requestId').that.equals(bid.auctionId); + expect(notificationPayload).to.have.property('siteId').that.deep.equals([bid.params[0].siteId]); + expect(notificationPayload).to.have.property('slotId').that.deep.equals([bid.params[0].id]); + }); + }); + + describe('onTimeout', function () { + it('should generate no notification if timeout data is undefined / has no bids', function () { + let notificationPayloadUndefined = spec.onTimeout(); + let notificationPayloadNoBids = spec.onTimeout([]); + + expect(notificationPayloadUndefined).to.be.undefined; + expect(notificationPayloadNoBids).to.be.undefined; + }); + + it('should generate single notification for any number of timeouted bids', function () { + const { bids_timeouted } = prepareTestData(); + + let notificationPayload = spec.onTimeout(bids_timeouted); + + expect(notificationPayload).to.have.property('event').that.equals('timeout'); + expect(notificationPayload).to.have.property('requestId').that.equals(bids_timeouted[0].auctionId); + expect(notificationPayload).to.have.property('siteId').that.deep.equals([bids_timeouted[0].params[0].siteId]); + expect(notificationPayload).to.have.property('slotId').that.deep.equals([bids_timeouted[0].params[0].id, bids_timeouted[1].params[0].id]); + }); + }); }); diff --git a/test/spec/modules/stroeerCoreBidAdapter_spec.js b/test/spec/modules/stroeerCoreBidAdapter_spec.js new file mode 100644 index 00000000000..dd02ebb7c8e --- /dev/null +++ b/test/spec/modules/stroeerCoreBidAdapter_spec.js @@ -0,0 +1,532 @@ +import {assert} from 'chai'; +import {spec} from 'modules/stroeerCoreBidAdapter.js'; +import * as utils from 'src/utils.js'; +import {BANNER, VIDEO} from '../../../src/mediaTypes.js'; + +describe('stroeerCore bid adapter', function () { + let sandbox; + let fakeServer; + let bidderRequest; + let clock; + + beforeEach(() => { + bidderRequest = buildBidderRequest(); + sandbox = sinon.sandbox.create(); + fakeServer = sandbox.useFakeServer(); + clock = sandbox.useFakeTimers(); + }); + + afterEach(() => { + sandbox.restore(); + }); + + function assertStandardFieldsOnBid(bidObject, bidId, ad, width, height, cpm) { + assert.propertyVal(bidObject, 'requestId', bidId); + assert.propertyVal(bidObject, 'ad', ad); + assert.propertyVal(bidObject, 'width', width); + assert.propertyVal(bidObject, 'height', height); + assert.propertyVal(bidObject, 'cpm', cpm); + assert.propertyVal(bidObject, 'currency', 'EUR'); + assert.propertyVal(bidObject, 'netRevenue', true); + assert.propertyVal(bidObject, 'creativeId', ''); + } + + const AUCTION_ID = utils.getUniqueIdentifierStr(); + + // Vendor user ids and associated data + const userIds = Object.freeze({ + criteoId: 'criteo-user-id', + digitrustid: { + data: { + id: 'encrypted-user-id==', + keyv: 4, + privacy: {optout: false}, + producer: 'ABC', + version: 2 + } + }, + lipb: { + lipbid: 'T7JiRRvsRAmh88', + segments: ['999'] + } + }); + + const buildBidderRequest = () => ({ + auctionId: AUCTION_ID, + bidderRequestId: 'bidder-request-id-123', + bidderCode: 'stroeerCore', + timeout: 5000, + auctionStart: 10000, + bids: [{ + bidId: 'bid1', + bidder: 'stroeerCore', + adUnitCode: 'div-1', + mediaTypes: { + banner: { + sizes: [[300, 600], [160, 60]] + } + }, + params: { + sid: 'NDA=' + }, + userId: userIds + }, { + bidId: 'bid2', + bidder: 'stroeerCore', + adUnitCode: 'div-2', + mediaTypes: { + banner: { + sizes: [[728, 90]], + } + }, + params: { + sid: 'ODA=' + }, + userId: userIds + }], + }); + + const buildBidderRequestPreVersion3 = () => { + const request = buildBidderRequest(); + request.bids.forEach((bid) => { + bid.sizes = bid.mediaTypes.banner.sizes; + delete bid.mediaTypes; + bid.mediaType = 'banner'; + }); + return request; + }; + + const buildBidderResponse = () => ({ + 'bids': [{ + 'bidId': 'bid1', 'cpm': 4.0, 'width': 300, 'height': 600, 'ad': '
tag1
', 'tracking': {'brandId': 123} + }, { + 'bidId': 'bid2', 'cpm': 7.3, 'width': 728, 'height': 90, 'ad': '
tag2
' + }] + }); + + const createWindow = (href, params = {}) => { + let {parent, referrer, top, frameElement, placementElements = []} = params; + + const protocol = (href.indexOf('https') === 0) ? 'https:' : 'http:'; + const win = { + frameElement, + parent, + top, + location: { + protocol, href + }, + document: { + createElement: function () { + return { + setAttribute: function () { + } + } + }, + referrer, + getElementById: id => placementElements.find(el => el.id === id) + } + }; + + win.self = win; + + if (!parent) { + win.parent = win; + } + + if (!top) { + win.top = win; + } + + return win; + }; + + function createElement(id, offsetTop = 0) { + return { + id, + getBoundingClientRect: function () { + return { + top: offsetTop, height: 1 + } + } + } + } + + function setupSingleWindow(sandBox, placementElements = [createElement('div-1', 17), createElement('div-2', 54)]) { + const win = createWindow('http://www.xyz.com/', { + parent: win, top: win, frameElement: createElement(undefined, 304), placementElements: placementElements + }); + + win.innerHeight = 200; + + sandBox.stub(utils, 'getWindowSelf').returns(win); + sandBox.stub(utils, 'getWindowTop').returns(win); + + return win; + } + + function setupNestedWindows(sandBox, placementElements = [createElement('div-1', 17), createElement('div-2', 54)]) { + const topWin = createWindow('http://www.abc.org/', {referrer: 'http://www.google.com/?query=monkey'}); + topWin.innerHeight = 800; + + const midWin = createWindow('http://www.abc.org/', {parent: topWin, top: topWin, frameElement: createElement()}); + midWin.innerHeight = 400; + + const win = createWindow('http://www.xyz.com/', { + parent: midWin, top: topWin, frameElement: createElement(undefined, 304), placementElements + }); + + win.innerHeight = 200; + + sandBox.stub(utils, 'getWindowSelf').returns(win); + sandBox.stub(utils, 'getWindowTop').returns(topWin); + + return {topWin, midWin, win}; + } + + it('should only support BANNER mediaType', function () { + assert.deepEqual(spec.supportedMediaTypes, [BANNER]); + }); + + describe('bid validation entry point', () => { + let bidRequest; + + beforeEach(() => { + bidRequest = buildBidderRequest().bids[0]; + }); + + it('should have \"isBidRequestValid\" function', () => { + assert.isFunction(spec.isBidRequestValid); + }); + + it('should pass a valid bid', () => { + assert.isTrue(spec.isBidRequestValid(bidRequest)); + }); + + it('should exclude bids without slot id param', () => { + bidRequest.params.sid = undefined; + assert.isFalse(spec.isBidRequestValid(bidRequest)); + }); + + it('should exclude non-banner bids', () => { + delete bidRequest.mediaTypes.banner; + bidRequest.mediaTypes.video = { + playerSize: [640, 480] + }; + + assert.isFalse(spec.isBidRequestValid(bidRequest)); + }); + + it('should exclude non-banner, pre-version 3 bids', () => { + delete bidRequest.mediaTypes; + bidRequest.mediaType = VIDEO; + assert.isFalse(spec.isBidRequestValid(bidRequest)); + }); + }); + + describe('build request entry point', () => { + it('should have \"buildRequests\" function', () => { + assert.isFunction(spec.buildRequests); + }); + + describe('url on server request info object', () => { + let win; + beforeEach(() => { + win = setupSingleWindow(sandbox); + }); + + afterEach(() => { + sandbox.restore(); + }); + + it('should use hardcoded url as default endpoint', () => { + const bidReq = buildBidderRequest(); + let serverRequestInfo = spec.buildRequests(bidReq.bids, bidReq); + + assert.equal(serverRequestInfo.method, 'POST'); + assert.isObject(serverRequestInfo.data); + assert.equal(serverRequestInfo.url, 'https://hb.adscale.de/dsh'); + }); + + describe('should use custom url if provided', () => { + const samples = [{ + protocol: 'http:', params: {sid: 'ODA=', host: 'other.com', port: '234', path: '/xyz'}, expected: 'https://other.com:234/xyz' + }, { + protocol: 'https:', params: {sid: 'ODA=', host: 'other.com', port: '234', path: '/xyz'}, expected: 'https://other.com:234/xyz' + }, { + protocol: 'https:', + params: {sid: 'ODA=', host: 'other.com', port: '234', securePort: '871', path: '/xyz'}, + expected: 'https://other.com:871/xyz' + }, { + protocol: 'http:', params: {sid: 'ODA=', port: '234', path: '/xyz'}, expected: 'https://hb.adscale.de:234/xyz' + }, ]; + + samples.forEach(sample => { + it(`should use ${sample.expected} as endpoint when given params ${JSON.stringify(sample.params)} and protocol ${sample.protocol}`, + function () { + win.location.protocol = sample.protocol; + + const bidReq = buildBidderRequest(); + bidReq.bids[0].params = sample.params; + bidReq.bids.length = 1; + + let serverRequestInfo = spec.buildRequests(bidReq.bids, bidReq); + + assert.equal(serverRequestInfo.method, 'POST'); + assert.isObject(serverRequestInfo.data); + assert.equal(serverRequestInfo.url, sample.expected); + }); + }); + }); + }); + + describe('payload on server request info object', () => { + let topWin; + let win; + + let placementElements; + beforeEach(() => { + placementElements = [createElement('div-1', 17), createElement('div-2', 54)]; + ({ topWin, win } = setupNestedWindows(sandbox, placementElements)); + }); + + afterEach(() => { + sandbox.restore(); + }); + + it('should have expected JSON structure', () => { + clock.tick(13500); + const bidReq = buildBidderRequest(); + + const serverRequestInfo = spec.buildRequests(bidReq.bids, bidReq); + + const expectedTimeout = bidderRequest.timeout - (13500 - bidderRequest.auctionStart); + + assert.equal(expectedTimeout, 1500); + + const expectedJsonPayload = { + 'id': AUCTION_ID, + 'timeout': expectedTimeout, + 'ref': topWin.document.referrer, + 'mpa': true, + 'ssl': false, + 'bids': [{ + 'sid': 'NDA=', 'bid': 'bid1', 'siz': [[300, 600], [160, 60]], 'viz': true + }, { + 'sid': 'ODA=', 'bid': 'bid2', 'siz': [[728, 90]], 'viz': true + }], + 'user': { + 'euids': userIds + } + }; + + // trim away fields with undefined + const actualJsonPayload = JSON.parse(JSON.stringify(serverRequestInfo.data)); + + assert.deepEqual(actualJsonPayload, expectedJsonPayload); + }); + + it('should handle banner sizes for pre version 3', () => { + // Version 3 changes the way how banner sizes are accessed. + // We can support backwards compatibility with version 2.x + const bidReq = buildBidderRequestPreVersion3(); + const serverRequestInfo = spec.buildRequests(bidReq.bids, bidReq); + assert.deepEqual(serverRequestInfo.data.bids[0].siz, [[300, 600], [160, 60]]); + assert.deepEqual(serverRequestInfo.data.bids[1].siz, [[728, 90]]); + }); + + describe('optional fields', () => { + it('should skip viz field when unable to determine visibility of placement', () => { + placementElements.length = 0; + const bidReq = buildBidderRequest(); + + const serverRequestInfo = spec.buildRequests(bidReq.bids, bidReq); + assert.lengthOf(serverRequestInfo.data.bids, 2); + + for (let bid of serverRequestInfo.data.bids) { + assert.isUndefined(bid.viz); + } + }); + + it('should skip ref field when unable to determine document referrer', () => { + // i.e., empty if user came from bookmark, or web page using 'rel="noreferrer" on link, etc + buildBidderRequest(); + + const serverRequestInfo = spec.buildRequests(bidderRequest.bids, bidderRequest); + assert.lengthOf(serverRequestInfo.data.bids, 2); + + for (let bid of serverRequestInfo.data.bids) { + assert.isUndefined(bid.ref); + } + }); + + const gdprSamples = [{consentString: 'RG9ua2V5IEtvbmc=', gdprApplies: true}, {consentString: 'UGluZyBQb25n', gdprApplies: false}]; + gdprSamples.forEach((sample) => { + it(`should add GDPR info ${JSON.stringify(sample)} when provided`, () => { + const bidReq = buildBidderRequest(); + bidReq.gdprConsent = sample; + + const serverRequestInfo = spec.buildRequests(bidReq.bids, bidReq); + + const actualGdpr = serverRequestInfo.data.gdpr; + assert.propertyVal(actualGdpr, 'applies', sample.gdprApplies); + assert.propertyVal(actualGdpr, 'consent', sample.consentString); + }); + }); + + const skippableGdprSamples = [{consentString: null, gdprApplies: true}, // + {consentString: 'UGluZyBQb25n', gdprApplies: null}, // + {consentString: null, gdprApplies: null}, // + {consentString: undefined, gdprApplies: true}, // + {consentString: 'UGluZyBQb25n', gdprApplies: undefined}, // + {consentString: undefined, gdprApplies: undefined}]; + skippableGdprSamples.forEach((sample) => { + it(`should not add GDPR info ${JSON.stringify(sample)} when one or more values are missing`, () => { + const bidReq = buildBidderRequest(); + bidReq.gdprConsent = sample; + + const serverRequestInfo = spec.buildRequests(bidReq.bids, bidReq); + + const actualGdpr = serverRequestInfo.data.gdpr; + assert.isUndefined(actualGdpr); + }); + }); + + it('should be able to build without third party user id data', () => { + const bidReq = buildBidderRequest(); + bidReq.bids.forEach(bid => delete bid.userId); + const serverRequestInfo = spec.buildRequests(bidReq.bids, bidReq); + assert.lengthOf(serverRequestInfo.data.bids, 2); + assert.notProperty(serverRequestInfo, 'uids'); + }); + }); + }); + }); + + describe('interpret response entry point', () => { + it('should have \"interpretResponse\" function', () => { + assert.isFunction(spec.interpretResponse); + }); + + const invalidResponses = ['', ' ', ' ', undefined, null]; + invalidResponses.forEach(sample => { + it('should ignore invalid responses (\"' + sample + '\") response', () => { + const result = spec.interpretResponse({body: sample}); + assert.isArray(result); + assert.lengthOf(result, 0); + }); + }); + + it('should interpret a standard response', () => { + const bidderResponse = buildBidderResponse(); + + const result = spec.interpretResponse({body: bidderResponse}); + assertStandardFieldsOnBid(result[0], 'bid1', '
tag1
', 300, 600, 4); + assertStandardFieldsOnBid(result[1], 'bid2', '
tag2
', 728, 90, 7.3); + }); + + it('should return empty array, when response contains no bids', () => { + const result = spec.interpretResponse({body: {bids: []}}); + assert.deepStrictEqual(result, []); + }); + }); + + describe('get user syncs entry point', () => { + let win; + beforeEach(() => { + win = setupSingleWindow(sandbox); + + // fake + win.document.createElement = function () { + const attrs = {}; + return { + setAttribute: (name, value) => { + attrs[name] = value + }, + getAttribute: (name) => attrs[name], + hasAttribute: (name) => attrs[name] !== undefined, + tagName: 'SCRIPT', + } + } + }); + + it('should have \"getUserSyncs\" function', () => { + assert.isFunction(spec.getUserSyncs); + }); + + describe('when iframe option is enabled', () => { + it('should perform user connect when there was a response', () => { + const expectedUrl = 'https://js.adscale.de/pbsync.html'; + const userSyncResponse = spec.getUserSyncs({iframeEnabled: true}, ['']); + + assert.deepStrictEqual(userSyncResponse, [{type: 'iframe', url: expectedUrl}]); + }); + + it('should not perform user connect when there was no response', () => { + const userSyncResponse = spec.getUserSyncs({iframeEnabled: true}, []); + + assert.deepStrictEqual(userSyncResponse, []); + }); + + describe('and gdpr consent is defined', () => { + describe('and gdpr applies', () => { + it('should place gdpr query param to the user sync url with value of 1', () => { + const expectedUrl = 'https://js.adscale.de/pbsync.html?gdpr=1&gdpr_consent='; + const userSyncResponse = spec.getUserSyncs({iframeEnabled: true}, [''], {gdprApplies: true}); + + assert.deepStrictEqual(userSyncResponse, [{type: 'iframe', url: expectedUrl}]); + }); + }); + + describe('and gdpr does not apply', () => { + it('should place gdpr query param to the user sync url with zero value', () => { + const expectedUrl = 'https://js.adscale.de/pbsync.html?gdpr=0&gdpr_consent='; + const userSyncResponse = spec.getUserSyncs({iframeEnabled: true}, [''], {gdprApplies: false}); + + assert.deepStrictEqual(userSyncResponse, [{type: 'iframe', url: expectedUrl}]); + }); + + describe('because consent does not specify it', () => { + it('should place gdpr query param to the user sync url with zero value', () => { + const expectedUrl = 'https://js.adscale.de/pbsync.html?gdpr=0&gdpr_consent='; + const userSyncResponse = spec.getUserSyncs({iframeEnabled: true}, [''], {}); + + assert.deepStrictEqual(userSyncResponse, [{type: 'iframe', url: expectedUrl}]); + }); + }); + }); + + describe('and consent string is defined', () => { + it('should pass consent string to gdpr consent query param', () => { + const consentString = 'consent_string'; + const expectedUrl = `https://js.adscale.de/pbsync.html?gdpr=1&gdpr_consent=${consentString}`; + const userSyncResponse = spec.getUserSyncs({iframeEnabled: true}, [''], {gdprApplies: true, consentString}); + + assert.deepStrictEqual(userSyncResponse, [{type: 'iframe', url: expectedUrl}]); + }); + + it('should correctly escape invalid characters', () => { + const consentString = 'consent ?stri&ng'; + const expectedUrl = `https://js.adscale.de/pbsync.html?gdpr=1&gdpr_consent=consent%20%3Fstri%26ng`; + const userSyncResponse = spec.getUserSyncs({iframeEnabled: true}, [''], {gdprApplies: true, consentString}); + + assert.deepStrictEqual(userSyncResponse, [{type: 'iframe', url: expectedUrl}]); + }); + }); + }); + }); + + describe('when iframe option is disabled', () => { + it('should not perform user connect even when there was a response', () => { + const userSyncResponse = spec.getUserSyncs({iframeEnabled: false}, ['']); + + assert.deepStrictEqual(userSyncResponse, []); + }); + + it('should not perform user connect when there was no response', () => { + const userSyncResponse = spec.getUserSyncs({iframeEnabled: false}, []); + + assert.deepStrictEqual(userSyncResponse, []); + }); + }); + }); +}); diff --git a/test/spec/modules/sublimeBidAdapter_spec.js b/test/spec/modules/sublimeBidAdapter_spec.js index ae9e293a757..a0765a0d396 100644 --- a/test/spec/modules/sublimeBidAdapter_spec.js +++ b/test/spec/modules/sublimeBidAdapter_spec.js @@ -1,5 +1,5 @@ import { expect } from 'chai'; -import { spec, sendEvent, log, setState, state } from 'modules/sublimeBidAdapter.js'; +import { spec } from 'modules/sublimeBidAdapter.js'; import { newBidder } from 'src/adapters/bidderFactory.js'; let utils = require('src/utils'); @@ -9,6 +9,16 @@ describe('Sublime Adapter', function() { describe('sendEvent', function() { let sandbox; + const triggeredPixelProperties = [ + 't', + 'tse', + 'z', + 'e', + 'src', + 'puid', + 'trId', + 'pbav', + ]; beforeEach(function () { sandbox = sinon.sandbox.create(); @@ -16,8 +26,10 @@ describe('Sublime Adapter', function() { it('should trigger pixel', function () { sandbox.spy(utils, 'triggerPixel'); - sendEvent('test', true); + spec.sendEvent('test'); expect(utils.triggerPixel.called).to.equal(true); + const params = utils.parseUrl(utils.triggerPixel.args[0][0]).search; + expect(Object.keys(params)).to.have.members(triggeredPixelProperties); }); afterEach(function () { @@ -94,8 +106,9 @@ describe('Sublime Adapter', function() { }); it('should contains a request id equals to the bid id', function() { - expect(request[0].data.requestId).to.equal(bidRequests[0].bidId); - expect(request[1].data.requestId).to.equal(bidRequests[1].bidId); + for (let i = 0; i < request.length; i = i + 1) { + expect(JSON.parse(request[i].data).requestId).to.equal(bidRequests[i].bidId); + } }); it('should have an url that contains bid keyword', function() { @@ -149,7 +162,7 @@ describe('Sublime Adapter', function() { currency: 'USD', netRevenue: true, ttl: 600, - pbav: '0.5.2', + pbav: '0.7.0', ad: '', }, ]; @@ -191,7 +204,7 @@ describe('Sublime Adapter', function() { netRevenue: true, ttl: 600, ad: '', - pbav: '0.5.2', + pbav: '0.7.0', }; expect(result[0]).to.deep.equal(expectedResponse); @@ -241,7 +254,7 @@ describe('Sublime Adapter', function() { netRevenue: true, ttl: 600, ad: '', - pbav: '0.5.2', + pbav: '0.7.0', }; expect(result[0]).to.deep.equal(expectedResponse); @@ -279,4 +292,24 @@ describe('Sublime Adapter', function() { }); }); }); + + describe('onBidWon', function() { + let sandbox; + let bid = { foo: 'bar' }; + + beforeEach(function () { + sandbox = sinon.sandbox.create(); + }); + + it('should trigger "bidwon" pixel', function () { + sandbox.spy(utils, 'triggerPixel'); + spec.onBidWon(bid); + const params = utils.parseUrl(utils.triggerPixel.args[0][0]).search; + expect(params.e).to.equal('bidwon'); + }); + + afterEach(function () { + sandbox.restore(); + }); + }) }); diff --git a/test/spec/modules/teadsBidAdapter_spec.js b/test/spec/modules/teadsBidAdapter_spec.js index 17841b271d4..a6ae17ec8ff 100644 --- a/test/spec/modules/teadsBidAdapter_spec.js +++ b/test/spec/modules/teadsBidAdapter_spec.js @@ -200,8 +200,8 @@ describe('teadsBidAdapter', () => { performance.getEntriesByType('navigation')[0] && performance.getEntriesByType('navigation')[0].responseStart && performance.getEntriesByType('navigation')[0].requestStart && - performance.getEntriesByType('navigation')[0].responseStart >= 0 && - performance.getEntriesByType('navigation')[0].requestStart >= 0 && + performance.getEntriesByType('navigation')[0].responseStart > 0 && + performance.getEntriesByType('navigation')[0].requestStart > 0 && Math.round( performance.getEntriesByType('navigation')[0].responseStart - performance.getEntriesByType('navigation')[0].requestStart ); @@ -211,9 +211,15 @@ describe('teadsBidAdapter', () => { if (ttfbExpectedV2) { expect(payload.timeToFirstByte).to.deep.equal(ttfbExpectedV2.toString()); } else { - const ttfbExpectedV1 = performance.timing.responseStart - performance.timing.requestStart; - - expect(payload.timeToFirstByte).to.deep.equal(ttfbExpectedV1.toString()); + const ttfbWithTimingV1 = performance && + performance.timing.responseStart && + performance.timing.requestStart && + performance.timing.responseStart > 0 && + performance.timing.requestStart > 0 && + performance.timing.responseStart - performance.timing.requestStart; + const ttfbExpectedV1 = ttfbWithTimingV1 ? ttfbWithTimingV1.toString() : ''; + + expect(payload.timeToFirstByte).to.deep.equal(ttfbExpectedV1); } }); diff --git a/test/spec/modules/tripleliftBidAdapter_spec.js b/test/spec/modules/tripleliftBidAdapter_spec.js index 797b3fab0c1..82578424027 100644 --- a/test/spec/modules/tripleliftBidAdapter_spec.js +++ b/test/spec/modules/tripleliftBidAdapter_spec.js @@ -83,15 +83,21 @@ describe('triplelift adapter', function () { expect(tripleliftAdapterSpec.isBidRequestValid(instreamBid)).to.equal(true); }); + it('should return true when required params found - instream - 2', function () { + delete instreamBid.mediaTypes.playerSize; + delete instreamBid.params.video.w; + delete instreamBid.params.video.h; + // the only required param is inventoryCode + expect(tripleliftAdapterSpec.isBidRequestValid(instreamBid)).to.equal(true); + }); + it('should return false when required params are not passed', function () { delete bid.params.inventoryCode; expect(tripleliftAdapterSpec.isBidRequestValid(bid)).to.equal(false); }); it('should return false when required params are not passed - instream', function () { - delete instreamBid.mediaTypes.playerSize; - delete instreamBid.params.video.w; - delete instreamBid.params.video.h; + delete instreamBid.params.inventoryCode; expect(tripleliftAdapterSpec.isBidRequestValid(instreamBid)).to.equal(false); }); }); @@ -137,6 +143,14 @@ describe('triplelift adapter', function () { auctionId: '1d1a030790a475', userId: {}, schain, + fpd: { + context: { + pbAdSlot: 'homepage-top-rect', + data: { + adUnitSpecificAttribute: 123 + } + } + } }, { bidder: 'triplelift', @@ -164,6 +178,174 @@ describe('triplelift adapter', function () { auctionId: '1d1a030790a475', userId: {}, schain, + }, + // banner and outstream video + { + bidder: 'triplelift', + params: { + inventoryCode: 'outstream_test', + floor: 1.0, + video: { + mimes: ['video/mp4'], + maxduration: 30, + minduration: 6, + w: 640, + h: 480 + } + }, + mediaTypes: { + video: { + context: 'outstream', + playerSize: [640, 480] + }, + banner: { + sizes: [ + [970, 250], + [1, 1] + ] + } + }, + adUnitCode: 'adunit-code-instream', + sizes: [[300, 250], [300, 600], [1, 1, 1], ['flex']], + bidId: '30b31c1838de1e', + bidderRequestId: '22edbae2733bf6', + auctionId: '1d1a030790a475', + userId: {}, + schain, + }, + // banner and incomplete video + { + bidder: 'triplelift', + params: { + inventoryCode: 'outstream_test', + floor: 1.0, + video: { + mimes: ['video/mp4'], + maxduration: 30, + minduration: 6, + w: 640, + h: 480 + } + }, + mediaTypes: { + video: { + + }, + banner: { + sizes: [ + [970, 250], + [1, 1] + ] + } + }, + adUnitCode: 'adunit-code-instream', + sizes: [[300, 250], [300, 600], [1, 1, 1], ['flex']], + bidId: '30b31c1838de1e', + bidderRequestId: '22edbae2733bf6', + auctionId: '1d1a030790a475', + userId: {}, + schain, + }, + // incomplete banner and incomplete video + { + bidder: 'triplelift', + params: { + inventoryCode: 'outstream_test', + floor: 1.0, + video: { + mimes: ['video/mp4'], + maxduration: 30, + minduration: 6, + w: 640, + h: 480 + } + }, + mediaTypes: { + video: { + + }, + banner: { + + } + }, + adUnitCode: 'adunit-code-instream', + sizes: [[300, 250], [300, 600], [1, 1, 1], ['flex']], + bidId: '30b31c1838de1e', + bidderRequestId: '22edbae2733bf6', + auctionId: '1d1a030790a475', + userId: {}, + schain, + }, + // banner and instream video + { + bidder: 'triplelift', + params: { + inventoryCode: 'outstream_test', + floor: 1.0, + video: { + mimes: ['video/mp4'], + maxduration: 30, + minduration: 6, + w: 640, + h: 480 + } + }, + mediaTypes: { + video: { + context: 'instream', + playerSize: [640, 480] + }, + banner: { + sizes: [ + [970, 250], + [1, 1] + ] + } + }, + adUnitCode: 'adunit-code-instream', + sizes: [[300, 250], [300, 600], [1, 1, 1], ['flex']], + bidId: '30b31c1838de1e', + bidderRequestId: '22edbae2733bf6', + auctionId: '1d1a030790a475', + userId: {}, + schain, + }, + // banner and outream video and native + { + bidder: 'triplelift', + params: { + inventoryCode: 'outstream_test', + floor: 1.0, + video: { + mimes: ['video/mp4'], + maxduration: 30, + minduration: 6, + w: 640, + h: 480 + } + }, + mediaTypes: { + video: { + context: 'outstream', + playerSize: [640, 480] + }, + banner: { + sizes: [ + [970, 250], + [1, 1] + ] + }, + native: { + + } + }, + adUnitCode: 'adunit-code-instream', + sizes: [[300, 250], [300, 600], [1, 1, 1], ['flex']], + bidId: '30b31c1838de1e', + bidderRequestId: '22edbae2733bf6', + auctionId: '1d1a030790a475', + userId: {}, + schain, } ]; @@ -228,6 +410,26 @@ describe('triplelift adapter', function () { expect(payload.imp[1].tagid).to.equal('insteam_test'); expect(payload.imp[1].floor).to.equal(1.0); expect(payload.imp[1].video).to.exist.and.to.be.a('object'); + // banner and outstream video + expect(payload.imp[2]).to.not.have.property('video'); + expect(payload.imp[2]).to.have.property('banner'); + expect(payload.imp[2].banner.format).to.deep.equal([{w: 300, h: 250}, {w: 300, h: 600}]); + // banner and incomplete video + expect(payload.imp[3]).to.not.have.property('video'); + expect(payload.imp[3]).to.have.property('banner'); + expect(payload.imp[3].banner.format).to.deep.equal([{w: 300, h: 250}, {w: 300, h: 600}]); + // incomplete mediatypes.banner and incomplete video + expect(payload.imp[4]).to.not.have.property('video'); + expect(payload.imp[4]).to.have.property('banner'); + expect(payload.imp[4].banner.format).to.deep.equal([{w: 300, h: 250}, {w: 300, h: 600}]); + // banner and instream video + expect(payload.imp[5]).to.not.have.property('banner'); + expect(payload.imp[5]).to.have.property('video'); + expect(payload.imp[5].video).to.exist.and.to.be.a('object'); + // banner and outream video and native + expect(payload.imp[6]).to.not.have.property('video'); + expect(payload.imp[6]).to.have.property('banner'); + expect(payload.imp[6].banner.format).to.deep.equal([{w: 300, h: 250}, {w: 300, h: 600}]); }); it('should add tdid to the payload if included', function () { @@ -403,17 +605,19 @@ describe('triplelift adapter', function () { const request = tripleliftAdapterSpec.buildRequests(bidRequests, bidderRequest); expect(request.data.imp[0].floor).to.equal(1.99); }); - it('should send fpd on root level ext if kvps are available', function() { + it('should send global config fpd if kvps are available', function() { const sens = null; const category = ['news', 'weather', 'hurricane']; const pmp_elig = 'true'; const fpd = { context: { - pmp_elig, - category, + pmp_elig: pmp_elig, + data: { + category: category + } }, user: { - sens, + sens: sens, } } sandbox.stub(config, 'getConfig').callsFake(key => { @@ -424,9 +628,16 @@ describe('triplelift adapter', function () { }); const request = tripleliftAdapterSpec.buildRequests(bidRequests, bidderRequest); const { data: payload } = request; - expect(payload.ext.fpd).to.not.haveOwnProperty('sens'); - expect(payload.ext.fpd).to.haveOwnProperty('category'); - expect(payload.ext.fpd).to.haveOwnProperty('pmp_elig'); + expect(payload.ext.fpd.user).to.not.exist; + expect(payload.ext.fpd.context.data).to.haveOwnProperty('category'); + expect(payload.ext.fpd.context).to.haveOwnProperty('pmp_elig'); + }); + it('should send ad unit fpd if kvps are available', function() { + const request = tripleliftAdapterSpec.buildRequests(bidRequests, bidderRequest); + expect(request.data.imp[0].fpd.context).to.haveOwnProperty('pbAdSlot'); + expect(request.data.imp[0].fpd.context).to.haveOwnProperty('data'); + expect(request.data.imp[0].fpd.context.data).to.haveOwnProperty('adUnitSpecificAttribute'); + expect(request.data.imp[1].fpd).to.not.exist; }); }); @@ -444,7 +655,8 @@ describe('triplelift adapter', function () { ad: 'ad-markup', iurl: 'https://s.adroll.com/a/IYR/N36/IYRN366MFVDITBAGNNT5U6.jpg', tl_source: 'tlx', - advertiser_name: 'fake advertiser name' + advertiser_name: 'fake advertiser name', + adomain: ['basspro.com', 'internetalerts.org'] }, { imp_id: 1, @@ -550,9 +762,16 @@ describe('triplelift adapter', function () { it('should include the advertiser name in the meta field if available', function () { let result = tripleliftAdapterSpec.interpretResponse(response, {bidderRequest}); - expect(result[0].meta.advertiserName).to.equal('fake advertiser name') + expect(result[0].meta.advertiserName).to.equal('fake advertiser name'); expect(result[1].meta).to.not.have.key('advertiserName'); }); + + it('should include the advertiser domain array in the meta field if available', function () { + let result = tripleliftAdapterSpec.interpretResponse(response, {bidderRequest}); + expect(result[0].meta.advertiserDomains[0]).to.equal('basspro.com'); + expect(result[0].meta.advertiserDomains[1]).to.equal('internetalerts.org'); + expect(result[1].meta).to.not.have.key('advertiserDomains'); + }); }); describe('getUserSyncs', function() { diff --git a/test/spec/modules/truereachBidAdapter_spec.js b/test/spec/modules/truereachBidAdapter_spec.js index 5fe053ceb91..36441722648 100644 --- a/test/spec/modules/truereachBidAdapter_spec.js +++ b/test/spec/modules/truereachBidAdapter_spec.js @@ -81,4 +81,25 @@ describe('truereachBidAdapterTests', function () { expect(bid.netRevenue).to.equal(false); expect(bid.meta.advertiserDomains[0]).to.equal('https://www.momagic.com/'); }); + + describe('user_sync', function() { + const user_sync_url = 'http://ads.momagic.com/jsp/usersync.jsp'; + it('register_iframe_pixel_if_iframeEnabled_is_true', function() { + let syncs = spec.getUserSyncs( + {iframeEnabled: true} + ); + expect(syncs).to.be.an('array'); + expect(syncs.length).to.equal(1); + expect(syncs[0].type).to.equal('iframe'); + expect(syncs[0].url).to.equal(user_sync_url); + }); + + it('if_pixelEnabled_is_true', function() { + let syncs = spec.getUserSyncs( + {pixelEnabled: true} + ); + expect(syncs).to.be.an('array'); + expect(syncs.length).to.equal(0); + }); + }); }); diff --git a/test/spec/modules/undertoneBidAdapter_spec.js b/test/spec/modules/undertoneBidAdapter_spec.js index 72321ce415b..e8729965c50 100644 --- a/test/spec/modules/undertoneBidAdapter_spec.js +++ b/test/spec/modules/undertoneBidAdapter_spec.js @@ -94,7 +94,8 @@ const bidReqUserIds = [{ userId: { idl_env: '1111', tdid: '123456', - digitrustid: {data: {id: 'DTID', keyv: 4, privacy: {optout: false}, producer: 'ABC', version: 2}} + digitrustid: {data: {id: 'DTID', keyv: 4, privacy: {optout: false}, producer: 'ABC', version: 2}}, + id5id: { uid: '1111' } } }, { @@ -316,6 +317,7 @@ describe('Undertone Adapter', () => { expect(bidCommons.uids.tdid).to.equal('123456'); expect(bidCommons.uids.idl_env).to.equal('1111'); expect(bidCommons.uids.digitrustid.data.id).to.equal('DTID'); + expect(bidCommons.uids.id5id.uid).to.equal('1111'); }); it('should send page sizes sizes correctly', function () { const request = spec.buildRequests(bidReqUserIds, bidderReq); diff --git a/test/spec/modules/userId_spec.js b/test/spec/modules/userId_spec.js index e9057ee26d9..981ebb5f50e 100644 --- a/test/spec/modules/userId_spec.js +++ b/test/spec/modules/userId_spec.js @@ -1,13 +1,14 @@ import { attachIdSystem, auctionDelay, + coreStorage, init, requestBidsHook, + setStoredConsentData, + setStoredValue, setSubmoduleRegistry, syncDelay, - coreStorage, - setStoredValue, - setStoredConsentData + PBJS_USER_ID_OPTOUT_NAME } from 'modules/userId/index.js'; import {createEidsArray} from 'modules/userId/eids.js'; import {config} from 'src/config.js'; @@ -15,8 +16,12 @@ import * as utils from 'src/utils.js'; import events from 'src/events.js'; import CONSTANTS from 'src/constants.json'; import {getGlobal} from 'src/prebidGlobal.js'; -import {setConsentConfig, requestBidsHook as consentManagementRequestBidsHook, resetConsentData} from 'modules/consentManagement.js'; -import {gdprDataHandler} from 'src/adapterManager.js'; +import { + requestBidsHook as consentManagementRequestBidsHook, + resetConsentData, + setConsentConfig +} from 'modules/consentManagement.js'; +import {server} from 'test/mocks/xhr.js'; import {unifiedIdSubmodule} from 'modules/unifiedIdSystem.js'; import {pubCommonIdSubmodule} from 'modules/pubCommonIdSystem.js'; import {britepoolIdSubmodule} from 'modules/britepoolIdSystem.js'; @@ -29,30 +34,20 @@ import {intentIqIdSubmodule} from 'modules/intentIqIdSystem.js'; import {zeotapIdPlusSubmodule} from 'modules/zeotapIdPlusIdSystem.js'; import {sharedIdSubmodule} from 'modules/sharedIdSystem.js'; import {haloIdSubmodule} from 'modules/haloIdSystem.js'; -import {server} from 'test/mocks/xhr.js'; +import {pubProvidedIdSubmodule} from 'modules/pubProvidedIdSystem.js'; +import {criteoIdSubmodule} from 'modules/criteoIdSystem.js'; let assert = require('chai').assert; let expect = require('chai').expect; const EXPIRED_COOKIE_DATE = 'Thu, 01 Jan 1970 00:00:01 GMT'; const CONSENT_LOCAL_STORAGE_NAME = '_pbjs_userid_consent_data'; -describe('User ID', function() { - function getConfigMock(configArr1, configArr2, configArr3, configArr4, configArr5, configArr6, configArr7, configArr8, configArr9, configArr10) { +describe('User ID', function () { + function getConfigMock(...configArrays) { return { userSync: { syncDelay: 0, - userIds: [ - (configArr1 && configArr1.length >= 3) ? getStorageMock.apply(null, configArr1) : null, - (configArr2 && configArr2.length >= 3) ? getStorageMock.apply(null, configArr2) : null, - (configArr3 && configArr3.length >= 3) ? getStorageMock.apply(null, configArr3) : null, - (configArr4 && configArr4.length >= 3) ? getStorageMock.apply(null, configArr4) : null, - (configArr5 && configArr5.length >= 3) ? getStorageMock.apply(null, configArr5) : null, - (configArr6 && configArr6.length >= 3) ? getStorageMock.apply(null, configArr6) : null, - (configArr7 && configArr7.length >= 3) ? getStorageMock.apply(null, configArr7) : null, - (configArr8 && configArr8.length >= 3) ? getStorageMock.apply(null, configArr8) : null, - (configArr9 && configArr9.length >= 3) ? getStorageMock.apply(null, configArr9) : null, - (configArr10 && configArr10.length >= 3) ? getStorageMock.apply(null, configArr10) : null - ].filter(i => i) + userIds: configArrays.map(configArray => (configArray && configArray.length >= 3) ? getStorageMock.apply(null, configArray) : null) } } } @@ -90,35 +85,39 @@ describe('User ID', function() { return cfg; } - before(function() { - coreStorage.setCookie('_pubcid_optout', '', EXPIRED_COOKIE_DATE); - localStorage.removeItem('_pbjs_id_optout'); - localStorage.removeItem('_pubcid_optout'); + function findEid(eids, source) { + return eids.find((eid) => { + if (eid.source === source) { return true; } + }); + } + + before(function () { + localStorage.removeItem(PBJS_USER_ID_OPTOUT_NAME); }); - beforeEach(function() { + beforeEach(function () { coreStorage.setCookie(CONSENT_LOCAL_STORAGE_NAME, '', EXPIRED_COOKIE_DATE); }); - describe('Decorate Ad Units', function() { - beforeEach(function() { + describe('Decorate Ad Units', function () { + beforeEach(function () { coreStorage.setCookie('pubcid', '', EXPIRED_COOKIE_DATE); coreStorage.setCookie('pubcid_alt', 'altpubcid200000', (new Date(Date.now() + 5000).toUTCString())); sinon.spy(coreStorage, 'setCookie'); }); - afterEach(function() { + afterEach(function () { $$PREBID_GLOBAL$$.requestBids.removeAll(); config.resetConfig(); coreStorage.setCookie.restore(); }); - after(function() { + after(function () { coreStorage.setCookie('pubcid', '', EXPIRED_COOKIE_DATE); coreStorage.setCookie('pubcid_alt', '', EXPIRED_COOKIE_DATE); }); - it('Check same cookie behavior', function() { + it('Check same cookie behavior', function () { let adUnits1 = [getAdUnitMock()]; let adUnits2 = [getAdUnitMock()]; let innerAdUnits1; @@ -153,7 +152,7 @@ describe('User ID', function() { assert.deepEqual(innerAdUnits1, innerAdUnits2); }); - it('Check different cookies', function() { + it('Check different cookies', function () { let adUnits1 = [getAdUnitMock()]; let adUnits2 = [getAdUnitMock()]; let innerAdUnits1; @@ -204,7 +203,7 @@ describe('User ID', function() { expect(pubcid1).to.not.equal(pubcid2); }); - it('Use existing cookie', function() { + it('Use existing cookie', function () { let adUnits = [getAdUnitMock()]; let innerAdUnits; @@ -229,7 +228,7 @@ describe('User ID', function() { expect(coreStorage.setCookie.callCount).to.equal(1); }); - it('Extend cookie', function() { + it('Extend cookie', function () { let adUnits = [getAdUnitMock()]; let innerAdUnits; let customConfig = getConfigMock(['pubCommonId', 'pubcid_alt', 'cookie']); @@ -256,7 +255,7 @@ describe('User ID', function() { expect(coreStorage.setCookie.callCount).to.equal(2); }); - it('Disable auto create', function() { + it('Disable auto create', function () { let adUnits = [getAdUnitMock()]; let innerAdUnits; let customConfig = getConfigMock(['pubCommonId', 'pubcid', 'cookie']); @@ -278,7 +277,7 @@ describe('User ID', function() { expect(coreStorage.setCookie.callCount).to.equal(1); }); - it('pbjs.getUserIds', function() { + it('pbjs.getUserIds', function () { setSubmoduleRegistry([pubCommonIdSubmodule]); init(config); config.setConfig({ @@ -293,7 +292,7 @@ describe('User ID', function() { expect((getGlobal()).getUserIds()).to.deep.equal({pubcid: '11111'}); }); - it('pbjs.getUserIdsAsEids', function() { + it('pbjs.getUserIdsAsEids', function () { setSubmoduleRegistry([pubCommonIdSubmodule]); init(config); config.setConfig({ @@ -307,57 +306,155 @@ describe('User ID', function() { expect(typeof (getGlobal()).getUserIdsAsEids).to.equal('function'); expect((getGlobal()).getUserIdsAsEids()).to.deep.equal(createEidsArray((getGlobal()).getUserIds())); }); + + it('pbjs.refreshUserIds refreshes', function() { + let sandbox = sinon.createSandbox(); + + let mockIdCallback = sandbox.stub().returns({id: {'MOCKID': '1111'}}); + + let mockIdSystem = { + name: 'mockId', + decode: function(value) { + return { + 'mid': value['MOCKID'] + }; + }, + getId: mockIdCallback + }; + + setSubmoduleRegistry([mockIdSystem]); + init(config); + config.setConfig({ + userSync: { + syncDelay: 0, + userIds: [{ + name: 'mockId', + value: {id: {mockId: '1111'}} + }] + } + }); + expect(typeof (getGlobal()).refreshUserIds).to.equal('function'); + + getGlobal().getUserIds(); // force initialization + + // update config so that getId will be called + config.setConfig({ + userSync: { + syncDelay: 0, + userIds: [{ + name: 'mockId', + storage: {name: 'mockid', type: 'cookie'}, + }] + } + }); + + getGlobal().refreshUserIds(); + expect(mockIdCallback.callCount).to.equal(1); + }); + + it('pbjs.refreshUserIds refreshes single', function() { + coreStorage.setCookie('MOCKID', '', EXPIRED_COOKIE_DATE); + coreStorage.setCookie('REFRESH', '', EXPIRED_COOKIE_DATE); + + let sandbox = sinon.createSandbox(); + let mockIdCallback = sandbox.stub().returns({id: {'MOCKID': '1111'}}); + let refreshUserIdsCallback = sandbox.stub(); + + let mockIdSystem = { + name: 'mockId', + decode: function(value) { + return { + 'mid': value['MOCKID'] + }; + }, + getId: mockIdCallback + }; + + let refreshedIdCallback = sandbox.stub().returns({id: {'REFRESH': '1111'}}); + + let refreshedIdSystem = { + name: 'refreshedId', + decode: function(value) { + return { + 'refresh': value['REFRESH'] + }; + }, + getId: refreshedIdCallback + }; + + setSubmoduleRegistry([refreshedIdSystem, mockIdSystem]); + init(config); + config.setConfig({ + userSync: { + syncDelay: 0, + userIds: [ + { + name: 'mockId', + storage: {name: 'MOCKID', type: 'cookie'}, + }, + { + name: 'refreshedId', + storage: {name: 'refreshedid', type: 'cookie'}, + } + ] + } + }); + + getGlobal().getUserIds(); // force initialization + + getGlobal().refreshUserIds({submoduleNames: 'refreshedId'}, refreshUserIdsCallback); + + expect(refreshedIdCallback.callCount).to.equal(2); + expect(mockIdCallback.callCount).to.equal(1); + expect(refreshUserIdsCallback.callCount).to.equal(1); + }); }); - describe('Opt out', function() { - before(function() { - coreStorage.setCookie('_pbjs_id_optout', '1', (new Date(Date.now() + 5000).toUTCString())); + describe('Opt out', function () { + before(function () { + coreStorage.setCookie(PBJS_USER_ID_OPTOUT_NAME, '1', (new Date(Date.now() + 5000).toUTCString())); }); - beforeEach(function() { + beforeEach(function () { sinon.stub(utils, 'logInfo'); }); - afterEach(function() { + afterEach(function () { // removed cookie - coreStorage.setCookie('_pbjs_id_optout', '', EXPIRED_COOKIE_DATE); + coreStorage.setCookie(PBJS_USER_ID_OPTOUT_NAME, '', EXPIRED_COOKIE_DATE); $$PREBID_GLOBAL$$.requestBids.removeAll(); utils.logInfo.restore(); config.resetConfig(); }); - after(function() { - coreStorage.setCookie('_pbjs_id_optout', '', EXPIRED_COOKIE_DATE); - }); - - it('fails initialization if opt out cookie exists', function() { + it('fails initialization if opt out cookie exists', function () { setSubmoduleRegistry([pubCommonIdSubmodule]); init(config); config.setConfig(getConfigMock(['pubCommonId', 'pubcid', 'cookie'])); expect(utils.logInfo.args[0][0]).to.exist.and.to.equal('User ID - opt-out cookie found, exit module'); }); - it('initializes if no opt out cookie exists', function() { + it('initializes if no opt out cookie exists', function () { setSubmoduleRegistry([pubCommonIdSubmodule]); init(config); config.setConfig(getConfigMock(['pubCommonId', 'pubcid', 'cookie'])); - expect(utils.logInfo.args[0][0]).to.exist.and.to.equal('User ID - usersync config updated for 1 submodules'); + expect(utils.logInfo.args[0][0]).to.exist.and.to.contain('User ID - usersync config updated for 1 submodules'); }); }); - describe('Handle variations of config values', function() { - beforeEach(function() { + describe('Handle variations of config values', function () { + beforeEach(function () { sinon.stub(utils, 'logInfo'); }); - afterEach(function() { + afterEach(function () { $$PREBID_GLOBAL$$.requestBids.removeAll(); utils.logInfo.restore(); config.resetConfig(); }); - it('handles config with no usersync object', function() { - setSubmoduleRegistry([pubCommonIdSubmodule, unifiedIdSubmodule, id5IdSubmodule, identityLinkSubmodule, merkleIdSubmodule, netIdSubmodule, sharedIdSubmodule, intentIqIdSubmodule, zeotapIdPlusSubmodule]); + it('handles config with no usersync object', function () { + setSubmoduleRegistry([pubCommonIdSubmodule, unifiedIdSubmodule, id5IdSubmodule, identityLinkSubmodule, merkleIdSubmodule, netIdSubmodule, sharedIdSubmodule, intentIqIdSubmodule, zeotapIdPlusSubmodule, pubProvidedIdSubmodule, criteoIdSubmodule]); init(config); config.setConfig({}); // usersync is undefined, and no logInfo message for 'User ID - usersync config updated' @@ -365,14 +462,14 @@ describe('User ID', function() { }); it('handles config with empty usersync object', function () { - setSubmoduleRegistry([pubCommonIdSubmodule, unifiedIdSubmodule, id5IdSubmodule, identityLinkSubmodule, merkleIdSubmodule, netIdSubmodule, sharedIdSubmodule, intentIqIdSubmodule, zeotapIdPlusSubmodule]); + setSubmoduleRegistry([pubCommonIdSubmodule, unifiedIdSubmodule, id5IdSubmodule, identityLinkSubmodule, merkleIdSubmodule, netIdSubmodule, sharedIdSubmodule, intentIqIdSubmodule, zeotapIdPlusSubmodule, pubProvidedIdSubmodule, criteoIdSubmodule]); init(config); config.setConfig({userSync: {}}); expect(typeof utils.logInfo.args[0]).to.equal('undefined'); }); it('handles config with usersync and userIds that are empty objs', function () { - setSubmoduleRegistry([pubCommonIdSubmodule, unifiedIdSubmodule, id5IdSubmodule, identityLinkSubmodule, merkleIdSubmodule, netIdSubmodule, sharedIdSubmodule, intentIqIdSubmodule, zeotapIdPlusSubmodule]); + setSubmoduleRegistry([pubCommonIdSubmodule, unifiedIdSubmodule, id5IdSubmodule, identityLinkSubmodule, merkleIdSubmodule, netIdSubmodule, sharedIdSubmodule, intentIqIdSubmodule, zeotapIdPlusSubmodule, pubProvidedIdSubmodule, criteoIdSubmodule]); init(config); config.setConfig({ userSync: { @@ -383,7 +480,7 @@ describe('User ID', function() { }); it('handles config with usersync and userIds with empty names or that dont match a submodule.name', function () { - setSubmoduleRegistry([pubCommonIdSubmodule, unifiedIdSubmodule, id5IdSubmodule, identityLinkSubmodule, merkleIdSubmodule, netIdSubmodule, sharedIdSubmodule, intentIqIdSubmodule, zeotapIdPlusSubmodule]); + setSubmoduleRegistry([pubCommonIdSubmodule, unifiedIdSubmodule, id5IdSubmodule, identityLinkSubmodule, merkleIdSubmodule, netIdSubmodule, sharedIdSubmodule, intentIqIdSubmodule, zeotapIdPlusSubmodule, pubProvidedIdSubmodule, criteoIdSubmodule]); init(config); config.setConfig({ userSync: { @@ -400,20 +497,22 @@ describe('User ID', function() { }); it('config with 1 configurations should create 1 submodules', function () { - setSubmoduleRegistry([pubCommonIdSubmodule, unifiedIdSubmodule, id5IdSubmodule, identityLinkSubmodule, netIdSubmodule, sharedIdSubmodule, intentIqIdSubmodule, zeotapIdPlusSubmodule]); + setSubmoduleRegistry([pubCommonIdSubmodule, unifiedIdSubmodule, id5IdSubmodule, identityLinkSubmodule, netIdSubmodule, sharedIdSubmodule, intentIqIdSubmodule, zeotapIdPlusSubmodule, pubProvidedIdSubmodule, criteoIdSubmodule]); init(config); config.setConfig(getConfigMock(['unifiedId', 'unifiedid', 'cookie'])); - expect(utils.logInfo.args[0][0]).to.exist.and.to.equal('User ID - usersync config updated for 1 submodules'); + expect(utils.logInfo.args[0][0]).to.exist.and.to.contain('User ID - usersync config updated for 1 submodules'); }); - it('config with 10 configurations should result in 11 submodules add', function () { - setSubmoduleRegistry([pubCommonIdSubmodule, unifiedIdSubmodule, id5IdSubmodule, identityLinkSubmodule, liveIntentIdSubmodule, britepoolIdSubmodule, netIdSubmodule, sharedIdSubmodule, intentIqIdSubmodule, zeotapIdPlusSubmodule, haloIdSubmodule]); + it('config with 13 configurations should result in 13 submodules add', function () { + setSubmoduleRegistry([pubCommonIdSubmodule, unifiedIdSubmodule, id5IdSubmodule, identityLinkSubmodule, liveIntentIdSubmodule, britepoolIdSubmodule, netIdSubmodule, sharedIdSubmodule, intentIqIdSubmodule, zeotapIdPlusSubmodule, haloIdSubmodule, pubProvidedIdSubmodule, criteoIdSubmodule]); init(config); config.setConfig({ userSync: { syncDelay: 0, userIds: [{ + name: 'pubProvidedId' + }, { name: 'pubCommonId', value: {'pubcid': '11111'} }, { name: 'unifiedId', @@ -438,20 +537,22 @@ describe('User ID', function() { storage: {name: 'sharedid', type: 'cookie'} }, { name: 'intentIqId', - storage: { name: 'intentIqId', type: 'cookie' } + storage: {name: 'intentIqId', type: 'cookie'} }, { name: 'haloId', - storage: { name: 'haloId', type: 'cookie' } + storage: {name: 'haloId', type: 'cookie'} }, { name: 'zeotapIdPlus' + }, { + name: 'criteo' }] } }); - expect(utils.logInfo.args[0][0]).to.exist.and.to.equal('User ID - usersync config updated for 11 submodules'); + expect(utils.logInfo.args[0][0]).to.exist.and.to.contain('User ID - usersync config updated for 13 submodules'); }); it('config syncDelay updates module correctly', function () { - setSubmoduleRegistry([pubCommonIdSubmodule, unifiedIdSubmodule, id5IdSubmodule, identityLinkSubmodule, netIdSubmodule, sharedIdSubmodule, intentIqIdSubmodule, zeotapIdPlusSubmodule, haloIdSubmodule]); + setSubmoduleRegistry([pubCommonIdSubmodule, unifiedIdSubmodule, id5IdSubmodule, identityLinkSubmodule, netIdSubmodule, sharedIdSubmodule, intentIqIdSubmodule, zeotapIdPlusSubmodule, haloIdSubmodule, pubProvidedIdSubmodule, criteoIdSubmodule]); init(config); config.setConfig({ userSync: { @@ -466,7 +567,7 @@ describe('User ID', function() { }); it('config auctionDelay updates module correctly', function () { - setSubmoduleRegistry([pubCommonIdSubmodule, unifiedIdSubmodule, id5IdSubmodule, identityLinkSubmodule, netIdSubmodule, sharedIdSubmodule, intentIqIdSubmodule, zeotapIdPlusSubmodule, haloIdSubmodule]); + setSubmoduleRegistry([pubCommonIdSubmodule, unifiedIdSubmodule, id5IdSubmodule, identityLinkSubmodule, netIdSubmodule, sharedIdSubmodule, intentIqIdSubmodule, zeotapIdPlusSubmodule, haloIdSubmodule, pubProvidedIdSubmodule, criteoIdSubmodule]); init(config); config.setConfig({ userSync: { @@ -481,7 +582,7 @@ describe('User ID', function() { }); it('config auctionDelay defaults to 0 if not a number', function () { - setSubmoduleRegistry([pubCommonIdSubmodule, unifiedIdSubmodule, id5IdSubmodule, identityLinkSubmodule, netIdSubmodule, sharedIdSubmodule, intentIqIdSubmodule, zeotapIdPlusSubmodule, haloIdSubmodule]); + setSubmoduleRegistry([pubCommonIdSubmodule, unifiedIdSubmodule, id5IdSubmodule, identityLinkSubmodule, netIdSubmodule, sharedIdSubmodule, intentIqIdSubmodule, zeotapIdPlusSubmodule, haloIdSubmodule, pubProvidedIdSubmodule, criteoIdSubmodule]); init(config); config.setConfig({ userSync: { @@ -496,15 +597,16 @@ describe('User ID', function() { }); }); - describe('auction and user sync delays', function() { + describe('auction and user sync delays', function () { let sandbox; let adUnits; let mockIdCallback; let auctionSpy; - beforeEach(function() { + beforeEach(function () { sandbox = sinon.createSandbox(); sandbox.stub(global, 'setTimeout').returns(2); + sandbox.stub(global, 'clearTimeout'); sandbox.stub(events, 'on'); sandbox.stub(coreStorage, 'getCookie'); @@ -517,12 +619,12 @@ describe('User ID', function() { mockIdCallback = sandbox.stub(); const mockIdSystem = { name: 'mockId', - decode: function(value) { + decode: function (value) { return { 'mid': value['MOCKID'] }; }, - getId: function() { + getId: function () { const storedId = coreStorage.getCookie('MOCKID'); if (storedId) { return {id: {'MOCKID': storedId}}; @@ -536,13 +638,13 @@ describe('User ID', function() { attachIdSystem(mockIdSystem, true); }); - afterEach(function() { + afterEach(function () { $$PREBID_GLOBAL$$.requestBids.removeAll(); config.resetConfig(); sandbox.restore(); }); - it('delays auction if auctionDelay is set, timing out at auction delay', function() { + it('delays auction if auctionDelay is set, timing out at auction delay', function () { config.setConfig({ userSync: { auctionDelay: 33, @@ -556,6 +658,7 @@ describe('User ID', function() { requestBidsHook(auctionSpy, {adUnits}); // check auction was delayed + global.clearTimeout.calledOnce.should.equal(false); global.setTimeout.calledOnce.should.equal(true); global.setTimeout.calledWith(sinon.match.func, 33); auctionSpy.calledOnce.should.equal(false); @@ -575,7 +678,7 @@ describe('User ID', function() { events.on.called.should.equal(false); }); - it('delays auction if auctionDelay is set, continuing auction if ids are fetched before timing out', function(done) { + it('delays auction if auctionDelay is set, continuing auction if ids are fetched before timing out', function (done) { config.setConfig({ userSync: { auctionDelay: 33, @@ -590,6 +693,7 @@ describe('User ID', function() { // check auction was delayed // global.setTimeout.calledOnce.should.equal(true); + global.clearTimeout.calledOnce.should.equal(false); global.setTimeout.calledWith(sinon.match.func, 33); auctionSpy.calledOnce.should.equal(false); @@ -614,7 +718,7 @@ describe('User ID', function() { events.on.called.should.equal(false); }); - it('does not delay auction if not set, delays id fetch after auction ends with syncDelay', function() { + it('does not delay auction if not set, delays id fetch after auction ends with syncDelay', function () { config.setConfig({ userSync: { syncDelay: 77, @@ -650,7 +754,7 @@ describe('User ID', function() { mockIdCallback.calledOnce.should.equal(true); }); - it('does not delay user id sync after auction ends if set to 0', function() { + it('does not delay user id sync after auction ends if set to 0', function () { config.setConfig({ userSync: { syncDelay: 0, @@ -679,7 +783,7 @@ describe('User ID', function() { mockIdCallback.calledOnce.should.equal(true); }); - it('does not delay auction if there are no ids to fetch', function() { + it('does not delay auction if there are no ids to fetch', function () { coreStorage.getCookie.withArgs('MOCKID').returns('123456778'); config.setConfig({ userSync: { @@ -702,21 +806,21 @@ describe('User ID', function() { }); }); - describe('Request bids hook appends userId to bid objs in adapters', function() { + describe('Request bids hook appends userId to bid objs in adapters', function () { let adUnits; - beforeEach(function() { + beforeEach(function () { adUnits = [getAdUnitMock()]; }); - it('test hook from pubcommonid cookie', function(done) { + it('test hook from pubcommonid cookie', function (done) { coreStorage.setCookie('pubcid', 'testpubcid', (new Date(Date.now() + 100000).toUTCString())); setSubmoduleRegistry([pubCommonIdSubmodule]); init(config); config.setConfig(getConfigMock(['pubCommonId', 'pubcid', 'cookie'])); - requestBidsHook(function() { + requestBidsHook(function () { adUnits.forEach(unit => { unit.bids.forEach(bid => { expect(bid).to.have.deep.nested.property('userId.pubcid'); @@ -725,6 +829,10 @@ describe('User ID', function() { source: 'pubcid.org', uids: [{id: 'testpubcid', atype: 1}] }); + + // verify no sharedid was added + expect(bid.userId).to.not.have.property('sharedid'); + expect(findEid(bid.userIdAsEids, 'sharedid.org')).to.be.undefined; }); }); coreStorage.setCookie('pubcid', '', EXPIRED_COOKIE_DATE); @@ -732,12 +840,42 @@ describe('User ID', function() { }, {adUnits}); }); - it('test hook from pubcommonid config value object', function(done) { + it('test hook from pubcommonid html5', function (done) { + // simulate existing browser local storage values + localStorage.setItem('pubcid', 'testpubcid'); + localStorage.setItem('pubcid_exp', new Date(Date.now() + 100000).toUTCString()); + + setSubmoduleRegistry([pubCommonIdSubmodule]); + init(config); + config.setConfig(getConfigMock(['pubCommonId', 'pubcid', 'html5'])); + + requestBidsHook(function () { + adUnits.forEach(unit => { + unit.bids.forEach(bid => { + expect(bid).to.have.deep.nested.property('userId.pubcid'); + expect(bid.userId.pubcid).to.equal('testpubcid'); + expect(bid.userIdAsEids[0]).to.deep.equal({ + source: 'pubcid.org', + uids: [{id: 'testpubcid', atype: 1}] + }); + + // verify no sharedid was added + expect(bid.userId).to.not.have.property('sharedid'); + expect(findEid(bid.userIdAsEids, 'sharedid.org')).to.be.undefined; + }); + }); + localStorage.removeItem('pubcid'); + localStorage.removeItem('pubcid_exp'); + done(); + }, {adUnits}); + }); + + it('test hook from pubcommonid config value object', function (done) { setSubmoduleRegistry([pubCommonIdSubmodule]); init(config); config.setConfig(getConfigValueMock('pubCommonId', {'pubcidvalue': 'testpubcidvalue'})); - requestBidsHook(function() { + requestBidsHook(function () { adUnits.forEach(unit => { unit.bids.forEach(bid => { expect(bid).to.have.deep.nested.property('userId.pubcidvalue'); @@ -749,7 +887,7 @@ describe('User ID', function() { }, {adUnits}); }); - it('test hook from pubcommonid html5', function(done) { + it('test hook from UnifiedId html5', function (done) { // simulate existing browser local storage values localStorage.setItem('unifiedid_alt', JSON.stringify({'TDID': 'testunifiedid_alt'})); localStorage.setItem('unifiedid_alt_exp', ''); @@ -758,7 +896,7 @@ describe('User ID', function() { init(config); config.setConfig(getConfigMock(['unifiedId', 'unifiedid_alt', 'html5'])); - requestBidsHook(function() { + requestBidsHook(function () { adUnits.forEach(unit => { unit.bids.forEach(bid => { expect(bid).to.have.deep.nested.property('userId.tdid'); @@ -775,7 +913,7 @@ describe('User ID', function() { }, {adUnits}); }); - it('test hook from identityLink html5', function(done) { + it('test hook from identityLink html5', function (done) { // simulate existing browser local storage values localStorage.setItem('idl_env', 'AiGNC8Z5ONyZKSpIPf'); localStorage.setItem('idl_env_exp', ''); @@ -783,7 +921,7 @@ describe('User ID', function() { setSubmoduleRegistry([identityLinkSubmodule]); init(config); config.setConfig(getConfigMock(['identityLink', 'idl_env', 'html5'])); - requestBidsHook(function() { + requestBidsHook(function () { adUnits.forEach(unit => { unit.bids.forEach(bid => { expect(bid).to.have.deep.nested.property('userId.idl_env'); @@ -800,14 +938,14 @@ describe('User ID', function() { }, {adUnits}); }); - it('test hook from identityLink cookie', function(done) { + it('test hook from identityLink cookie', function (done) { coreStorage.setCookie('idl_env', 'AiGNC8Z5ONyZKSpIPf', (new Date(Date.now() + 100000).toUTCString())); setSubmoduleRegistry([identityLinkSubmodule]); init(config); config.setConfig(getConfigMock(['identityLink', 'idl_env', 'cookie'])); - requestBidsHook(function() { + requestBidsHook(function () { adUnits.forEach(unit => { unit.bids.forEach(bid => { expect(bid).to.have.deep.nested.property('userId.idl_env'); @@ -823,7 +961,30 @@ describe('User ID', function() { }, {adUnits}); }); - it('test hook from liveIntentId html5', function(done) { + it('test hook from criteoIdModule cookie', function (done) { + coreStorage.setCookie('storage_bidid', JSON.stringify({'criteoId': 'test_bidid'}), (new Date(Date.now() + 100000).toUTCString())); + + setSubmoduleRegistry([criteoIdSubmodule]); + init(config); + config.setConfig(getConfigMock(['criteo', 'storage_bidid', 'cookie'])); + + requestBidsHook(function () { + adUnits.forEach(unit => { + unit.bids.forEach(bid => { + expect(bid).to.have.deep.nested.property('userId.criteoId'); + expect(bid.userId.criteoId).to.equal('test_bidid'); + expect(bid.userIdAsEids[0]).to.deep.equal({ + source: 'criteo.com', + uids: [{id: 'test_bidid', atype: 1}] + }); + }); + }); + coreStorage.setCookie('storage_bidid', '', EXPIRED_COOKIE_DATE); + done(); + }, {adUnits}); + }); + + it('test hook from liveIntentId html5', function (done) { // simulate existing browser local storage values localStorage.setItem('_li_pbid', JSON.stringify({'unifiedId': 'random-ls-identifier'})); localStorage.setItem('_li_pbid_exp', ''); @@ -831,7 +992,7 @@ describe('User ID', function() { setSubmoduleRegistry([liveIntentIdSubmodule]); init(config); config.setConfig(getConfigMock(['liveIntentId', '_li_pbid', 'html5'])); - requestBidsHook(function() { + requestBidsHook(function () { adUnits.forEach(unit => { unit.bids.forEach(bid => { expect(bid).to.have.deep.nested.property('userId.lipb'); @@ -848,14 +1009,14 @@ describe('User ID', function() { }, {adUnits}); }); - it('test hook from liveIntentId cookie', function(done) { + it('test hook from liveIntentId cookie', function (done) { coreStorage.setCookie('_li_pbid', JSON.stringify({'unifiedId': 'random-cookie-identifier'}), (new Date(Date.now() + 100000).toUTCString())); setSubmoduleRegistry([liveIntentIdSubmodule]); init(config); config.setConfig(getConfigMock(['liveIntentId', '_li_pbid', 'cookie'])); - requestBidsHook(function() { + requestBidsHook(function () { adUnits.forEach(unit => { unit.bids.forEach(bid => { expect(bid).to.have.deep.nested.property('userId.lipb'); @@ -871,7 +1032,7 @@ describe('User ID', function() { }, {adUnits}); }); - it('test hook from sharedId html5', function(done) { + it('test hook from sharedId html5', function (done) { // simulate existing browser local storage values localStorage.setItem('sharedid', JSON.stringify({'id': 'test_sharedId', 'ts': 1590525289611})); localStorage.setItem('sharedid_exp', ''); @@ -879,7 +1040,7 @@ describe('User ID', function() { setSubmoduleRegistry([sharedIdSubmodule]); init(config); config.setConfig(getConfigMock(['sharedId', 'sharedid', 'html5'])); - requestBidsHook(function() { + requestBidsHook(function () { adUnits.forEach(unit => { unit.bids.forEach(bid => { expect(bid).to.have.deep.nested.property('userId.sharedid'); @@ -907,7 +1068,7 @@ describe('User ID', function() { }, {adUnits}); }); - it('test hook from sharedId html5 (id not synced)', function(done) { + it('test hook from sharedId html5 (id not synced)', function (done) { // simulate existing browser local storage values localStorage.setItem('sharedid', JSON.stringify({'id': 'test_sharedId', 'ns': true, 'ts': 1590525289611})); localStorage.setItem('sharedid_exp', ''); @@ -915,7 +1076,7 @@ describe('User ID', function() { setSubmoduleRegistry([sharedIdSubmodule]); init(config); config.setConfig(getConfigMock(['sharedId', 'sharedid', 'html5'])); - requestBidsHook(function() { + requestBidsHook(function () { adUnits.forEach(unit => { unit.bids.forEach(bid => { expect(bid).to.have.deep.nested.property('userId.sharedid'); @@ -937,14 +1098,17 @@ describe('User ID', function() { done(); }, {adUnits}); }); - it('test hook from sharedId cookie', function(done) { - coreStorage.setCookie('sharedid', JSON.stringify({'id': 'test_sharedId', 'ts': 1590525289611}), (new Date(Date.now() + 100000).toUTCString())); + it('test hook from sharedId cookie', function (done) { + coreStorage.setCookie('sharedid', JSON.stringify({ + 'id': 'test_sharedId', + 'ts': 1590525289611 + }), (new Date(Date.now() + 100000).toUTCString())); setSubmoduleRegistry([sharedIdSubmodule]); init(config); config.setConfig(getConfigMock(['sharedId', 'sharedid', 'cookie'])); - requestBidsHook(function() { + requestBidsHook(function () { adUnits.forEach(unit => { unit.bids.forEach(bid => { expect(bid).to.have.deep.nested.property('userId.sharedid'); @@ -970,14 +1134,18 @@ describe('User ID', function() { done(); }, {adUnits}); }); - it('test hook from sharedId cookie (id not synced) ', function(done) { - coreStorage.setCookie('sharedid', JSON.stringify({'id': 'test_sharedId', 'ns': true, 'ts': 1590525289611}), (new Date(Date.now() + 100000).toUTCString())); + it('test hook from sharedId cookie (id not synced) ', function (done) { + coreStorage.setCookie('sharedid', JSON.stringify({ + 'id': 'test_sharedId', + 'ns': true, + 'ts': 1590525289611 + }), (new Date(Date.now() + 100000).toUTCString())); setSubmoduleRegistry([sharedIdSubmodule]); init(config); config.setConfig(getConfigMock(['sharedId', 'sharedid', 'cookie'])); - requestBidsHook(function() { + requestBidsHook(function () { adUnits.forEach(unit => { unit.bids.forEach(bid => { expect(bid).to.have.deep.nested.property('userId.sharedid'); @@ -999,7 +1167,104 @@ describe('User ID', function() { }, {adUnits}); }); - it('test hook from liveIntentId html5', function(done) { + it('test hook from pubProvidedId config params', function (done) { + setSubmoduleRegistry([pubProvidedIdSubmodule]); + init(config); + config.setConfig({ + userSync: { + syncDelay: 0, + userIds: [{ + name: 'pubProvidedId', + params: { + eids: [{ + source: 'example.com', + uids: [{ + id: 'value read from cookie or local storage', + ext: { + stype: 'ppuid' + } + }] + }, { + source: 'id-partner.com', + uids: [{ + id: 'value read from cookie or local storage', + ext: { + stype: 'dmp' + } + }] + }], + eidsFunction: function () { + return [{ + source: 'provider.com', + uids: [{ + id: 'value read from cookie or local storage', + ext: { + stype: 'sha256email' + } + }] + }] + } + } + } + ] + } + }); + + requestBidsHook(function () { + adUnits.forEach(unit => { + unit.bids.forEach(bid => { + expect(bid).to.have.deep.nested.property('userId.pubProvidedId'); + expect(bid.userId.pubProvidedId).to.deep.equal([{ + source: 'example.com', + uids: [{ + id: 'value read from cookie or local storage', + ext: { + stype: 'ppuid' + } + }] + }, { + source: 'id-partner.com', + uids: [{ + id: 'value read from cookie or local storage', + ext: { + stype: 'dmp' + } + }] + }, { + source: 'provider.com', + uids: [{ + id: 'value read from cookie or local storage', + ext: { + stype: 'sha256email' + } + }] + }]); + + expect(bid.userIdAsEids[0]).to.deep.equal({ + source: 'example.com', + uids: [{ + id: 'value read from cookie or local storage', + ext: { + stype: 'ppuid' + } + }] + }); + expect(bid.userIdAsEids[2]).to.deep.equal({ + source: 'provider.com', + uids: [{ + id: 'value read from cookie or local storage', + ext: { + stype: 'sha256email' + } + }] + }); + }); + }); + done(); + }, {adUnits}); + }); + + it('test hook from liveIntentId html5', function (done) { // simulate existing browser local storage values localStorage.setItem('_li_pbid', JSON.stringify({'unifiedId': 'random-ls-identifier', 'segments': ['123']})); localStorage.setItem('_li_pbid_exp', ''); @@ -1007,7 +1272,7 @@ describe('User ID', function() { setSubmoduleRegistry([liveIntentIdSubmodule]); init(config); config.setConfig(getConfigMock(['liveIntentId', '_li_pbid', 'html5'])); - requestBidsHook(function() { + requestBidsHook(function () { adUnits.forEach(unit => { unit.bids.forEach(bid => { expect(bid).to.have.deep.nested.property('userId.lipb'); @@ -1026,7 +1291,7 @@ describe('User ID', function() { }, {adUnits}); }); - it('test hook from liveIntentId cookie', function(done) { + it('test hook from liveIntentId cookie', function (done) { coreStorage.setCookie('_li_pbid', JSON.stringify({ 'unifiedId': 'random-cookie-identifier', 'segments': ['123'] @@ -1036,7 +1301,7 @@ describe('User ID', function() { init(config); config.setConfig(getConfigMock(['liveIntentId', '_li_pbid', 'cookie'])); - requestBidsHook(function() { + requestBidsHook(function () { adUnits.forEach(unit => { unit.bids.forEach(bid => { expect(bid).to.have.deep.nested.property('userId.lipb'); @@ -1054,7 +1319,7 @@ describe('User ID', function() { }, {adUnits}); }); - it('test hook from britepoolid cookies', function(done) { + it('test hook from britepoolid cookies', function (done) { // simulate existing browser local storage values coreStorage.setCookie('britepoolid', JSON.stringify({'primaryBPID': '279c0161-5152-487f-809e-05d7f7e653fd'}), (new Date(Date.now() + 5000).toUTCString())); @@ -1062,7 +1327,7 @@ describe('User ID', function() { init(config); config.setConfig(getConfigMock(['britepoolId', 'britepoolid', 'cookie'])); - requestBidsHook(function() { + requestBidsHook(function () { adUnits.forEach(unit => { unit.bids.forEach(bid => { expect(bid).to.have.deep.nested.property('userId.britepoolid'); @@ -1078,7 +1343,7 @@ describe('User ID', function() { }, {adUnits}); }); - it('test hook from netId cookies', function(done) { + it('test hook from netId cookies', function (done) { // simulate existing browser local storage values coreStorage.setCookie('netId', JSON.stringify({'netId': 'fH5A3n2O8_CZZyPoJVD-eabc6ECb7jhxCicsds7qSg'}), (new Date(Date.now() + 5000).toUTCString())); @@ -1086,7 +1351,7 @@ describe('User ID', function() { init(config); config.setConfig(getConfigMock(['netId', 'netId', 'cookie'])); - requestBidsHook(function() { + requestBidsHook(function () { adUnits.forEach(unit => { unit.bids.forEach(bid => { expect(bid).to.have.deep.nested.property('userId.netId'); @@ -1102,7 +1367,7 @@ describe('User ID', function() { }, {adUnits}); }); - it('test hook from intentIqId cookies', function(done) { + it('test hook from intentIqId cookies', function (done) { // simulate existing browser local storage values coreStorage.setCookie('intentIqId', 'abcdefghijk', (new Date(Date.now() + 5000).toUTCString())); @@ -1110,7 +1375,7 @@ describe('User ID', function() { init(config); config.setConfig(getConfigMock(['intentIqId', 'intentIqId', 'cookie'])); - requestBidsHook(function() { + requestBidsHook(function () { adUnits.forEach(unit => { unit.bids.forEach(bid => { expect(bid).to.have.deep.nested.property('userId.intentIqId'); @@ -1126,7 +1391,7 @@ describe('User ID', function() { }, {adUnits}); }); - it('test hook from haloId html5', function(done) { + it('test hook from haloId html5', function (done) { // simulate existing browser local storage values localStorage.setItem('haloId', JSON.stringify({'haloId': 'random-ls-identifier'})); localStorage.setItem('haloId_exp', ''); @@ -1135,7 +1400,7 @@ describe('User ID', function() { init(config); config.setConfig(getConfigMock(['haloId', 'haloId', 'html5'])); - requestBidsHook(function() { + requestBidsHook(function () { adUnits.forEach(unit => { unit.bids.forEach(bid => { expect(bid).to.have.deep.nested.property('userId.haloId'); @@ -1152,7 +1417,7 @@ describe('User ID', function() { }, {adUnits}); }); - it('test hook from merkleId cookies', function(done) { + it('test hook from merkleId cookies', function (done) { // simulate existing browser local storage values coreStorage.setCookie('merkleId', JSON.stringify({'ppid': {'id': 'testmerkleId'}}), (new Date(Date.now() + 5000).toUTCString())); @@ -1160,7 +1425,7 @@ describe('User ID', function() { init(config); config.setConfig(getConfigMock(['merkleId', 'merkleId', 'cookie'])); - requestBidsHook(function() { + requestBidsHook(function () { adUnits.forEach(unit => { unit.bids.forEach(bid => { expect(bid).to.have.deep.nested.property('userId.merkleId'); @@ -1176,7 +1441,7 @@ describe('User ID', function() { }, {adUnits}); }); - it('test hook from zeotapIdPlus cookies', function(done) { + it('test hook from zeotapIdPlus cookies', function (done) { // simulate existing browser local storage values coreStorage.setCookie('IDP', btoa(JSON.stringify('abcdefghijk')), (new Date(Date.now() + 5000).toUTCString())); @@ -1184,7 +1449,7 @@ describe('User ID', function() { init(config); config.setConfig(getConfigMock(['zeotapIdPlus', 'IDP', 'cookie'])); - requestBidsHook(function() { + requestBidsHook(function () { adUnits.forEach(unit => { unit.bids.forEach(bid => { expect(bid).to.have.deep.nested.property('userId.IDP'); @@ -1200,7 +1465,7 @@ describe('User ID', function() { }, {adUnits}); }); - it('test hook when pubCommonId, unifiedId, id5Id, identityLink, britepoolId, intentIqId, zeotapIdPlus, sharedId, netId and haloId have data to pass', function(done) { + it('test hook when pubCommonId, unifiedId, id5Id, identityLink, britepoolId, intentIqId, zeotapIdPlus, sharedId, netId, haloId and Criteo have data to pass', function (done) { coreStorage.setCookie('pubcid', 'testpubcid', (new Date(Date.now() + 5000).toUTCString())); coreStorage.setCookie('unifiedid', JSON.stringify({'TDID': 'testunifiedid'}), (new Date(Date.now() + 5000).toUTCString())); coreStorage.setCookie('id5id', JSON.stringify({'universal_uid': 'testid5id'}), (new Date(Date.now() + 5000).toUTCString())); @@ -1209,10 +1474,14 @@ describe('User ID', function() { coreStorage.setCookie('netId', JSON.stringify({'netId': 'testnetId'}), (new Date(Date.now() + 5000).toUTCString())); coreStorage.setCookie('intentIqId', 'testintentIqId', (new Date(Date.now() + 5000).toUTCString())); coreStorage.setCookie('IDP', btoa(JSON.stringify('zeotapId')), (new Date(Date.now() + 5000).toUTCString())); - coreStorage.setCookie('sharedid', JSON.stringify({'id': 'test_sharedId', 'ts': 1590525289611}), (new Date(Date.now() + 5000).toUTCString())); + coreStorage.setCookie('sharedid', JSON.stringify({ + 'id': 'test_sharedId', + 'ts': 1590525289611 + }), (new Date(Date.now() + 5000).toUTCString())); coreStorage.setCookie('haloId', JSON.stringify({'haloId': 'testHaloId'}), (new Date(Date.now() + 5000).toUTCString())); + coreStorage.setCookie('storage_criteo', JSON.stringify({'criteoId': 'test_bidid'}), (new Date(Date.now() + 5000).toUTCString())); - setSubmoduleRegistry([pubCommonIdSubmodule, unifiedIdSubmodule, id5IdSubmodule, identityLinkSubmodule, britepoolIdSubmodule, netIdSubmodule, sharedIdSubmodule, intentIqIdSubmodule, zeotapIdPlusSubmodule, haloIdSubmodule]); + setSubmoduleRegistry([pubCommonIdSubmodule, unifiedIdSubmodule, id5IdSubmodule, identityLinkSubmodule, britepoolIdSubmodule, netIdSubmodule, sharedIdSubmodule, intentIqIdSubmodule, zeotapIdPlusSubmodule, haloIdSubmodule, criteoIdSubmodule]); init(config); config.setConfig(getConfigMock(['pubCommonId', 'pubcid', 'cookie'], ['unifiedId', 'unifiedid', 'cookie'], @@ -1223,9 +1492,10 @@ describe('User ID', function() { ['sharedId', 'sharedid', 'cookie'], ['intentIqId', 'intentIqId', 'cookie'], ['zeotapIdPlus', 'IDP', 'cookie'], - ['haloId', 'haloId', 'cookie'])); + ['haloId', 'haloId', 'cookie'], + ['criteo', 'storage_criteo', 'cookie'])); - requestBidsHook(function() { + requestBidsHook(function () { adUnits.forEach(unit => { unit.bids.forEach(bid => { // verify that the PubCommonId id data was copied to bid @@ -1235,8 +1505,8 @@ describe('User ID', function() { expect(bid).to.have.deep.nested.property('userId.tdid'); expect(bid.userId.tdid).to.equal('testunifiedid'); // also check that Id5Id id data was copied to bid - expect(bid).to.have.deep.nested.property('userId.id5id'); - expect(bid.userId.id5id).to.equal('testid5id'); + expect(bid).to.have.deep.nested.property('userId.id5id.uid'); + expect(bid.userId.id5id.uid).to.equal('testid5id'); // check that identityLink id data was copied to bid expect(bid).to.have.deep.nested.property('userId.idl_env'); expect(bid.userId.idl_env).to.equal('AiGNC8Z5ONyZKSpIPf'); @@ -1260,7 +1530,11 @@ describe('User ID', function() { // also check that haloId id was copied to bid expect(bid).to.have.deep.nested.property('userId.haloId'); expect(bid.userId.haloId).to.equal('testHaloId'); - expect(bid.userIdAsEids.length).to.equal(10); + // also check that criteo id was copied to bid + expect(bid).to.have.deep.nested.property('userId.criteoId'); + expect(bid.userId.criteoId).to.equal('test_bidid'); + + expect(bid.userIdAsEids.length).to.equal(11); }); }); coreStorage.setCookie('pubcid', '', EXPIRED_COOKIE_DATE); @@ -1273,21 +1547,26 @@ describe('User ID', function() { coreStorage.setCookie('intentIqId', '', EXPIRED_COOKIE_DATE); coreStorage.setCookie('IDP', '', EXPIRED_COOKIE_DATE); coreStorage.setCookie('haloId', '', EXPIRED_COOKIE_DATE); + coreStorage.setCookie('storage_criteo', '', EXPIRED_COOKIE_DATE); done(); }, {adUnits}); }); - it('test hook when pubCommonId, unifiedId, id5Id, britepoolId, intentIqId, zeotapIdPlus, sharedId, netId and haloId have their modules added before and after init', function(done) { + it('test hook when pubCommonId, unifiedId, id5Id, britepoolId, intentIqId, zeotapIdPlus, sharedId, criteo, netId and haloId have their modules added before and after init', function (done) { coreStorage.setCookie('pubcid', 'testpubcid', (new Date(Date.now() + 5000).toUTCString())); coreStorage.setCookie('unifiedid', JSON.stringify({'TDID': 'cookie-value-add-module-variations'}), new Date(Date.now() + 5000).toUTCString()); coreStorage.setCookie('id5id', JSON.stringify({'universal_uid': 'testid5id'}), (new Date(Date.now() + 5000).toUTCString())); coreStorage.setCookie('idl_env', 'AiGNC8Z5ONyZKSpIPf', new Date(Date.now() + 5000).toUTCString()); coreStorage.setCookie('britepoolid', JSON.stringify({'primaryBPID': 'testbritepoolid'}), (new Date(Date.now() + 5000).toUTCString())); coreStorage.setCookie('netId', JSON.stringify({'netId': 'testnetId'}), (new Date(Date.now() + 5000).toUTCString())); - coreStorage.setCookie('sharedid', JSON.stringify({'id': 'test_sharedId', 'ts': 1590525289611}), (new Date(Date.now() + 5000).toUTCString())); + coreStorage.setCookie('sharedid', JSON.stringify({ + 'id': 'test_sharedId', + 'ts': 1590525289611 + }), (new Date(Date.now() + 5000).toUTCString())); coreStorage.setCookie('intentIqId', 'testintentIqId', (new Date(Date.now() + 5000).toUTCString())); coreStorage.setCookie('IDP', btoa(JSON.stringify('zeotapId')), (new Date(Date.now() + 5000).toUTCString())); coreStorage.setCookie('haloId', JSON.stringify({'haloId': 'testHaloId'}), (new Date(Date.now() + 5000).toUTCString())); + coreStorage.setCookie('storage_criteo', JSON.stringify({'criteoId': 'test_bidid'}), (new Date(Date.now() + 5000).toUTCString())); setSubmoduleRegistry([]); @@ -1306,6 +1585,7 @@ describe('User ID', function() { attachIdSystem(intentIqIdSubmodule); attachIdSystem(zeotapIdPlusSubmodule); attachIdSystem(haloIdSubmodule); + attachIdSystem(criteoIdSubmodule); config.setConfig(getConfigMock(['pubCommonId', 'pubcid', 'cookie'], ['unifiedId', 'unifiedid', 'cookie'], @@ -1316,9 +1596,10 @@ describe('User ID', function() { ['sharedId', 'sharedid', 'cookie'], ['intentIqId', 'intentIqId', 'cookie'], ['zeotapIdPlus', 'IDP', 'cookie'], - ['haloId', 'haloId', 'cookie'])); + ['haloId', 'haloId', 'cookie'], + ['criteo', 'storage_criteo', 'cookie'])); - requestBidsHook(function() { + requestBidsHook(function () { adUnits.forEach(unit => { unit.bids.forEach(bid => { // verify that the PubCommonId id data was copied to bid @@ -1328,8 +1609,8 @@ describe('User ID', function() { expect(bid).to.have.deep.nested.property('userId.tdid'); expect(bid.userId.tdid).to.equal('cookie-value-add-module-variations'); // also check that Id5Id id data was copied to bid - expect(bid).to.have.deep.nested.property('userId.id5id'); - expect(bid.userId.id5id).to.equal('testid5id'); + expect(bid).to.have.deep.nested.property('userId.id5id.uid'); + expect(bid.userId.id5id.uid).to.equal('testid5id'); // also check that identityLink id data was copied to bid expect(bid).to.have.deep.nested.property('userId.idl_env'); expect(bid.userId.idl_env).to.equal('AiGNC8Z5ONyZKSpIPf'); @@ -1354,7 +1635,12 @@ describe('User ID', function() { // also check that haloId id data was copied to bid expect(bid).to.have.deep.nested.property('userId.haloId'); expect(bid.userId.haloId).to.equal('testHaloId'); - expect(bid.userIdAsEids.length).to.equal(10); + + // also check that criteo id data was copied to bid + expect(bid).to.have.deep.nested.property('userId.criteoId'); + expect(bid.userId.criteoId).to.equal('test_bidid'); + + expect(bid.userIdAsEids.length).to.equal(11); }); }); coreStorage.setCookie('pubcid', '', EXPIRED_COOKIE_DATE); @@ -1367,12 +1653,16 @@ describe('User ID', function() { coreStorage.setCookie('intentIqId', '', EXPIRED_COOKIE_DATE); coreStorage.setCookie('IDP', '', EXPIRED_COOKIE_DATE); coreStorage.setCookie('haloId', '', EXPIRED_COOKIE_DATE); + coreStorage.setCookie('storage_criteo', '', EXPIRED_COOKIE_DATE); done(); }, {adUnits}); }); - it('test hook when sharedId(opted out) have their modules added before and after init', function(done) { - coreStorage.setCookie('sharedid', JSON.stringify({'id': '00000000000000000000000000', 'ts': 1590525289611}), (new Date(Date.now() + 5000).toUTCString())); + it('test hook when sharedId(opted out) have their modules added before and after init', function (done) { + coreStorage.setCookie('sharedid', JSON.stringify({ + 'id': '00000000000000000000000000', + 'ts': 1590525289611 + }), (new Date(Date.now() + 5000).toUTCString())); setSubmoduleRegistry([]); init(config); @@ -1381,7 +1671,7 @@ describe('User ID', function() { config.setConfig(getConfigMock(['sharedId', 'sharedid', 'cookie'])); - requestBidsHook(function() { + requestBidsHook(function () { adUnits.forEach(unit => { unit.bids.forEach(bid => { expect(bid.userIdAsEids).to.be.undefined; @@ -1392,14 +1682,203 @@ describe('User ID', function() { }, {adUnits}); }); - it('should add new id system ', function(done) { + it('test sharedid enabled via pubcid cookie', function (done) { + coreStorage.setCookie('pubcid', 'testpubcid', (new Date(Date.now() + 5000).toUTCString())); + coreStorage.setCookie('pubcid_sharedid', 'testsharedid', (new Date(Date.now() + 5000).toUTCString())); + + setSubmoduleRegistry([pubCommonIdSubmodule]); + init(config); + + const customCfg = getConfigMock(['pubCommonId', 'pubcid', 'cookie']); + addConfig(customCfg, 'params', {enableSharedId: true}); + config.setConfig(customCfg); + + requestBidsHook(function () { + adUnits.forEach(unit => { + unit.bids.forEach(bid => { + // verify that the PubCommonId id data was copied to bid + expect(bid).to.have.deep.nested.property('userId.pubcid'); + expect(bid.userId.pubcid).to.equal('testpubcid'); + expect(findEid(bid.userIdAsEids, 'pubcid.org')).to.deep.equal( + {'source': 'pubcid.org', 'uids': [{'id': 'testpubcid', 'atype': 1}]} + ); + // verify that the sharedid was also copied to bid + expect(bid).to.have.deep.nested.property('userId.sharedid'); + expect(bid.userId.sharedid).to.deep.equal({id: 'testsharedid'}); + expect(findEid(bid.userIdAsEids, 'sharedid.org')).to.deep.equal( + {'source': 'sharedid.org', 'uids': [{'id': 'testsharedid', 'atype': 1}]} + ); + }); + }); + coreStorage.setCookie('pubcid', '', EXPIRED_COOKIE_DATE); + coreStorage.setCookie('pubcid_sharedid', '', EXPIRED_COOKIE_DATE); + + done(); + }, {adUnits}); + }); + + it('test sharedid disabled via pubcid cookie', function (done) { + coreStorage.setCookie('pubcid', 'testpubcid', (new Date(Date.now() + 5000).toUTCString())); + coreStorage.setCookie('pubcid_sharedid', 'testsharedid', (new Date(Date.now() + 5000).toUTCString())); + + setSubmoduleRegistry([pubCommonIdSubmodule]); + init(config); + + // pubCommonId's support for sharedId is off by default + config.setConfig(getConfigMock(['pubCommonId', 'pubcid', 'cookie'])); + + requestBidsHook(function () { + adUnits.forEach(unit => { + unit.bids.forEach(bid => { + // verify that the PubCommonId id data was copied to bid + expect(bid).to.have.deep.nested.property('userId.pubcid'); + expect(bid.userId.pubcid).to.equal('testpubcid'); + expect(findEid(bid.userIdAsEids, 'pubcid.org')).to.deep.equal( + {'source': 'pubcid.org', 'uids': [{'id': 'testpubcid', 'atype': 1}]} + ); + // verify that the sharedid was not added to bid + expect(bid.userId).to.not.have.property('sharedid'); + expect(findEid(bid.userIdAsEids, 'sharedid.org')).to.be.undefined; + }); + }); + coreStorage.setCookie('pubcid', '', EXPIRED_COOKIE_DATE); + coreStorage.setCookie('pubcid_sharedid', '', EXPIRED_COOKIE_DATE); + + done(); + }, {adUnits}); + }); + + it('test sharedid enabled via pubcid html5', function (done) { + coreStorage.setDataInLocalStorage('pubcid', 'testpubcid'); + coreStorage.setDataInLocalStorage('pubcid_exp', new Date(Date.now() + 5000).toUTCString()); + coreStorage.setDataInLocalStorage('pubcid_sharedid', 'testsharedid'); + coreStorage.setDataInLocalStorage('pubcid_sharedid_exp', new Date(Date.now() + 5000).toUTCString()); + + setSubmoduleRegistry([pubCommonIdSubmodule]); + init(config); + + const customCfg = getConfigMock(['pubCommonId', 'pubcid', 'html5']); + addConfig(customCfg, 'params', {enableSharedId: true}); + config.setConfig(customCfg); + + requestBidsHook(function () { + adUnits.forEach(unit => { + unit.bids.forEach(bid => { + // verify that the PubCommonId id data was copied to bid + expect(bid).to.have.deep.nested.property('userId.pubcid'); + expect(bid.userId.pubcid).to.equal('testpubcid'); + expect(findEid(bid.userIdAsEids, 'pubcid.org')).to.deep.equal( + {'source': 'pubcid.org', 'uids': [{'id': 'testpubcid', 'atype': 1}]} + ); + // verify that the sharedid was also copied to bid + expect(bid).to.have.deep.nested.property('userId.sharedid'); + expect(bid.userId.sharedid).to.deep.equal({id: 'testsharedid'}); + expect(findEid(bid.userIdAsEids, 'sharedid.org')).to.deep.equal( + {'source': 'sharedid.org', 'uids': [{'id': 'testsharedid', 'atype': 1}]} + ); + }); + }); + coreStorage.setCookie('pubcid', '', EXPIRED_COOKIE_DATE); + coreStorage.setCookie('pubcid_sharedid', '', EXPIRED_COOKIE_DATE); + + coreStorage.removeDataFromLocalStorage('pubcid'); + coreStorage.removeDataFromLocalStorage('pubcid_exp'); + coreStorage.removeDataFromLocalStorage('pubcid_sharedid'); + coreStorage.removeDataFromLocalStorage('pubcid_sharedid_exp'); + done(); + }, {adUnits}); + }); + + it('test expired sharedid via pubcid html5', function (done) { + coreStorage.setDataInLocalStorage('pubcid', 'testpubcid'); + coreStorage.setDataInLocalStorage('pubcid_exp', new Date(Date.now() + 5000).toUTCString()); + + // set sharedid to expired already + coreStorage.setDataInLocalStorage('pubcid_sharedid', 'testsharedid'); + coreStorage.setDataInLocalStorage('pubcid_sharedid_exp', new Date(Date.now() - 100).toUTCString()); + + setSubmoduleRegistry([pubCommonIdSubmodule]); + init(config); + + const customCfg = getConfigMock(['pubCommonId', 'pubcid', 'html5']); + addConfig(customCfg, 'params', {enableSharedId: true}); + config.setConfig(customCfg); + + requestBidsHook(function () { + adUnits.forEach(unit => { + unit.bids.forEach(bid => { + // verify that the PubCommonId id data was copied to bid + expect(bid).to.have.deep.nested.property('userId.pubcid'); + expect(bid.userId.pubcid).to.equal('testpubcid'); + expect(findEid(bid.userIdAsEids, 'pubcid.org')).to.deep.equal( + {'source': 'pubcid.org', 'uids': [{'id': 'testpubcid', 'atype': 1}]} + ); + // verify that the sharedid was not added + expect(bid.userId).to.not.have.property('sharedid'); + expect(findEid(bid.userIdAsEids, 'sharedid.org')).to.be.undefined; + }); + }); + + coreStorage.removeDataFromLocalStorage('pubcid'); + coreStorage.removeDataFromLocalStorage('pubcid_exp'); + coreStorage.removeDataFromLocalStorage('pubcid_sharedid'); + coreStorage.removeDataFromLocalStorage('pubcid_sharedid_exp'); + done(); + }, {adUnits}); + }); + + it('test pubcid coexisting with sharedid', function (done) { + coreStorage.setCookie('pubcid', 'testpubcid', (new Date(Date.now() + 5000).toUTCString())); + coreStorage.setCookie('pubcid_sharedid', 'test111', (new Date(Date.now() + 5000).toUTCString())); + coreStorage.setCookie('sharedid', JSON.stringify({ + 'id': 'test222', + 'ts': 1590525289611 + }), (new Date(Date.now() + 5000).toUTCString())); + + // When both pubcommon and sharedid are configured, pubcommon are invoked first due to + // module loading order. This mean the output from the primary sharedid module will overwrite + // the one in pubcommon. + + setSubmoduleRegistry([pubCommonIdSubmodule, sharedIdSubmodule]); + init(config); + config.setConfig(getConfigMock(['pubCommonId', 'pubcid', 'cookie'], + ['sharedId', 'sharedid', 'cookie'], + )); + + requestBidsHook(function () { + adUnits.forEach(unit => { + unit.bids.forEach(bid => { + // verify that the PubCommonId id data was copied to bid + expect(bid).to.have.deep.nested.property('userId.pubcid'); + expect(bid.userId.pubcid).to.equal('testpubcid'); + expect(findEid(bid.userIdAsEids, 'pubcid.org')).to.deep.equal( + {'source': 'pubcid.org', 'uids': [{'id': 'testpubcid', 'atype': 1}]} + ); + // verify that the sharedid was also copied to bid + expect(bid).to.have.deep.nested.property('userId.sharedid'); + expect(bid.userId.sharedid.id).to.equal('test222'); + expect(findEid(bid.userIdAsEids, 'sharedid.org').uids[0].id).to.equal('test222'); + }); + }); + coreStorage.setCookie('pubcid', '', EXPIRED_COOKIE_DATE); + coreStorage.setCookie('pubcid_sharedid', '', EXPIRED_COOKIE_DATE); + coreStorage.setCookie('sharedid', '', EXPIRED_COOKIE_DATE); + + done(); + }, {adUnits}); + }); + + it('should add new id system ', function (done) { coreStorage.setCookie('pubcid', 'testpubcid', (new Date(Date.now() + 5000).toUTCString())); coreStorage.setCookie('unifiedid', JSON.stringify({'TDID': 'cookie-value-add-module-variations'}), new Date(Date.now() + 5000).toUTCString()); coreStorage.setCookie('id5id', JSON.stringify({'universal_uid': 'testid5id'}), (new Date(Date.now() + 5000).toUTCString())); coreStorage.setCookie('idl_env', 'AiGNC8Z5ONyZKSpIPf', new Date(Date.now() + 5000).toUTCString()); coreStorage.setCookie('britepoolid', JSON.stringify({'primaryBPID': 'testbritepoolid'}), (new Date(Date.now() + 5000).toUTCString())); coreStorage.setCookie('netId', JSON.stringify({'netId': 'testnetId'}), new Date(Date.now() + 5000).toUTCString()); - coreStorage.setCookie('sharedid', JSON.stringify({'id': 'test_sharedId', 'ts': 1590525289611}), new Date(Date.now() + 5000).toUTCString()); + coreStorage.setCookie('sharedid', JSON.stringify({ + 'id': 'test_sharedId', + 'ts': 1590525289611 + }), new Date(Date.now() + 5000).toUTCString()); coreStorage.setCookie('intentIqId', 'testintentIqId', (new Date(Date.now() + 5000).toUTCString())); coreStorage.setCookie('IDP', btoa(JSON.stringify('zeotapId')), (new Date(Date.now() + 5000).toUTCString())); coreStorage.setCookie('haloId', JSON.stringify({'haloId': 'testHaloId'}), (new Date(Date.now() + 5000).toUTCString())); @@ -1426,11 +1905,11 @@ describe('User ID', function() { }, { name: 'sharedId', storage: {name: 'sharedid', type: 'cookie'} }, { - name: 'intentIqId', storage: { name: 'intentIqId', type: 'cookie' } + name: 'intentIqId', storage: {name: 'intentIqId', type: 'cookie'} }, { name: 'zeotapIdPlus' }, { - name: 'haloId', storage: { name: 'haloId', type: 'cookie' } + name: 'haloId', storage: {name: 'haloId', type: 'cookie'} }, { name: 'mockId', storage: {name: 'MOCKID', type: 'cookie'} }] @@ -1440,18 +1919,18 @@ describe('User ID', function() { // Add new submodule named 'mockId' attachIdSystem({ name: 'mockId', - decode: function(value) { + decode: function (value) { return { 'mid': value['MOCKID'] }; }, - getId: function(params, storedId) { + getId: function (config, storedId) { if (storedId) return {}; return {id: {'MOCKID': '1234'}}; } }); - requestBidsHook(function() { + requestBidsHook(function () { adUnits.forEach(unit => { unit.bids.forEach(bid => { // check PubCommonId id data was copied to bid @@ -1461,8 +1940,8 @@ describe('User ID', function() { expect(bid).to.have.deep.nested.property('userId.tdid'); expect(bid.userId.tdid).to.equal('cookie-value-add-module-variations'); // also check that Id5Id id data was copied to bid - expect(bid).to.have.deep.nested.property('userId.id5id'); - expect(bid.userId.id5id).to.equal('testid5id'); + expect(bid).to.have.deep.nested.property('userId.id5id.uid'); + expect(bid.userId.id5id.uid).to.equal('testid5id'); // also check that identityLink id data was copied to bid expect(bid).to.have.deep.nested.property('userId.idl_env'); expect(bid.userId.idl_env).to.equal('AiGNC8Z5ONyZKSpIPf'); @@ -1509,27 +1988,29 @@ describe('User ID', function() { }); }); - describe('callbacks at the end of auction', function() { - beforeEach(function() { + describe('callbacks at the end of auction', function () { + beforeEach(function () { sinon.stub(events, 'getEvents').returns([]); sinon.stub(utils, 'triggerPixel'); coreStorage.setCookie('pubcid', '', EXPIRED_COOKIE_DATE); + coreStorage.setCookie('pubcid_sharedid', '', EXPIRED_COOKIE_DATE); coreStorage.setCookie('unifiedid', '', EXPIRED_COOKIE_DATE); coreStorage.setCookie('_parrable_eid', '', EXPIRED_COOKIE_DATE); }); - afterEach(function() { + afterEach(function () { events.getEvents.restore(); utils.triggerPixel.restore(); coreStorage.setCookie('pubcid', '', EXPIRED_COOKIE_DATE); + coreStorage.setCookie('pubcid_sharedid', '', EXPIRED_COOKIE_DATE); coreStorage.setCookie('unifiedid', '', EXPIRED_COOKIE_DATE); coreStorage.setCookie('_parrable_eid', '', EXPIRED_COOKIE_DATE); }); - it('pubcid callback with url', function() { + it('pubcid callback with url', function () { let adUnits = [getAdUnitMock()]; let innerAdUnits; - let customCfg = getConfigMock(['pubCommonId', 'pubcid_alt', 'cookie']); + let customCfg = getConfigMock(['pubCommonId', 'pubcid', 'cookie']); customCfg = addConfig(customCfg, 'params', {pixelUrl: '/any/pubcid/url'}); setSubmoduleRegistry([pubCommonIdSubmodule, unifiedIdSubmodule]); @@ -1544,7 +2025,7 @@ describe('User ID', function() { expect(utils.triggerPixel.getCall(0).args[0]).to.include('/any/pubcid/url'); }); - it('unifiedid callback with url', function() { + it('unifiedid callback with url', function () { let adUnits = [getAdUnitMock()]; let innerAdUnits; let customCfg = getConfigMock(['unifiedId', 'unifiedid', 'cookie']); @@ -1562,7 +2043,7 @@ describe('User ID', function() { expect(server.requests[0].url).to.equal('/any/unifiedid/url'); }); - it('unifiedid callback with partner', function() { + it('unifiedid callback with partner', function () { let adUnits = [getAdUnitMock()]; let innerAdUnits; let customCfg = getConfigMock(['unifiedId', 'unifiedid', 'cookie']); @@ -1579,9 +2060,88 @@ describe('User ID', function() { events.emit(CONSTANTS.EVENTS.AUCTION_END, {}); expect(server.requests[0].url).to.equal('https://match.adsrvr.org/track/rid?ttd_pid=rubicon&fmt=json'); }); + + it('verify sharedid callback via pubcid when enabled', function () { + let adUnits = [getAdUnitMock()]; + let innerAdUnits; + let customCfg = getConfigMock(['pubCommonId', 'pubcid', 'cookie']); + customCfg = addConfig(customCfg, 'params', {pixelUrl: '/any/pubcid/url', enableSharedId: true}); + + server.respondWith('https://id.sharedid.org/id', function(xhr) { + xhr.respond(200, {}, '{"sharedId":"testsharedid"}'); + }); + server.respondImmediately = true; + + setSubmoduleRegistry([pubCommonIdSubmodule]); + init(config); + config.setConfig(customCfg); + requestBidsHook((config) => { + innerAdUnits = config.adUnits + }, {adUnits}); + + expect(utils.triggerPixel.called).to.be.false; + events.emit(CONSTANTS.EVENTS.AUCTION_END, {}); + expect(utils.triggerPixel.getCall(0).args[0]).to.include('/any/pubcid/url'); + + expect(server.requests[0].url).to.equal('https://id.sharedid.org/id'); + expect(coreStorage.getCookie('pubcid_sharedid')).to.equal('testsharedid'); + }); + + it('verify no sharedid callback via pubcid when disabled', function () { + let adUnits = [getAdUnitMock()]; + let innerAdUnits; + let customCfg = getConfigMock(['pubCommonId', 'pubcid', 'cookie']); + customCfg = addConfig(customCfg, 'params', {pixelUrl: '/any/pubcid/url'}); + + server.respondWith('https://id.sharedid.org/id', function(xhr) { + xhr.respond(200, {}, '{"sharedId":"testsharedid"}'); + }); + server.respondImmediately = true; + + setSubmoduleRegistry([pubCommonIdSubmodule]); + init(config); + config.setConfig(customCfg); + requestBidsHook((config) => { + innerAdUnits = config.adUnits + }, {adUnits}); + + expect(utils.triggerPixel.called).to.be.false; + events.emit(CONSTANTS.EVENTS.AUCTION_END, {}); + expect(utils.triggerPixel.getCall(0).args[0]).to.include('/any/pubcid/url'); + + expect(server.requests).to.have.lengthOf(0); + expect(coreStorage.getCookie('pubcid_sharedid')).to.be.null; + }); + + it('verify sharedid optout via pubcid when enabled', function () { + let adUnits = [getAdUnitMock()]; + let innerAdUnits; + let customCfg = getConfigMock(['pubCommonId', 'pubcid', 'cookie']); + customCfg = addConfig(customCfg, 'params', {pixelUrl: '/any/pubcid/url', enableSharedId: true}); + coreStorage.setCookie('pubcid_sharedid', 'testsharedid', (new Date(Date.now() + 5000).toUTCString())); + + server.respondWith('https://id.sharedid.org/id', function(xhr) { + xhr.respond(200, {}, '{"sharedId":"00000000000000000000000000"}'); + }); + server.respondImmediately = true; + + setSubmoduleRegistry([pubCommonIdSubmodule]); + init(config); + config.setConfig(customCfg); + requestBidsHook((config) => { + innerAdUnits = config.adUnits + }, {adUnits}); + + expect(utils.triggerPixel.called).to.be.false; + events.emit(CONSTANTS.EVENTS.AUCTION_END, {}); + expect(utils.triggerPixel.getCall(0).args[0]).to.include('/any/pubcid/url'); + + expect(server.requests[0].url).to.equal('https://id.sharedid.org/id'); + expect(coreStorage.getCookie('pubcid_sharedid')).to.be.null; + }); }); - describe('Set cookie behavior', function() { + describe('Set cookie behavior', function () { let coreStorageSpy; beforeEach(function () { coreStorageSpy = sinon.spy(coreStorage, 'setCookie'); @@ -1620,7 +2180,7 @@ describe('User ID', function() { }); }); - describe('Consent changes determine getId refreshes', function() { + describe('Consent changes determine getId refreshes', function () { let expStr; let adUnits; @@ -1656,7 +2216,7 @@ describe('User ID', function() { allowAuctionWithoutConsent: false }; - const sharedBeforeFunction = function() { + const sharedBeforeFunction = function () { // clear cookies expStr = (new Date(Date.now() + 25000).toUTCString()); coreStorage.setCookie(mockIdCookieName, '', EXPIRED_COOKIE_DATE); @@ -1682,7 +2242,7 @@ describe('User ID', function() { delete window.__tcfapi; }; - describe('TCF v1', function() { + describe('TCF v1', function () { testConsentData = { gdprApplies: true, consentData: 'xyz', @@ -1693,7 +2253,8 @@ describe('User ID', function() { sharedBeforeFunction(); // init v1 consent management - window.__cmp = function () { }; + window.__cmp = function () { + }; delete window.__tcfapi; cmpStub = sinon.stub(window, '__cmp').callsFake((...args) => { args[2](testConsentData); @@ -1701,17 +2262,20 @@ describe('User ID', function() { setConsentConfig(consentConfig); }); - afterEach(function() { + afterEach(function () { sharedAfterFunction(); }); it('does not call getId if no stored consent data and refresh is not needed', function () { - coreStorage.setCookie(mockIdCookieName, JSON.stringify({ id: '1234' }), expStr); + coreStorage.setCookie(mockIdCookieName, JSON.stringify({id: '1234'}), expStr); coreStorage.setCookie(`${mockIdCookieName}_last`, (new Date(Date.now() - 1 * 1000).toUTCString()), expStr); let innerAdUnits; - consentManagementRequestBidsHook(() => { }, {}); - requestBidsHook((config) => { innerAdUnits = config.adUnits }, { adUnits }); + consentManagementRequestBidsHook(() => { + }, {}); + requestBidsHook((config) => { + innerAdUnits = config.adUnits + }, {adUnits}); sinon.assert.notCalled(mockGetId); sinon.assert.calledOnce(mockDecode); @@ -1719,12 +2283,15 @@ describe('User ID', function() { }); it('calls getId if no stored consent data but refresh is needed', function () { - coreStorage.setCookie(mockIdCookieName, JSON.stringify({ id: '1234' }), expStr); + coreStorage.setCookie(mockIdCookieName, JSON.stringify({id: '1234'}), expStr); coreStorage.setCookie(`${mockIdCookieName}_last`, (new Date(Date.now() - 60 * 1000).toUTCString()), expStr); let innerAdUnits; - consentManagementRequestBidsHook(() => { }, {}); - requestBidsHook((config) => { innerAdUnits = config.adUnits }, { adUnits }); + consentManagementRequestBidsHook(() => { + }, {}); + requestBidsHook((config) => { + innerAdUnits = config.adUnits + }, {adUnits}); sinon.assert.calledOnce(mockGetId); sinon.assert.calledOnce(mockDecode); @@ -1732,14 +2299,17 @@ describe('User ID', function() { }); it('calls getId if empty stored consent and refresh not needed', function () { - coreStorage.setCookie(mockIdCookieName, JSON.stringify({ id: '1234' }), expStr); + coreStorage.setCookie(mockIdCookieName, JSON.stringify({id: '1234'}), expStr); coreStorage.setCookie(`${mockIdCookieName}_last`, (new Date(Date.now() - 1 * 1000).toUTCString()), expStr); setStoredConsentData(); let innerAdUnits; - consentManagementRequestBidsHook(() => { }, {}); - requestBidsHook((config) => { innerAdUnits = config.adUnits }, { adUnits }); + consentManagementRequestBidsHook(() => { + }, {}); + requestBidsHook((config) => { + innerAdUnits = config.adUnits + }, {adUnits}); sinon.assert.calledOnce(mockGetId); sinon.assert.calledOnce(mockDecode); @@ -1747,7 +2317,7 @@ describe('User ID', function() { }); it('calls getId if stored consent does not match current consent and refresh not needed', function () { - coreStorage.setCookie(mockIdCookieName, JSON.stringify({ id: '1234' }), expStr); + coreStorage.setCookie(mockIdCookieName, JSON.stringify({id: '1234'}), expStr); coreStorage.setCookie(`${mockIdCookieName}_last`, (new Date(Date.now() - 1 * 1000).toUTCString()), expStr); setStoredConsentData({ @@ -1757,8 +2327,11 @@ describe('User ID', function() { }); let innerAdUnits; - consentManagementRequestBidsHook(() => { }, {}); - requestBidsHook((config) => { innerAdUnits = config.adUnits }, { adUnits }); + consentManagementRequestBidsHook(() => { + }, {}); + requestBidsHook((config) => { + innerAdUnits = config.adUnits + }, {adUnits}); sinon.assert.calledOnce(mockGetId); sinon.assert.calledOnce(mockDecode); @@ -1766,7 +2339,7 @@ describe('User ID', function() { }); it('does not call getId if stored consent matches current consent and refresh not needed', function () { - coreStorage.setCookie(mockIdCookieName, JSON.stringify({ id: '1234' }), expStr); + coreStorage.setCookie(mockIdCookieName, JSON.stringify({id: '1234'}), expStr); coreStorage.setCookie(`${mockIdCookieName}_last`, (new Date(Date.now() - 1 * 1000).toUTCString()), expStr); setStoredConsentData({ @@ -1776,8 +2349,11 @@ describe('User ID', function() { }); let innerAdUnits; - consentManagementRequestBidsHook(() => { }, {}); - requestBidsHook((config) => { innerAdUnits = config.adUnits }, { adUnits }); + consentManagementRequestBidsHook(() => { + }, {}); + requestBidsHook((config) => { + innerAdUnits = config.adUnits + }, {adUnits}); sinon.assert.notCalled(mockGetId); sinon.assert.calledOnce(mockDecode); diff --git a/test/spec/modules/vdoaiBidAdapter_spec.js b/test/spec/modules/vdoaiBidAdapter_spec.js index c9d826d8dc8..5318bb43eca 100644 --- a/test/spec/modules/vdoaiBidAdapter_spec.js +++ b/test/spec/modules/vdoaiBidAdapter_spec.js @@ -37,6 +37,7 @@ describe('vdoaiBidAdapter', function () { 'bidId': '23beaa6af6cdde', 'bidderRequestId': '19c0c1efdf37e7', 'auctionId': '61466567-d482-4a16-96f0-fe5f25ffbdf1', + 'mediaTypes': 'banner' } ]; @@ -101,5 +102,40 @@ describe('vdoaiBidAdapter', function () { let result = spec.interpretResponse(serverResponse, bidRequest[0]); expect(Object.keys(result)).to.deep.equal(Object.keys(expectedResponse)); }); + + it('handles instream video responses', function () { + let serverResponse = { + body: { + 'vdoCreative': '', + 'price': 4.2, + 'adid': '12345asdfg', + 'currency': 'EUR', + 'statusMessage': 'Bid available', + 'requestId': 'bidId123', + 'width': 300, + 'height': 250, + 'netRevenue': true, + 'mediaType': 'video' + } + }; + let bidRequest = [ + { + 'method': 'POST', + 'url': ENDPOINT_URL, + 'data': { + 'placementId': 'testPlacementId', + 'width': '300', + 'height': '200', + 'bidId': 'bidId123', + 'referer': 'www.example.com', + 'mediaType': 'video' + } + } + ]; + + let result = spec.interpretResponse(serverResponse, bidRequest[0]); + expect(result[0]).to.have.property('vastXml'); + expect(result[0]).to.have.property('mediaType', 'video'); + }); }); }); diff --git a/test/spec/modules/verizonMediaIdSystem_spec.js b/test/spec/modules/verizonMediaIdSystem_spec.js new file mode 100644 index 00000000000..c5d743235d6 --- /dev/null +++ b/test/spec/modules/verizonMediaIdSystem_spec.js @@ -0,0 +1,204 @@ +import {expect} from 'chai'; +import * as utils from 'src/utils.js'; +import {verizonMediaIdSubmodule} from 'modules/verizonMediaIdSystem.js'; + +describe('Verizon Media ID Submodule', () => { + const HASHED_EMAIL = '6bda6f2fa268bf0438b5423a9861a2cedaa5dec163c03f743cfe05c08a8397b2'; + const PIXEL_ID = '1234'; + const PROD_ENDPOINT = `https://ups.analytics.yahoo.com/ups/${PIXEL_ID}/fed`; + const OVERRIDE_ENDPOINT = 'https://foo/bar'; + + it('should have the correct module name declared', () => { + expect(verizonMediaIdSubmodule.name).to.equal('verizonMediaId'); + }); + + it('should have the correct TCFv2 Vendor ID declared', () => { + expect(verizonMediaIdSubmodule.gvlid).to.equal(25); + }); + + describe('getId()', () => { + let ajaxStub; + let getAjaxFnStub; + let consentData; + beforeEach(() => { + ajaxStub = sinon.stub(); + getAjaxFnStub = sinon.stub(verizonMediaIdSubmodule, 'getAjaxFn'); + getAjaxFnStub.returns(ajaxStub); + + consentData = { + gdpr: { + gdprApplies: 1, + consentString: 'GDPR_CONSENT_STRING' + }, + uspConsent: 'USP_CONSENT_STRING' + }; + }); + + afterEach(() => { + getAjaxFnStub.restore(); + }); + + function invokeGetIdAPI(configParams, consentData) { + let result = verizonMediaIdSubmodule.getId({ + params: configParams + }, consentData); + if (typeof result === 'object') { + result.callback(sinon.stub()); + } + return result; + } + + it('returns undefined if he and pixelId params are not passed', () => { + expect(invokeGetIdAPI({}, consentData)).to.be.undefined; + expect(ajaxStub.callCount).to.equal(0); + }); + + it('returns undefined if the pixelId param is not passed', () => { + expect(invokeGetIdAPI({ + he: HASHED_EMAIL + }, consentData)).to.be.undefined; + expect(ajaxStub.callCount).to.equal(0); + }); + + it('returns undefined if the he param is not passed', () => { + expect(invokeGetIdAPI({ + pixelId: PIXEL_ID + }, consentData)).to.be.undefined; + expect(ajaxStub.callCount).to.equal(0); + }); + + it('returns an object with the callback function if the correct params are passed', () => { + let result = invokeGetIdAPI({ + he: HASHED_EMAIL, + pixelId: PIXEL_ID + }, consentData); + expect(result).to.be.an('object').that.has.all.keys('callback'); + expect(result.callback).to.be.a('function'); + }); + + it('Makes an ajax GET request to the production API endpoint with query params', () => { + invokeGetIdAPI({ + he: HASHED_EMAIL, + pixelId: PIXEL_ID + }, consentData); + + const expectedParams = { + he: HASHED_EMAIL, + pixelId: PIXEL_ID, + '1p': '0', + gdpr: '1', + euconsent: consentData.gdpr.consentString, + us_privacy: consentData.uspConsent + }; + const requestQueryParams = utils.parseQS(ajaxStub.firstCall.args[0].split('?')[1]); + + expect(ajaxStub.firstCall.args[0].indexOf(`${PROD_ENDPOINT}?`)).to.equal(0); + expect(requestQueryParams).to.deep.equal(expectedParams); + expect(ajaxStub.firstCall.args[3]).to.deep.equal({method: 'GET', withCredentials: true}); + }); + + it('Makes an ajax GET request to the specified override API endpoint with query params', () => { + invokeGetIdAPI({ + he: HASHED_EMAIL, + endpoint: OVERRIDE_ENDPOINT + }, consentData); + + const expectedParams = { + he: HASHED_EMAIL, + '1p': '0', + gdpr: '1', + euconsent: consentData.gdpr.consentString, + us_privacy: consentData.uspConsent + }; + const requestQueryParams = utils.parseQS(ajaxStub.firstCall.args[0].split('?')[1]); + + expect(ajaxStub.firstCall.args[0].indexOf(`${OVERRIDE_ENDPOINT}?`)).to.equal(0); + expect(requestQueryParams).to.deep.equal(expectedParams); + expect(ajaxStub.firstCall.args[3]).to.deep.equal({method: 'GET', withCredentials: true}); + }); + + it('sets the callbacks param of the ajax function call correctly', () => { + invokeGetIdAPI({ + he: HASHED_EMAIL, + pixelId: PIXEL_ID, + }, consentData); + + expect(ajaxStub.firstCall.args[1]).to.be.an('object').that.has.all.keys(['success', 'error']); + }); + + it('sets GDPR consent data flag correctly when call is under GDPR jurisdiction.', () => { + invokeGetIdAPI({ + he: HASHED_EMAIL, + pixelId: PIXEL_ID, + }, consentData); + + const requestQueryParams = utils.parseQS(ajaxStub.firstCall.args[0].split('?')[1]); + expect(requestQueryParams.gdpr).to.equal('1'); + expect(requestQueryParams.euconsent).to.equal(consentData.gdpr.consentString); + }); + + it('sets GDPR consent data flag correctly when call is NOT under GDPR jurisdiction.', () => { + consentData.gdpr.gdprApplies = false; + + invokeGetIdAPI({ + he: HASHED_EMAIL, + pixelId: PIXEL_ID, + }, consentData); + + const requestQueryParams = utils.parseQS(ajaxStub.firstCall.args[0].split('?')[1]); + expect(requestQueryParams.gdpr).to.equal('0'); + expect(requestQueryParams.euconsent).to.equal(''); + }); + + [1, '1', true].forEach(firstPartyParamValue => { + it(`sets 1p payload property to '1' for a config value of ${firstPartyParamValue}`, () => { + invokeGetIdAPI({ + '1p': firstPartyParamValue, + he: HASHED_EMAIL, + pixelId: PIXEL_ID, + }, consentData); + + const requestQueryParams = utils.parseQS(ajaxStub.firstCall.args[0].split('?')[1]); + expect(requestQueryParams['1p']).to.equal('1'); + }); + }); + }); + + describe('decode()', () => { + const VALID_API_RESPONSES = [{ + key: 'vmiud', + expected: '1234', + payload: { + vmuid: '1234' + } + }, + { + key: 'connectid', + expected: '4567', + payload: { + connectid: '4567' + } + }, + { + key: 'both', + expected: '4567', + payload: { + vmuid: '1234', + connectid: '4567' + } + }]; + VALID_API_RESPONSES.forEach(responseData => { + it('should return a newly constructed object with the connectid for a payload with ${responseData.key} key(s)', () => { + expect(verizonMediaIdSubmodule.decode(responseData.payload)).to.deep.equal( + {connectid: responseData.expected} + ); + }); + }); + + [{}, '', {foo: 'bar'}].forEach((response) => { + it(`should return undefined for an invalid response "${JSON.stringify(response)}"`, () => { + expect(verizonMediaIdSubmodule.decode(response)).to.be.undefined; + }); + }); + }); +}); diff --git a/test/spec/modules/vidazooBidAdapter_spec.js b/test/spec/modules/vidazooBidAdapter_spec.js index 1a503e46b5c..d7f20c434ca 100644 --- a/test/spec/modules/vidazooBidAdapter_spec.js +++ b/test/spec/modules/vidazooBidAdapter_spec.js @@ -244,6 +244,7 @@ describe('VidazooBidAdapter', function () { case 'digitrustid': return { data: { id: id } }; case 'lipb': return { lipbid: id }; case 'parrableId': return { eid: id }; + case 'id5id': return { uid: id }; default: return id; } })(); diff --git a/test/spec/modules/visxBidAdapter_spec.js b/test/spec/modules/visxBidAdapter_spec.js index 2a4f8e4c7b5..a06f530e145 100755 --- a/test/spec/modules/visxBidAdapter_spec.js +++ b/test/spec/modules/visxBidAdapter_spec.js @@ -231,7 +231,7 @@ describe('VisxAdapter', function () { const schainBidRequests = [ Object.assign({userId: { tdid: '111', - id5id: '222', + id5id: { uid: '222' }, digitrustid: {data: {id: 'DTID', keyv: 4, privacy: {optout: false}, producer: 'ABC', version: 2}} }}, bidRequests[0]), bidRequests[1], diff --git a/test/spec/modules/vuukleBidAdapter_spec.js b/test/spec/modules/vuukleBidAdapter_spec.js new file mode 100644 index 00000000000..fdca4085c8e --- /dev/null +++ b/test/spec/modules/vuukleBidAdapter_spec.js @@ -0,0 +1,59 @@ +import { expect } from 'chai'; +import { spec } from 'modules/vuukleBidAdapter.js'; + +describe('vuukleBidAdapterTests', function() { + let bidRequestData = { + bids: [ + { + bidId: 'testbid', + bidder: 'vuukle', + params: { + test: 1 + }, + sizes: [[300, 250]] + } + ] + }; + let request = []; + + it('validate_pub_params', function() { + expect( + spec.isBidRequestValid({ + bidder: 'vuukle', + params: { + test: 1 + } + }) + ).to.equal(true); + }); + + it('validate_generated_params', function() { + request = spec.buildRequests(bidRequestData.bids); + let req_data = request[0].data; + + expect(req_data.bidId).to.equal('testbid'); + }); + + it('validate_response_params', function() { + let serverResponse = { + body: { + 'cpm': 0.01, + 'width': 300, + 'height': 250, + 'creative_id': '12345', + 'ad': 'test ad' + } + }; + + request = spec.buildRequests(bidRequestData.bids); + let bids = spec.interpretResponse(serverResponse, request[0]); + expect(bids).to.have.lengthOf(1); + + let bid = bids[0]; + expect(bid.ad).to.equal('test ad'); + expect(bid.cpm).to.equal(0.01); + expect(bid.width).to.equal(300); + expect(bid.height).to.equal(250); + expect(bid.creativeId).to.equal('12345'); + }); +}); diff --git a/test/spec/modules/yieldlabBidAdapter_spec.js b/test/spec/modules/yieldlabBidAdapter_spec.js index 90fa26fa823..cd2c46a5664 100644 --- a/test/spec/modules/yieldlabBidAdapter_spec.js +++ b/test/spec/modules/yieldlabBidAdapter_spec.js @@ -138,6 +138,12 @@ describe('yieldlabBidAdapter', function () { } }) + it('passes unencoded schain string to bid request when complete == 0', function () { + REQUEST.schain.complete = 0; + const request = spec.buildRequests([REQUEST]) + expect(request.url).to.include('schain=1.0,0!indirectseller.com,1,1,,,,!indirectseller2.com,2,1,,indirectseller2%20name%20with%20comma%20%2C%20and%20bang%20%21,,') + }) + it('passes encoded referer to bid request', function () { expect(refererRequest.url).to.include('pubref=https%3A%2F%2Fwww.yieldlab.de%2Ftest%3Fwith%3Dquerystring') }) diff --git a/test/spec/modules/yieldmoBidAdapter_spec.js b/test/spec/modules/yieldmoBidAdapter_spec.js index caeb26266fe..deabef69093 100644 --- a/test/spec/modules/yieldmoBidAdapter_spec.js +++ b/test/spec/modules/yieldmoBidAdapter_spec.js @@ -1,19 +1,16 @@ import { expect } from 'chai'; import { spec } from 'modules/yieldmoBidAdapter.js'; -import { newBidder } from 'src/adapters/bidderFactory.js'; import * as utils from 'src/utils.js'; describe('YieldmoAdapter', function () { - const adapter = newBidder(spec); - const ENDPOINT = 'https://ads.yieldmo.com/exchange/prebid'; + const BANNER_ENDPOINT = 'https://ads.yieldmo.com/exchange/prebid'; + const VIDEO_ENDPOINT = 'https://ads.yieldmo.com/exchange/prebidvideo'; - let tdid = '8d146286-91d4-4958-aff4-7e489dd1abd6'; - let criteoId = 'aff4'; - - let bid = { + const mockBannerBid = (rootParams = {}, params = {}) => ({ bidder: 'yieldmo', params: { bidFloor: 0.1, + ...params, }, adUnitCode: 'adunit-code', mediaTypes: { @@ -31,15 +28,44 @@ describe('YieldmoAdapter', function () { pubcid: 'c604130c-0144-4b63-9bf2-c2bd8c8d86da', }, userId: { - tdid, + tdid: '8d146286-91d4-4958-aff4-7e489dd1abd6' + }, + ...rootParams + }); + + const mockVideoBid = (rootParams = {}, params = {}, videoParams = {}) => ({ + bidder: 'yieldmo', + adUnitCode: 'adunit-code-video', + bidId: '321video123', + mediaTypes: { + video: { + playerSize: [640, 480], + context: 'instream', + mimes: ['video/mp4'] + }, }, - }; - let bidArray = [bid]; - let bidderRequest = { + params: { + placementId: '123', + ...params, + video: { + placement: 1, + maxduration: 30, + startdelay: 10, + protocols: [2, 3], + api: [2, 3], + skipppable: true, + playbackmethod: [1, 2], + ...videoParams + } + }, + ...rootParams + }); + + const mockBidderRequest = (params = {}, bids = [mockBannerBid()]) => ({ bidderCode: 'yieldmo', auctionId: 'e3a336ad-2761-4a1c-b421-ecc7c5294a34', bidderRequestId: '14c4ede8c693f', - bids: bidArray, + bids, auctionStart: 1520001292880, timeout: 3000, start: 1520001292884, @@ -49,236 +75,215 @@ describe('YieldmoAdapter', function () { reachedTop: true, referer: 'yieldmo.com', }, - }; + ...params + }); describe('isBidRequestValid', function () { - it('should return true when necessary information is found', function () { - expect(spec.isBidRequestValid(bid)).to.be.true; + describe('Banner:', function () { + it('should return true when necessary information is found', function () { + expect(spec.isBidRequestValid(mockBannerBid())).to.be.true; + }); + + it('should return false when necessary information is not found', function () { + // empty bid + expect(spec.isBidRequestValid({})).to.be.false; + + // empty bidId + expect(spec.isBidRequestValid(mockBannerBid({bidId: ''}))).to.be.false; + + // empty adUnitCode + expect(spec.isBidRequestValid(mockBannerBid({adUnitCode: ''}))).to.be.false; + + let invalidBid = mockBannerBid(); + delete invalidBid.mediaTypes.banner; + expect(spec.isBidRequestValid(invalidBid)).to.be.false; + }); }); - it('should return false when necessary information is not found', function () { - // empty bid - expect(spec.isBidRequestValid({})).to.be.false; + describe('Instream video:', function () { + const getVideoBidWithoutParam = (key, paramToRemove) => { + let bid = mockVideoBid(); + delete utils.deepAccess(bid, key)[paramToRemove]; + return bid; + } + + it('should return true when necessary information is found', function () { + expect(spec.isBidRequestValid(mockVideoBid())).to.be.true; + }); + + it('should return false when necessary information is not found', function () { + // empty bidId + expect(spec.isBidRequestValid(mockVideoBid({bidId: ''}))).to.be.false; - // empty bidId - bid.bidId = ''; - expect(spec.isBidRequestValid(bid)).to.be.false; + // empty adUnitCode + expect(spec.isBidRequestValid(mockVideoBid({adUnitCode: ''}))).to.be.false; + }); + + it('should return false when required mediaTypes.video.* param is not found', function () { + const getBidAndExclude = paramToRemove => getVideoBidWithoutParam('mediaTypes.video', paramToRemove); + + expect(spec.isBidRequestValid(getBidAndExclude('playerSize'))).to.be.false; + expect(spec.isBidRequestValid(getBidAndExclude('mimes'))).to.be.false; + }); + + it('should return false when required bid.params.* is not found', function () { + const getBidAndExclude = paramToRemove => getVideoBidWithoutParam('params', paramToRemove); + + expect(spec.isBidRequestValid(getBidAndExclude('placementId'))).to.be.false; + expect(spec.isBidRequestValid(getBidAndExclude('video'))).to.be.false; + }); - // empty adUnitCode - bid.bidId = '30b31c1838de1e'; - bid.adUnitCode = ''; - expect(spec.isBidRequestValid(bid)).to.be.false; + it('should return false when required bid.params.video.* is not found', function () { + const getBidAndExclude = paramToRemove => getVideoBidWithoutParam('params.video', paramToRemove); - bid.adUnitCode = 'adunit-code'; + expect(spec.isBidRequestValid(getBidAndExclude('placement'))).to.be.false; + expect(spec.isBidRequestValid(getBidAndExclude('maxduration'))).to.be.false; + expect(spec.isBidRequestValid(getBidAndExclude('startdelay'))).to.be.false; + expect(spec.isBidRequestValid(getBidAndExclude('protocols'))).to.be.false; + expect(spec.isBidRequestValid(getBidAndExclude('api'))).to.be.false; + }); }); }); describe('buildRequests', function () { - it('should attempt to send bid requests to the endpoint via GET', function () { - const request = spec.buildRequests(bidArray, bidderRequest); - expect(request.method).to.equal('GET'); - expect(request.url).to.be.equal(ENDPOINT); - }); + const build = (bidRequests, bidderReq = mockBidderRequest()) => spec.buildRequests(bidRequests, bidderReq); + const buildAndGetPlacementInfo = (bidRequests, index = 0, bidderReq = mockBidderRequest()) => + utils.deepAccess(build(bidRequests, bidderReq), `${index}.data.p`); + const buildAndGetData = (bidRequests, index = 0, bidderReq = mockBidderRequest()) => + utils.deepAccess(build(bidRequests, bidderReq), `${index}.data`) || {}; - it('should not blow up if crumbs is undefined', function () { - let bidArray = [{ ...bid, crumbs: undefined }]; - expect(function () { - spec.buildRequests(bidArray, bidderRequest); - }).not.to.throw(); - }); + describe('Banner:', function () { + it('should attempt to send banner bid requests to the endpoint via GET', function () { + const requests = build([mockBannerBid()]); + expect(requests.length).to.equal(1); + expect(requests[0].method).to.equal('GET'); + expect(requests[0].url).to.be.equal(BANNER_ENDPOINT); + }); - it('should place bid information into the p parameter of data', function () { - let placementInfo = spec.buildRequests(bidArray, bidderRequest).data.p; - expect(placementInfo).to.equal( - '[{"placement_id":"adunit-code","callback_id":"30b31c1838de1e","sizes":[[300,250],[300,600]],"bidFloor":0.1}]' - ); - bidArray.push({ - bidder: 'yieldmo', - params: { - bidFloor: 0.2, - }, - adUnitCode: 'adunit-code-1', - mediaTypes: { - banner: { - sizes: [ - [300, 250], - [300, 600], - ], - }, - }, - bidId: '123456789', - bidderRequestId: '987654321', - auctionId: '0246810', - crumbs: { - pubcid: 'c604130c-0144-4b63-9bf2-c2bd8c8d86da', - }, + it('should not blow up if crumbs is undefined', function () { + expect(function () { + build([mockBannerBid({crumbs: undefined})]); + }).not.to.throw(); }); - // multiple placements - placementInfo = spec.buildRequests(bidArray, bidderRequest).data.p; - expect(placementInfo).to.equal( - '[{"placement_id":"adunit-code","callback_id":"30b31c1838de1e","sizes":[[300,250],[300,600]],"bidFloor":0.1},{"placement_id":"adunit-code-1","callback_id":"123456789","sizes":[[300,250],[300,600]],"bidFloor":0.2}]' - ); - }); + it('should place bid information into the p parameter of data', function () { + let bidArray = [mockBannerBid()]; + expect(buildAndGetPlacementInfo(bidArray)).to.equal( + '[{"placement_id":"adunit-code","callback_id":"30b31c1838de1e","sizes":[[300,250],[300,600]],"bidFloor":0.1}]' + ); - it('should add placement id if given', function () { - bidArray[0].params.placementId = 'ym_1293871298'; - let placementInfo = spec.buildRequests(bidArray, bidderRequest).data.p; - expect(placementInfo).to.include('"ym_placement_id":"ym_1293871298"'); - expect(placementInfo).not.to.include('"ym_placement_id":"ym_0987654321"'); + // multiple placements + bidArray.push(mockBannerBid( + {adUnitCode: 'adunit-2', bidId: '123a', bidderRequestId: '321', auctionId: '222'}, {bidFloor: 0.2})); + expect(buildAndGetPlacementInfo(bidArray)).to.equal( + '[{"placement_id":"adunit-code","callback_id":"30b31c1838de1e","sizes":[[300,250],[300,600]],"bidFloor":0.1},' + + '{"placement_id":"adunit-2","callback_id":"123a","sizes":[[300,250],[300,600]],"bidFloor":0.2}]' + ); + }); - bidArray[1].params.placementId = 'ym_0987654321'; - placementInfo = spec.buildRequests(bidArray, bidderRequest).data.p; - expect(placementInfo).to.include('"ym_placement_id":"ym_1293871298"'); - expect(placementInfo).to.include('"ym_placement_id":"ym_0987654321"'); - }); + it('should add placement id if given', function () { + let bidArray = [mockBannerBid({}, {placementId: 'ym_1293871298'})]; + let placementInfo = buildAndGetPlacementInfo(bidArray); + expect(placementInfo).to.include('"ym_placement_id":"ym_1293871298"'); + expect(placementInfo).not.to.include('"ym_placement_id":"ym_0987654321"'); - it('should add additional information to data parameter of request', function () { - const data = spec.buildRequests(bidArray, bidderRequest).data; - expect(data.hasOwnProperty('page_url')).to.be.true; - expect(data.hasOwnProperty('bust')).to.be.true; - expect(data.hasOwnProperty('pr')).to.be.true; - expect(data.hasOwnProperty('scrd')).to.be.true; - expect(data.dnt).to.be.false; - expect(data.hasOwnProperty('description')).to.be.true; - expect(data.hasOwnProperty('title')).to.be.true; - expect(data.hasOwnProperty('h')).to.be.true; - expect(data.hasOwnProperty('w')).to.be.true; - expect(data.hasOwnProperty('pubcid')).to.be.true; - expect(data.userConsent).to.equal('{"gdprApplies":"","cmp":""}'); - expect(data.us_privacy).to.equal(''); - }); + bidArray.push(mockBannerBid({}, {placementId: 'ym_0987654321'})); + placementInfo = buildAndGetPlacementInfo(bidArray); + expect(placementInfo).to.include('"ym_placement_id":"ym_1293871298"'); + expect(placementInfo).to.include('"ym_placement_id":"ym_0987654321"'); + }); - it('should add pubcid as parameter of request', function () { - const pubcidBid = { - bidder: 'yieldmo', - params: {}, - adUnitCode: 'adunit-code', - mediaTypes: { - banner: { - sizes: [ - [300, 250], - [300, 600], - ], - }, - }, - bidId: '30b31c1838de1e', - bidderRequestId: '22edbae2733bf6', - auctionId: '1d1a030790a475', - userId: { - pubcid: 'c604130c-0144-4b63-9bf2-c2bd8c8d86da2', - }, - }; - const data = spec.buildRequests([pubcidBid], bidderRequest).data; - expect(data.pubcid).to.deep.equal( - 'c604130c-0144-4b63-9bf2-c2bd8c8d86da2' - ); - }); + it('should add additional information to data parameter of request', function () { + const data = buildAndGetData([mockBannerBid()]); + expect(data.hasOwnProperty('page_url')).to.be.true; + expect(data.hasOwnProperty('bust')).to.be.true; + expect(data.hasOwnProperty('pr')).to.be.true; + expect(data.hasOwnProperty('scrd')).to.be.true; + expect(data.dnt).to.be.false; + expect(data.hasOwnProperty('description')).to.be.true; + expect(data.hasOwnProperty('title')).to.be.true; + expect(data.hasOwnProperty('h')).to.be.true; + expect(data.hasOwnProperty('w')).to.be.true; + expect(data.hasOwnProperty('pubcid')).to.be.true; + expect(data.userConsent).to.equal('{"gdprApplies":"","cmp":""}'); + expect(data.us_privacy).to.equal(''); + }); - it('should add unified id as parameter of request', function () { - const unifiedIdBid = { - bidder: 'yieldmo', - params: {}, - adUnitCode: 'adunit-code', - mediaTypes: { - banner: { - sizes: [ - [300, 250], - [300, 600], - ], - }, - }, - bidId: '30b31c1838de1e', - bidderRequestId: '22edbae2733bf6', - auctionId: '1d1a030790a475', - userId: { - tdid, - }, - }; - const data = spec.buildRequests([unifiedIdBid], bidderRequest).data; - expect(data.tdid).to.deep.equal(tdid); - }); + it('should add pubcid as parameter of request', function () { + const pubcid = 'c604130c-0144-4b63-9bf2-c2bd8c8d86da2'; + const pubcidBid = mockBannerBid({crumbs: undefined, userId: {pubcid}}); + expect(buildAndGetData([pubcidBid]).pubcid).to.deep.equal(pubcid); + }); - it('should add CRITEO RTUS id as parameter of request', function () { - const criteoIdBid = { - bidder: 'yieldmo', - params: {}, - adUnitCode: 'adunit-code', - mediaTypes: { - banner: { - sizes: [ - [300, 250], - [300, 600], - ], - }, - }, - bidId: '30b31c1838de1e', - bidderRequestId: '22edbae2733bf6', - auctionId: '1d1a030790a475', - userId: { - criteoId, - }, - }; - const data = spec.buildRequests([criteoIdBid], bidderRequest).data; - expect(data.cri_prebid).to.deep.equal(criteoId); - }); + it('should add unified id as parameter of request', function () { + const unifiedIdBid = mockBannerBid({crumbs: undefined}); + expect(buildAndGetData([unifiedIdBid]).tdid).to.deep.equal(mockBannerBid().userId.tdid); + }); + + it('should add CRITEO RTUS id as parameter of request', function () { + const criteoId = 'aff4'; + const criteoIdBid = mockBannerBid({crumbs: undefined, userId: { criteoId }}); + expect(buildAndGetData([criteoIdBid]).cri_prebid).to.deep.equal(criteoId); + }); - it('should add gdpr information to request if available', () => { - bidderRequest.gdprConsent = { - consentString: 'BOJ/P2HOJ/P2HABABMAAAAAZ+A==', - vendorData: { blerp: 1 }, - gdprApplies: true, - }; - const data = spec.buildRequests(bidArray, bidderRequest).data; - expect(data.userConsent).equal( - JSON.stringify({ + it('should add gdpr information to request if available', () => { + const gdprConsent = { + consentString: 'BOJ/P2HOJ/P2HABABMAAAAAZ+A==', + vendorData: {blerp: 1}, gdprApplies: true, - cmp: 'BOJ/P2HOJ/P2HABABMAAAAAZ+A==', - }) - ); - }); + }; + const data = buildAndGetData([mockBannerBid()], 0, mockBidderRequest({gdprConsent})); + expect(data.userConsent).equal( + JSON.stringify({ + gdprApplies: true, + cmp: 'BOJ/P2HOJ/P2HABABMAAAAAZ+A==', + }) + ); + }); + + it('should add ccpa information to request if available', () => { + const uspConsent = '1YNY'; + const data = buildAndGetData([mockBannerBid()], 0, mockBidderRequest({uspConsent})); + expect(data.us_privacy).equal(uspConsent); + }); - it('should add ccpa information to request if available', () => { - const privacy = '1YNY'; - bidderRequest.uspConsent = privacy; - const data = spec.buildRequests(bidArray, bidderRequest).data; - expect(data.us_privacy).equal(privacy); + it('should add schain if it is in the bidRequest', () => { + const schain = { + ver: '1.0', + complete: 1, + nodes: [{asi: 'indirectseller.com', sid: '00001', hp: 1}], + }; + const data = buildAndGetData([mockBannerBid({schain})]); + expect(data.schain).equal(JSON.stringify(schain)); + }); }); - it('should add schain if it is in the bidRequest', () => { - const schain = { - ver: '1.0', - complete: 1, - nodes: [{ asi: 'indirectseller.com', sid: '00001', hp: 1 }], - }; - bidArray[0].schain = schain; - const request = spec.buildRequests([bidArray[0]], bidderRequest); - expect(request.data.schain).equal(JSON.stringify(schain)); + describe('Instream video:', function () { + it('should attempt to send banner bid requests to the endpoint via POST', function () { + const requests = build([mockVideoBid()]); + expect(requests.length).to.equal(1); + expect(requests[0].method).to.equal('POST'); + expect(requests[0].url).to.be.equal(VIDEO_ENDPOINT); + }); }); }); describe('interpretResponse', function () { - let serverResponse; - - beforeEach(function () { - serverResponse = { - body: [ - { - callback_id: '21989fdbef550a', - cpm: 3.45455, - width: 300, - height: 250, - ad: - '
', - creative_id: '9874652394875', - }, - ], - header: 'header?', - }; + const mockServerResponse = () => ({ + body: [{ + callback_id: '21989fdbef550a', + cpm: 3.45455, + width: 300, + height: 250, + ad: '' + + '
', + creative_id: '9874652394875', + }], + header: 'header?', }); it('should correctly reorder the server response', function () { - const newResponse = spec.interpretResponse(serverResponse); + const newResponse = spec.interpretResponse(mockServerResponse()); expect(newResponse.length).to.be.equal(1); expect(newResponse[0]).to.deep.equal({ requestId: '21989fdbef550a', @@ -289,19 +294,18 @@ describe('YieldmoAdapter', function () { currency: 'USD', netRevenue: true, ttl: 300, - ad: - '
', + ad: '' + + '
', }); }); it('should not add responses if the cpm is 0 or null', function () { - serverResponse.body[0].cpm = 0; - let response = spec.interpretResponse(serverResponse); - expect(response).to.deep.equal([]); + let response = mockServerResponse(); + response.body[0].cpm = 0; + expect(spec.interpretResponse(response)).to.deep.equal([]); - serverResponse.body[0].cpm = null; - response = spec.interpretResponse(serverResponse); - expect(response).to.deep.equal([]); + response.body[0].cpm = null; + expect(spec.interpretResponse(response)).to.deep.equal([]); }); }); diff --git a/test/spec/modules/yuktamediaAnalyticsAdapter_spec.js b/test/spec/modules/yuktamediaAnalyticsAdapter_spec.js index c8643c547e0..e8eb4ab73be 100644 --- a/test/spec/modules/yuktamediaAnalyticsAdapter_spec.js +++ b/test/spec/modules/yuktamediaAnalyticsAdapter_spec.js @@ -30,7 +30,7 @@ let prebidAuction = { } }, 'userId': { - 'id5id': 'ID5-ZHMOxXeRXPe3inZKGD-Lj0g7y8UWdDbsYXQ_n6aWMQ', + 'id5id': { uid: 'ID5-ZHMOxXeRXPe3inZKGD-Lj0g7y8UWdDbsYXQ_n6aWMQ' }, 'parrableid': '01.1595590997.46d951017bdc272ca50b88dbcfb0545cfc636bec3e3d8c02091fb1b413328fb2fd3baf65cb4114b3f782895fd09f82f02c9042b85b42c4654d08ba06dc77f0ded936c8ea3fc4085b4a99', 'pubcid': '100a8bc9-f588-4c22-873e-a721cb68bc34' }, diff --git a/test/spec/modules/zemantaBidAdapter_spec.js b/test/spec/modules/zemantaBidAdapter_spec.js new file mode 100644 index 00000000000..ab685890620 --- /dev/null +++ b/test/spec/modules/zemantaBidAdapter_spec.js @@ -0,0 +1,487 @@ +import {expect} from 'chai'; +import {spec} from 'modules/zemantaBidAdapter.js'; +import {config} from 'src/config.js'; +import {server} from 'test/mocks/xhr'; + +describe('Zemanta Adapter', function () { + describe('Bid request and response', function () { + const commonBidRequest = { + bidder: 'zemanta', + params: { + publisher: { + id: 'publisher-id' + }, + }, + bidId: '2d6815a92ba1ba', + auctionId: '12043683-3254-4f74-8934-f941b085579e', + } + const nativeBidRequestParams = { + nativeParams: { + image: { + required: true, + sizes: [ + 120, + 100 + ], + sendId: true + }, + title: { + required: true, + sendId: true + }, + sponsoredBy: { + required: false + } + }, + } + + const displayBidRequestParams = { + sizes: [ + [ + 300, + 250 + ] + ] + } + + describe('isBidRequestValid', function () { + before(() => { + config.setConfig({ + zemanta: { + bidderUrl: 'https://bidder-url.com', + } + } + ) + }) + after(() => { + config.resetConfig() + }) + + it('should fail when bid is invalid', function () { + const bid = { + bidder: 'zemanta', + params: { + publisher: { + id: 'publisher-id', + } + }, + } + expect(spec.isBidRequestValid(bid)).to.equal(false) + }) + it('should succeed when bid contains native params', function () { + const bid = { + bidder: 'zemanta', + params: { + publisher: { + id: 'publisher-id', + } + }, + ...nativeBidRequestParams, + } + expect(spec.isBidRequestValid(bid)).to.equal(true) + }) + it('should succeed when bid contains sizes', function () { + const bid = { + bidder: 'zemanta', + params: { + publisher: { + id: 'publisher-id', + } + }, + ...displayBidRequestParams, + } + expect(spec.isBidRequestValid(bid)).to.equal(true) + }) + it('should fail if publisher id is not set', function () { + const bid = { + bidder: 'zemanta', + ...nativeBidRequestParams, + } + expect(spec.isBidRequestValid(bid)).to.equal(false) + }) + it('should fail if bidder url is not set', function () { + const bid = { + bidder: 'zemanta', + params: { + publisher: { + id: 'publisher-id', + } + }, + ...nativeBidRequestParams, + } + config.resetConfig() + expect(spec.isBidRequestValid(bid)).to.equal(false) + }) + }) + + describe('buildRequests', function () { + before(() => { + config.setConfig({ + zemanta: { + bidderUrl: 'https://bidder-url.com', + } + } + ) + }) + after(() => { + config.resetConfig() + }) + + const commonBidderRequest = { + refererInfo: { + referer: 'https://example.com/' + } + } + + it('should build native request', function () { + const bidRequest = { + ...commonBidRequest, + ...nativeBidRequestParams, + } + const expectedNativeAssets = { + assets: [ + { + required: 1, + id: 3, + img: { + type: 3, + w: 120, + h: 100 + } + }, + { + required: 1, + id: 0, + title: {} + }, + { + required: 0, + id: 5, + data: { + type: 1 + } + } + ] + } + const expectedData = { + site: { + page: 'https://example.com/', + publisher: { + id: 'publisher-id' + } + }, + device: { + ua: navigator.userAgent + }, + source: { + fd: 1 + }, + cur: [ + 'USD' + ], + imp: [ + { + id: '1', + native: { + request: JSON.stringify(expectedNativeAssets) + } + } + ] + } + const res = spec.buildRequests([bidRequest], commonBidderRequest) + expect(res.url).to.equal('https://bidder-url.com') + expect(res.data).to.deep.equal(JSON.stringify(expectedData)) + }); + + it('should build display request', function () { + const bidRequest = { + ...commonBidRequest, + ...displayBidRequestParams, + } + const expectedData = { + site: { + page: 'https://example.com/', + publisher: { + id: 'publisher-id' + } + }, + device: { + ua: navigator.userAgent + }, + source: { + fd: 1 + }, + cur: [ + 'USD' + ], + imp: [ + { + id: '1', + banner: { + format: [ + { + w: 300, + h: 250 + } + ] + } + } + ] + } + const res = spec.buildRequests([bidRequest], commonBidderRequest) + expect(res.url).to.equal('https://bidder-url.com') + expect(res.data).to.deep.equal(JSON.stringify(expectedData)) + }) + + it('should pass optional tagid in request', function () { + const bidRequest = { + ...commonBidRequest, + ...nativeBidRequestParams, + } + bidRequest.params.tagid = 'test-tag' + + const res = spec.buildRequests([bidRequest], commonBidderRequest) + const resData = JSON.parse(res.data) + expect(resData.imp[0].tagid).to.equal('test-tag') + }); + + it('should pass bidder timeout', function () { + const bidRequest = { + ...commonBidRequest, + ...nativeBidRequestParams, + } + const bidderRequest = { + ...commonBidderRequest, + timeout: 500 + } + const res = spec.buildRequests([bidRequest], bidderRequest) + const resData = JSON.parse(res.data) + expect(resData.tmax).to.equal(500) + }); + + it('should pass GDPR consent', function () { + const bidRequest = { + ...commonBidRequest, + ...nativeBidRequestParams, + } + const bidderRequest = { + ...commonBidderRequest, + gdprConsent: { + gdprApplies: true, + consentString: 'consentString', + } + } + const res = spec.buildRequests([bidRequest], bidderRequest) + const resData = JSON.parse(res.data) + expect(resData.user.ext.consent).to.equal('consentString') + expect(resData.regs.ext.gdpr).to.equal(1) + }); + + it('should pass us privacy consent', function () { + const bidRequest = { + ...commonBidRequest, + ...nativeBidRequestParams, + } + const bidderRequest = { + ...commonBidderRequest, + uspConsent: 'consentString' + } + const res = spec.buildRequests([bidRequest], bidderRequest) + const resData = JSON.parse(res.data) + expect(resData.regs.ext.us_privacy).to.equal('consentString') + }); + + it('should pass coppa consent', function () { + const bidRequest = { + ...commonBidRequest, + ...nativeBidRequestParams, + } + config.setConfig({coppa: true}) + + const res = spec.buildRequests([bidRequest], commonBidderRequest) + const resData = JSON.parse(res.data) + expect(resData.regs.coppa).to.equal(1) + + config.resetConfig() + }); + }) + + describe('interpretResponse', function () { + it('should return empty array if no valid bids', function () { + const res = spec.interpretResponse({}, []) + expect(res).to.be.an('array').that.is.empty + }); + + it('should interpret native response', function () { + const serverResponse = { + body: { + id: '0a73e68c-9967-4391-b01b-dda2d9fc54e4', + seatbid: [ + { + bid: [ + { + id: '82822cf5-259c-11eb-8a52-f29e5275aa57', + impid: '1', + price: 1.1, + nurl: 'http://example.com/win/${AUCTION_PRICE}', + adm: '{"ver":"1.2","assets":[{"id":3,"required":1,"img":{"url":"http://example.com/img/url","w":120,"h":100}},{"id":0,"required":1,"title":{"text":"Test title"}},{"id":5,"data":{"value":"Test sponsor"}}],"link":{"url":"http://example.com/click/url"},"eventtrackers":[{"event":1,"method":1,"url":"http://example.com/impression"}]}', + adomain: [ + 'example.co' + ], + cid: '3487171', + crid: '28023739', + cat: [ + 'IAB10-2' + ] + } + ], + seat: 'acc-5537' + } + ], + bidid: '82822cf5-259c-11eb-8a52-b48e7518c657', + cur: 'USD' + }, + } + const request = { + bids: [ + { + ...commonBidRequest, + ...nativeBidRequestParams, + } + ] + } + const expectedRes = [ + { + requestId: request.bids[0].bidId, + cpm: 1.1, + creativeId: '28023739', + ttl: 360, + netRevenue: false, + currency: 'USD', + mediaType: 'native', + nurl: 'http://example.com/win/${AUCTION_PRICE}', + native: { + clickTrackers: undefined, + clickUrl: 'http://example.com/click/url', + image: { + url: 'http://example.com/img/url', + width: 120, + height: 100 + }, + title: 'Test title', + sponsoredBy: 'Test sponsor', + impressionTrackers: [ + 'http://example.com/impression', + ] + } + } + ] + + const res = spec.interpretResponse(serverResponse, request) + expect(res).to.deep.equal(expectedRes) + }); + + it('should interpret display response', function () { + const serverResponse = { + body: { + id: '6b2eedc8-8ff5-46ef-adcf-e701b508943e', + seatbid: [ + { + bid: [ + { + id: 'd90fe7fa-28d7-11eb-8ce4-462a842a7cf9', + impid: '1', + price: 1.1, + nurl: 'http://example.com/win/${AUCTION_PRICE}', + adm: '
ad
', + adomain: [ + 'example.com' + ], + cid: '3865084', + crid: '29998660', + cat: [ + 'IAB10-2' + ], + w: 300, + h: 250 + } + ], + seat: 'acc-6536' + } + ], + bidid: 'd90fe7fa-28d7-11eb-8ce4-13d94bfa26f9', + cur: 'USD' + } + } + const request = { + bids: [ + { + ...commonBidRequest, + ...displayBidRequestParams + } + ] + } + const expectedRes = [ + { + requestId: request.bids[0].bidId, + cpm: 1.1, + creativeId: '29998660', + ttl: 360, + netRevenue: false, + currency: 'USD', + mediaType: 'banner', + nurl: 'http://example.com/win/${AUCTION_PRICE}', + ad: '
ad
', + width: 300, + height: 250 + } + ] + + const res = spec.interpretResponse(serverResponse, request) + expect(res).to.deep.equal(expectedRes) + }); + }) + }) + + describe('getUserSyncs', function () { + before(() => { + config.setConfig({ + zemanta: { + usersyncUrl: 'https://usersync-url.com', + } + } + ) + }) + after(() => { + config.resetConfig() + }) + + it('should return user sync if pixel enabled', function () { + const ret = spec.getUserSyncs({pixelEnabled: true}) + expect(ret).to.deep.equal([{type: 'image', url: 'https://usersync-url.com'}]) + }) + + it('should not return user sync if pixel disabled', function () { + const ret = spec.getUserSyncs({pixelEnabled: false}) + expect(ret).to.be.an('array').that.is.empty + }) + + it('should not return user sync if url is not set', function () { + config.resetConfig() + const ret = spec.getUserSyncs({pixelEnabled: true}) + expect(ret).to.be.an('array').that.is.empty + }) + }) + + describe('onBidWon', function () { + it('should make an ajax call with the original cpm', function () { + const bid = { + nurl: 'http://example.com/win/${AUCTION_PRICE}', + cpm: 2.1, + originalCpm: 1.1, + } + spec.onBidWon(bid) + expect(server.requests[0].url).to.equals('http://example.com/win/1.1') + }); + }) +}) diff --git a/test/spec/modules/zeotapIdPlusIdSystem_spec.js b/test/spec/modules/zeotapIdPlusIdSystem_spec.js index 54082618120..4f9e691f12e 100644 --- a/test/spec/modules/zeotapIdPlusIdSystem_spec.js +++ b/test/spec/modules/zeotapIdPlusIdSystem_spec.js @@ -34,26 +34,23 @@ function getAdUnitMock(code = 'adUnit-code') { }; } -describe('Zeotap ID System', function() { - let getDataFromLocalStorageStub, localStorageIsEnabledStub; - let getCookieStub, cookiesAreEnabledStub; - beforeEach(function () { - getDataFromLocalStorageStub = sinon.stub(storage, 'getDataFromLocalStorage'); - localStorageIsEnabledStub = sinon.stub(storage, 'localStorageIsEnabled'); - getCookieStub = sinon.stub(storage, 'getCookie'); - cookiesAreEnabledStub = sinon.stub(storage, 'cookiesAreEnabled'); - }); +function unsetCookie() { + storage.setCookie(ZEOTAP_COOKIE_NAME, ''); +} - afterEach(function () { - getDataFromLocalStorageStub.restore(); - localStorageIsEnabledStub.restore(); - getCookieStub.restore(); - cookiesAreEnabledStub.restore(); - }); +function unsetLocalStorage() { + storage.setDataInLocalStorage(ZEOTAP_COOKIE_NAME, ''); +} +describe('Zeotap ID System', function() { describe('test method: getId', function() { + afterEach(() => { + unsetCookie(); + unsetLocalStorage(); + }); + it('provides the stored Zeotap id if a cookie exists', function() { - getCookieStub.withArgs(ZEOTAP_COOKIE_NAME).returns(ENCODED_ZEOTAP_COOKIE); + storage.setCookie(ZEOTAP_COOKIE_NAME, ENCODED_ZEOTAP_COOKIE); let id = zeotapIdPlusSubmodule.getId(); expect(id).to.deep.equal({ id: ENCODED_ZEOTAP_COOKIE @@ -61,7 +58,7 @@ describe('Zeotap ID System', function() { }); it('provides the stored Zeotap id if cookie is absent but present in local storage', function() { - getDataFromLocalStorageStub.withArgs(ZEOTAP_COOKIE_NAME).returns(ENCODED_ZEOTAP_COOKIE); + storage.setDataInLocalStorage(ZEOTAP_COOKIE_NAME, ENCODED_ZEOTAP_COOKIE); let id = zeotapIdPlusSubmodule.getId(); expect(id).to.deep.equal({ id: ENCODED_ZEOTAP_COOKIE @@ -99,10 +96,18 @@ describe('Zeotap ID System', function() { beforeEach(function() { adUnits = [getAdUnitMock()]; + storage.setCookie( + ZEOTAP_COOKIE_NAME, + ENCODED_ZEOTAP_COOKIE + ); setSubmoduleRegistry([zeotapIdPlusSubmodule]); init(config); config.setConfig(getConfigMock()); - getCookieStub.withArgs(ZEOTAP_COOKIE_NAME).returns(ENCODED_ZEOTAP_COOKIE); + }); + + afterEach(function() { + unsetCookie(); + unsetLocalStorage(); }); it('when a stored Zeotap ID exists it is added to bids', function(done) { diff --git a/test/spec/modules/zetaBidAdapter_spec.js b/test/spec/modules/zetaBidAdapter_spec.js new file mode 100644 index 00000000000..0d11614c926 --- /dev/null +++ b/test/spec/modules/zetaBidAdapter_spec.js @@ -0,0 +1,79 @@ +import { spec } from '../../../modules/zetaBidAdapter.js' + +describe('Zeta Bid Adapter', function() { + const bannerRequest = [{ + bidId: 12345, + auctionId: 67890, + mediaTypes: { + banner: { + sizes: [[300, 250]], + } + }, + refererInfo: { + referer: 'zetaglobal.com' + }, + params: { + placement: 12345, + user: { + uid: 12345, + buyeruid: 12345 + }, + ip: '111.222.33.44', + definerId: 1, + test: 1 + } + }]; + + it('Test the bid validation function', function() { + const validBid = spec.isBidRequestValid(bannerRequest[0]); + const invalidBid = spec.isBidRequestValid(null); + + expect(validBid).to.be.true; + expect(invalidBid).to.be.false; + }); + + it('Test the request processing function', function () { + const request = spec.buildRequests(bannerRequest, bannerRequest[0]); + expect(request).to.not.be.empty; + + const payload = request.data; + expect(payload).to.not.be.empty; + }); + + const responseBody = { + id: '12345', + seatbid: [ + { + bid: [ + { + id: 'auctionId', + impid: 'impId', + price: 0.0, + adm: 'adMarkup', + crid: 'creativeId', + h: 250, + w: 300 + } + ] + } + ], + cur: 'USD' + }; + + it('Test the response parsing function', function () { + const receivedBid = responseBody.seatbid[0].bid[0]; + const response = {}; + response.body = responseBody; + + const bidResponse = spec.interpretResponse(response, null); + expect(bidResponse).to.not.be.empty; + + const bid = bidResponse[0]; + expect(bid).to.not.be.empty; + expect(bid.ad).to.equal(receivedBid.adm); + expect(bid.cpm).to.equal(receivedBid.price); + expect(bid.height).to.equal(receivedBid.h); + expect(bid.width).to.equal(receivedBid.w); + expect(bid.requestId).to.equal(receivedBid.impid); + }); +}); diff --git a/test/spec/refererDetection_spec.js b/test/spec/refererDetection_spec.js index 90892d915fe..46990ae841f 100644 --- a/test/spec/refererDetection_spec.js +++ b/test/spec/refererDetection_spec.js @@ -1,87 +1,357 @@ import { detectReferer } from 'src/refererDetection.js'; import { expect } from 'chai'; -var mocks = { - createFakeWindow: function (referrer, href) { - return { - document: { - referrer: referrer - }, - location: { - href: href, - // TODO: add ancestorOrigins to increase test coverage - }, - parent: null, - top: null - }; +/** + * Build a walkable linked list of window-like objects for testing. + * + * @param {Array} urls Array of URL strings starting from the top window. + * @param {string} [topReferrer] + * @param {string} [canonicalUrl] + * @param {boolean} [ancestorOrigins] + * @returns {Object} + */ +function buildWindowTree(urls, topReferrer = '', canonicalUrl = null, ancestorOrigins = false) { + /** + * Find the origin from a given fully-qualified URL. + * + * @param {string} url The fully qualified URL + * @returns {string|null} + */ + function getOrigin(url) { + const originRegex = new RegExp('^(https?://[^/]+/?)'); + + const result = originRegex.exec(url); + + if (result && result[0]) { + return result[0]; + } + + return null; } -} -describe('referer detection', () => { - it('should return referer details in nested friendly iframes', function() { - // Fake window object to test friendly iframes - // - Main page http://example.com/page.html - // - - Iframe1 http://example.com/iframe1.html - // - - - Iframe2 http://example.com/iframe2.html - let mockIframe2WinObject = mocks.createFakeWindow('http://example.com/iframe1.html', 'http://example.com/iframe2.html'); - let mockIframe1WinObject = mocks.createFakeWindow('http://example.com/page.html', 'http://example.com/iframe1.html'); - let mainWinObject = mocks.createFakeWindow('http://example.com/page.html', 'http://example.com/page.html'); - mainWinObject.document.querySelector = function() { - return { - href: 'http://prebid.org' + let previousWindow; + const myOrigin = getOrigin(urls[urls.length - 1]); + + const windowList = urls.map((url, index) => { + const theirOrigin = getOrigin(url), + sameOrigin = (myOrigin === theirOrigin); + + const win = {}; + + if (sameOrigin) { + win.location = { + href: url + }; + + if (ancestorOrigins) { + win.location.ancestorOrigins = urls.slice(0, index).reverse().map(getOrigin); + } + + if (index === 0) { + win.document = { + referrer: topReferrer + }; + + if (canonicalUrl) { + win.document.querySelector = function(selector) { + if (selector === "link[rel='canonical']") { + return { + href: canonicalUrl + }; + } + + return null; + }; + } + } else { + win.document = { + referrer: urls[index - 1] + }; } } - mockIframe2WinObject.parent = mockIframe1WinObject; - mockIframe2WinObject.top = mainWinObject; - mockIframe1WinObject.parent = mainWinObject; - mockIframe1WinObject.top = mainWinObject; - mainWinObject.top = mainWinObject; - - const getRefererInfo = detectReferer(mockIframe2WinObject); - let result = getRefererInfo(); - let expectedResult = { - referer: 'http://example.com/page.html', - reachedTop: true, - numIframes: 2, - stack: [ - 'http://example.com/page.html', - 'http://example.com/iframe1.html', - 'http://example.com/iframe2.html' - ], - canonicalUrl: 'http://prebid.org' - }; - expect(result).to.deep.equal(expectedResult); + + previousWindow = win; + + return win; + }); + + const topWindow = windowList[0]; + + previousWindow = null; + + windowList.forEach((win) => { + win.top = topWindow; + win.parent = previousWindow || topWindow; + previousWindow = win; + }); + + return windowList[windowList.length - 1]; +} + +describe('Referer detection', () => { + describe('Non cross-origin scenarios', () => { + describe('No iframes', () => { + it('Should return the current window location and no canonical URL', () => { + const testWindow = buildWindowTree(['https://example.com/some/page'], 'https://othersite.com/'), + result = detectReferer(testWindow)(); + + expect(result).to.deep.equal({ + referer: 'https://example.com/some/page', + reachedTop: true, + isAmp: false, + numIframes: 0, + stack: ['https://example.com/some/page'], + canonicalUrl: null + }); + }); + + it('Should return the current window location and a canonical URL', () => { + const testWindow = buildWindowTree(['https://example.com/some/page'], 'https://othersite.com/', 'https://example.com/canonical/page'), + result = detectReferer(testWindow)(); + + expect(result).to.deep.equal({ + referer: 'https://example.com/some/page', + reachedTop: true, + isAmp: false, + numIframes: 0, + stack: ['https://example.com/some/page'], + canonicalUrl: 'https://example.com/canonical/page' + }); + }); + }); + + describe('Friendly iframes', () => { + it('Should return the top window location and no canonical URL', () => { + const testWindow = buildWindowTree(['https://example.com/some/page', 'https://example.com/other/page', 'https://example.com/third/page'], 'https://othersite.com/'), + result = detectReferer(testWindow)(); + + expect(result).to.deep.equal({ + referer: 'https://example.com/some/page', + reachedTop: true, + isAmp: false, + numIframes: 2, + stack: [ + 'https://example.com/some/page', + 'https://example.com/other/page', + 'https://example.com/third/page' + ], + canonicalUrl: null + }); + }); + + it('Should return the top window location and a canonical URL', () => { + const testWindow = buildWindowTree(['https://example.com/some/page', 'https://example.com/other/page', 'https://example.com/third/page'], 'https://othersite.com/', 'https://example.com/canonical/page'), + result = detectReferer(testWindow)(); + + expect(result).to.deep.equal({ + referer: 'https://example.com/some/page', + reachedTop: true, + isAmp: false, + numIframes: 2, + stack: [ + 'https://example.com/some/page', + 'https://example.com/other/page', + 'https://example.com/third/page' + ], + canonicalUrl: 'https://example.com/canonical/page' + }); + }); + }); }); - it('should return referer details in nested cross domain iframes', function() { - // Fake window object to test cross domain iframes. - // - Main page http://example.com/page.html - // - - Iframe1 http://aaa.com/iframe1.html - // - - - Iframe2 http://bbb.com/iframe2.html - let mockIframe2WinObject = mocks.createFakeWindow('http://aaa.com/iframe1.html', 'http://bbb.com/iframe2.html'); - // Sinon cannot throw exception when accessing a propery so passing null to create cross domain - // environment for refererDetection module - let mockIframe1WinObject = mocks.createFakeWindow(null, null); - let mainWinObject = mocks.createFakeWindow(null, null); - mockIframe2WinObject.parent = mockIframe1WinObject; - mockIframe2WinObject.top = mainWinObject; - mockIframe1WinObject.parent = mainWinObject; - mockIframe1WinObject.top = mainWinObject; - mainWinObject.top = mainWinObject; - - const getRefererInfo = detectReferer(mockIframe2WinObject); - let result = getRefererInfo(); - let expectedResult = { - referer: 'http://aaa.com/iframe1.html', - reachedTop: false, - numIframes: 2, - stack: [ - null, - 'http://aaa.com/iframe1.html', - 'http://bbb.com/iframe2.html' - ], - canonicalUrl: undefined - }; - expect(result).to.deep.equal(expectedResult); + describe('Cross-origin scenarios', () => { + it('Should return the top URL and no canonical URL with one cross-origin iframe', () => { + const testWindow = buildWindowTree(['https://example.com/some/page', 'https://safe.frame/ad'], 'https://othersite.com/', 'https://canonical.example.com/'), + result = detectReferer(testWindow)(); + + expect(result).to.deep.equal({ + referer: 'https://example.com/some/page', + reachedTop: true, + isAmp: false, + numIframes: 1, + stack: [ + 'https://example.com/some/page', + 'https://safe.frame/ad' + ], + canonicalUrl: null + }); + }); + + it('Should return the top URL and no canonical URL with one cross-origin iframe and one friendly iframe', () => { + const testWindow = buildWindowTree(['https://example.com/some/page', 'https://safe.frame/ad', 'https://safe.frame/ad'], 'https://othersite.com/', 'https://canonical.example.com/'), + result = detectReferer(testWindow)(); + + expect(result).to.deep.equal({ + referer: 'https://example.com/some/page', + reachedTop: true, + isAmp: false, + numIframes: 2, + stack: [ + 'https://example.com/some/page', + 'https://safe.frame/ad', + 'https://safe.frame/ad' + ], + canonicalUrl: null + }); + }); + + it('Should return the second iframe location with three cross-origin windows and no ancessorOrigins', () => { + const testWindow = buildWindowTree(['https://example.com/some/page', 'https://safe.frame/ad', 'https://otherfr.ame/ad'], 'https://othersite.com/', 'https://canonical.example.com/'), + result = detectReferer(testWindow)(); + + expect(result).to.deep.equal({ + referer: 'https://safe.frame/ad', + reachedTop: false, + isAmp: false, + numIframes: 2, + stack: [ + null, + 'https://safe.frame/ad', + 'https://otherfr.ame/ad' + ], + canonicalUrl: null + }); + }); + + it('Should return the top window origin with three cross-origin windows with ancessorOrigins', () => { + const testWindow = buildWindowTree(['https://example.com/some/page', 'https://safe.frame/ad', 'https://otherfr.ame/ad'], 'https://othersite.com/', 'https://canonical.example.com/', true), + result = detectReferer(testWindow)(); + + expect(result).to.deep.equal({ + referer: 'https://example.com/', + reachedTop: false, + isAmp: false, + numIframes: 2, + stack: [ + 'https://example.com/', + 'https://safe.frame/ad', + 'https://otherfr.ame/ad' + ], + canonicalUrl: null + }); + }); + }); + + describe('Cross-origin AMP page scenarios', () => { + it('Should return the AMP page source and canonical URLs in an amp-ad iframe for a non-cached AMP page', () => { + const testWindow = buildWindowTree(['https://example.com/some/page/amp/', 'https://ad-iframe.ampproject.org/ad']); + + testWindow.context = { + sourceUrl: 'https://example.com/some/page/amp/', + canonicalUrl: 'https://example.com/some/page/' + }; + + const result = detectReferer(testWindow)(); + + expect(result).to.deep.equal({ + referer: 'https://example.com/some/page/amp/', + reachedTop: true, + isAmp: true, + numIframes: 1, + stack: [ + 'https://example.com/some/page/amp/', + 'https://ad-iframe.ampproject.org/ad' + ], + canonicalUrl: 'https://example.com/some/page/' + }); + }); + + it('Should return the AMP page source and canonical URLs in an amp-ad iframe for a cached AMP page on top', () => { + const testWindow = buildWindowTree(['https://example-com.amp-cache.example.com/some/page/amp/', 'https://ad-iframe.ampproject.org/ad']); + + testWindow.context = { + sourceUrl: 'https://example.com/some/page/amp/', + canonicalUrl: 'https://example.com/some/page/' + }; + + const result = detectReferer(testWindow)(); + + expect(result).to.deep.equal({ + referer: 'https://example.com/some/page/amp/', + reachedTop: true, + isAmp: true, + numIframes: 1, + stack: [ + 'https://example.com/some/page/amp/', + 'https://ad-iframe.ampproject.org/ad' + ], + canonicalUrl: 'https://example.com/some/page/' + }); + }); + + describe('Cached AMP page in iframed search result', () => { + it('Should return the AMP source and canonical URLs but with a null top-level stack location Without ancesorOrigins', () => { + const testWindow = buildWindowTree(['https://google.com/amp/example-com/some/page/amp/', 'https://example-com.amp-cache.example.com/some/page/amp/', 'https://ad-iframe.ampproject.org/ad']); + + testWindow.context = { + sourceUrl: 'https://example.com/some/page/amp/', + canonicalUrl: 'https://example.com/some/page/' + }; + + const result = detectReferer(testWindow)(); + + expect(result).to.deep.equal({ + referer: 'https://example.com/some/page/amp/', + reachedTop: false, + isAmp: true, + numIframes: 2, + stack: [ + null, + 'https://example.com/some/page/amp/', + 'https://ad-iframe.ampproject.org/ad' + ], + canonicalUrl: 'https://example.com/some/page/' + }); + }); + + it('Should return the AMP source and canonical URLs and include the top window origin in the stack with ancesorOrigins', () => { + const testWindow = buildWindowTree(['https://google.com/amp/example-com/some/page/amp/', 'https://example-com.amp-cache.example.com/some/page/amp/', 'https://ad-iframe.ampproject.org/ad'], null, null, true); + + testWindow.context = { + sourceUrl: 'https://example.com/some/page/amp/', + canonicalUrl: 'https://example.com/some/page/' + }; + + const result = detectReferer(testWindow)(); + + expect(result).to.deep.equal({ + referer: 'https://example.com/some/page/amp/', + reachedTop: false, + isAmp: true, + numIframes: 2, + stack: [ + 'https://google.com/', + 'https://example.com/some/page/amp/', + 'https://ad-iframe.ampproject.org/ad' + ], + canonicalUrl: 'https://example.com/some/page/' + }); + }); + + it('Should return the AMP source and canonical URLs and include the top window origin in the stack with ancesorOrigins and a friendly iframe under the amp-ad iframe', () => { + const testWindow = buildWindowTree(['https://google.com/amp/example-com/some/page/amp/', 'https://example-com.amp-cache.example.com/some/page/amp/', 'https://ad-iframe.ampproject.org/ad', 'https://ad-iframe.ampproject.org/ad'], null, null, true); + + testWindow.parent.context = { + sourceUrl: 'https://example.com/some/page/amp/', + canonicalUrl: 'https://example.com/some/page/' + }; + + const result = detectReferer(testWindow)(); + + expect(result).to.deep.equal({ + referer: 'https://example.com/some/page/amp/', + reachedTop: false, + isAmp: true, + numIframes: 3, + stack: [ + 'https://google.com/', + 'https://example.com/some/page/amp/', + 'https://ad-iframe.ampproject.org/ad', + 'https://ad-iframe.ampproject.org/ad' + ], + canonicalUrl: 'https://example.com/some/page/' + }); + }); + }); }); }); diff --git a/test/spec/renderer_spec.js b/test/spec/renderer_spec.js index 9bf551f35e8..dcca94396cd 100644 --- a/test/spec/renderer_spec.js +++ b/test/spec/renderer_spec.js @@ -155,6 +155,36 @@ describe('Renderer', function () { expect(loadExternalScript.called).to.be.true; }); + it('should load external script instead of publisher-defined one when backupOnly option is true in mediaTypes.video options', function() { + $$PREBID_GLOBAL$$.adUnits = [{ + code: 'video1', + mediaTypes: { + video: { + context: 'outstream', + mimes: ['video/mp4'], + playerSize: [[400, 300]], + renderer: { + url: 'https://acdn.adnxs.com/video/outstream/ANOutstreamVideo.js', + backupOnly: true, + render: sinon.spy() + }, + } + } + }] + + let testRenderer = Renderer.install({ + url: 'https://httpbin.org/post', + config: { test: 'config1' }, + id: 1, + adUnitCode: 'video1' + + }); + testRenderer.setRender(() => {}) + + testRenderer.render() + expect(loadExternalScript.called).to.be.true; + }); + it('should call loadExternalScript() for script not defined on adUnit, only when .render() is called', function() { $$PREBID_GLOBAL$$.adUnits = [{ code: 'video1', diff --git a/test/spec/unit/core/targeting_spec.js b/test/spec/unit/core/targeting_spec.js index 3aae6e3c33a..5d43ed48266 100644 --- a/test/spec/unit/core/targeting_spec.js +++ b/test/spec/unit/core/targeting_spec.js @@ -416,6 +416,46 @@ describe('targeting tests', function () { }); }); + describe('targetingControls.allowTargetingKeys', function () { + let bid4; + + beforeEach(function() { + bid4 = utils.deepClone(bid1); + bid4.adserverTargeting = { + hb_deal: '4321', + hb_pb: '0.1', + hb_adid: '567891011', + hb_bidder: 'appnexus', + }; + bid4.bidder = bid4.bidderCode = 'appnexus'; + bid4.cpm = 0.1; // losing bid so not included if enableSendAllBids === false + bid4.dealId = '4321'; + enableSendAllBids = true; + config.setConfig({ + targetingControls: { + allowTargetingKeys: ['BIDDER', 'AD_ID', 'PRICE_BUCKET'] + } + }); + bidsReceived.push(bid4); + }); + + it('targeting should include custom keys', function () { + const targeting = targetingInstance.getAllTargeting(['/123456/header-bid-tag-0']); + expect(targeting['/123456/header-bid-tag-0']).to.include.all.keys('foobar'); + }); + + it('targeting should include keys prefixed by allowed default targeting keys', function () { + const targeting = targetingInstance.getAllTargeting(['/123456/header-bid-tag-0']); + expect(targeting['/123456/header-bid-tag-0']).to.include.all.keys('hb_bidder_rubicon', 'hb_adid_rubicon', 'hb_pb_rubicon'); + expect(targeting['/123456/header-bid-tag-0']).to.include.all.keys('hb_bidder_appnexus', 'hb_adid_appnexus', 'hb_pb_appnexus'); + }); + + it('targeting should not include keys prefixed by disallowed default targeting keys', function () { + const targeting = targetingInstance.getAllTargeting(['/123456/header-bid-tag-0']); + expect(targeting['/123456/header-bid-tag-0']).to.not.have.all.keys(['hb_deal_appnexus', 'hb_deal_rubicon']); + }); + }); + describe('targetingControls.alwaysIncludeDeals', function () { let bid4; diff --git a/test/spec/unit/pbjs_api_spec.js b/test/spec/unit/pbjs_api_spec.js index 960ccf08c92..afad1d665fd 100644 --- a/test/spec/unit/pbjs_api_spec.js +++ b/test/spec/unit/pbjs_api_spec.js @@ -1197,6 +1197,14 @@ describe('Unit: Prebid Module', function () { assert.deepEqual($$PREBID_GLOBAL$$.getAllWinningBids()[0], adResponse); }); + it('should replace ${CLICKTHROUGH} macro in winning bids response', function () { + pushBidResponseToAuction({ + ad: "" + }); + $$PREBID_GLOBAL$$.renderAd(doc, bidId, {clickThrough: 'https://someadserverclickurl.com'}); + expect(adResponse).to.have.property('ad').and.to.match(/https:\/\/someadserverclickurl\.com/i); + }); + it('fires billing url if present on s2s bid', function () { const burl = 'http://www.example.com/burl'; pushBidResponseToAuction({ @@ -1765,6 +1773,32 @@ describe('Unit: Prebid Module', function () { expect(auctionArgs.adUnits[0].mediaTypes.native.icon).to.exist; assert.ok(logErrorSpy.calledWith('Please use an array of sizes for native.icon.sizes field. Removing invalid mediaTypes.native.icon.sizes property from request.')); }); + + it('should throw error message and remove adUnit if adUnit.bids is not defined correctly', function () { + const adUnits = [{ + code: 'ad-unit-1', + mediaTypes: { + banner: { + sizes: [300, 400] + } + }, + bids: [{code: 'appnexus', params: 1234}] + }, { + code: 'bad-ad-unit-2', + mediaTypes: { + banner: { + sizes: [300, 400] + } + } + }]; + + $$PREBID_GLOBAL$$.requestBids({ + adUnits: adUnits + }); + expect(auctionArgs.adUnits.length).to.equal(1); + expect(auctionArgs.adUnits[1]).to.not.exist; + assert.ok(logErrorSpy.calledWith("Detected adUnit.code 'bad-ad-unit-2' did not have 'adUnit.bids' defined or 'adUnit.bids' is not an array. Removing adUnit from auction.")); + }); }); }); }); diff --git a/test/spec/video_spec.js b/test/spec/video_spec.js index 72a585049c3..3ce8ba081da 100644 --- a/test/spec/video_spec.js +++ b/test/spec/video_spec.js @@ -71,6 +71,29 @@ describe('video.js', function () { expect(valid).to.equal(true); }); + it('validates valid outstream bids with a publisher defined renderer', function () { + const bid = { + requestId: '123abc', + }; + const bidRequests = [{ + bids: [{ + bidId: '123abc', + bidder: 'appnexus', + mediaTypes: { + video: { + context: 'outstream', + renderer: { + url: 'render.url', + render: () => true, + } + } + } + }] + }]; + const valid = isValidVideoBid(bid, bidRequests); + expect(valid).to.equal(true); + }); + it('catches invalid outstream bids', function () { const bid = { requestId: '123abc'